I have a class which stores generic items in an array. These items are accessed by a default array property:
TMyList<TData> = class
private
items: array of TItem<TData>;
public
function get(position: integer): TData;
procedure edit(position: integer; data: TData);
property Values[position: integer]: TData read get write edit; default;
end;
implementation
function TMyList<TData>.get(position: integer): TData;
begin
result:= items[position];
end;
procedure TMyList<TData>.edit(position: integer; data: TData);
var
item: TItem<TData>;
begin
items[position]:= item;
end;
end;
In this case the items I am storing are all of the type TTest which also has its own properties:
TTest = record
private
FTest: string;
procedure setFTest(const Value: string);
public
property Test: string read FTest write setFTest;
end;
implementation
procedure TColouredPoint.setFTest(const Value: String);
begin
FTest:= Value;
end;
end;
I want to be able to change the value of FTest for an instance of TTest like this:
var
points: TMyList<TTest>;
...
points[index].Test:= 'test';
but this doesn't do anything. There is no error message but the value of points[index].Test doesn't change.
Instead I have to do this:
var
points: TMyList<TTest>;
temp: TTest;
...
temp:= points[index];
temp.Test:= 'test';
points[index]:= temp;
Why does the first version not work?
Why does the first version not work?
Consider this code
points[index].Test := 'test';
The indexed property is converted by the compiler into the a function call and so the compiler effectively compiles this:
points.get(index).Text := 'test';
Now, points.get(index) returns a copy of the TTest value. Since you don't assign that to anything, the compiler introduces a local variable to hold the return value.
So your code becomes, in effect:
var
tmp: TTest;
...
tmp := points.get(index);
tmp.Text := 'test';
That's the last thing that is ever done with tmp, and so the modifications that you make to tmp.Text are simply discarded leaving the underlying object untouched.
This issue is pretty hard to get around when working with value types.
The generic Delphi TList<T> collection allows you direct access to the underlying array, which allows you to operate on the stored values directly.
Another approach is to use a reference rather than a value. One simple way to achieve that is to use a T that is a class rather than a record, i.e. a reference type rather than a value type.
It is because TTest is a record. The getter of the list returns a copy of the actual record and that is what you are changing. It should work when you declare TTest as a class, but then you have to take care of creating and destroying it.
Related
Delphi RIO - have defined a class called TBizObj. One of the properties has to do with DUNS numbers. DUNS numbers are sometimes '0' padded on the left to be exactly 9 characters long, so I have a property called SiteDUNS9 (based off of fSiteDUNS9). The calling program sets the SiteDUNS9 property, but I don't the caller to have to worry about if the DUNS is 9 characters or not, I will handle that in getter/setter properties.
When I define my property to call this function, I get an error 'Incompatible types'. Everything is string... no other types involved. Here is the relevant portion of code:
type
TBizObj = class(TObject)
private
...
fSiteDUNS9: string;
...
function FixDunsLength9(DUNS:string) :string;
published
...
property SiteDUNS9: string read fSiteDUNS9 write FixDunsLength9;
end; // End of the tBizObj Class;
implementation
...
function TBizObj.FixDunsLength9(DUNS:string):string;
begin
// This is a setter function for the DUNS9 routine
result := glib_LeftPad(DUNS, 9, '0');
end;
I have followed the examples on the Embaracadero site but still cannot determine what I am doing wrong.
http://docwiki.embarcadero.com/RADStudio/Rio/en/Properties_(Delphi)
If I change my property definition to
property SiteDUNS9: string read fSiteDUNS9 write fSiteDUNS9;
then my program compiles correctly.
You need to use a procedure instead of a function for a property setter. I would leave the existing function as-is, in case you need it for other purposes, and define a separate procedure for the setter:
type
TBizObj = class(TObject)
private
...
fSiteDUNS9: string;
...
function FixDunsLength9(const DUNS: string): string;
procedure SetSiteDUNS9(const Value: string);
published
...
property SiteDUNS9: string read fSiteDUNS9 write SetSiteDUNS9;
end;
// End of the tBizObj Class;
implementation
...
function TBizObj.FixDunsLength9(const DUNS: string): string;
begin
Result := glib_LeftPad(DUNS, 9, '0');
end;
procedure TBizObj.SetSiteDUNS9(const Value: string);
var
NewValue: string;
begin
NewValue := FixDunsLength9(Value);
if fSiteDUNS9 <> NewValue then
begin
fSiteDUNS9 := NewValue;
...
end;
end;
You need to declare a procedure for the setter method. As the Property Access help says:
write fieldOrMethod
In a write specifier, if fieldOrMethod is a method, it must be a
procedure that takes a single value or const parameter of the same
type as the property (or more, if it is an array property or indexed
property).
In your case you can then write a setter like this:
type
TBizObj = class(TObject)
private
FSiteDUNS9: string;
procedure FixDunsLength9(const DUNS: string);
published
property SiteDUNS9: string read FSiteDUNS9 write FixDunsLength9;
end;
implementation
procedure TBizObj.FixDunsLength9(const DUNS: string);
begin
if DUNS <> FSiteDUNS9 then
begin
DoSomeExtraStuff;
FSiteDUNS9 := DUNS;
end;
end;
But following the naming conventions I would recommend you to name your setter like SetSiteDUNS9 and the parameter call Value.
I'm trying to get variable name using RTTI like this.
Here is my test.
type
TStringHelper = record helper for string
function Name: string;
end;
TMyRecord = record
Field1:string;
end;
implementation
{ TStringHelper }
function TStringHelper.Name: string;
var
context : TRttiContext;
begin
context := TRttiContext.Create;
result := context.GetType(#Self).Name; // return empty
context.Free;
end;
procedure TForm2.FormCreate(Sender: TObject);
var
r : TMyRecord;
begin
ShowMessage(r.Field1.Name);
end;
Name of TRttiType returning is empty.
Is there any way get variable name ?
RTTI gives information about types and not about variables. In general there is no way, using RTTI, given the address of a variable, to find its name.
Not only does RTTI not help, but what you are attempting, as a method of a string object, is not actually possible. Imagine a scenario where you have two variables referring to the same object.
S := 'foo';
T := S;
What is the name of the single string object here. Is it S or is it T?
Arrays can be indexed using user-defined enumerated types. For example:
type
TIndexValue = (ZERO = 0, ONE, TWO, THREE, FOUR);
var
MyArray: array[Low(TIndexValue) .. High(TIndexValue)] of String;
Elements from this array can then be referenced using TIndexValue values as an index:
MyArray[ZERO] := 'abc';
I am trying to obtain this same general functionality with a TStringList.
One simple solution is to cast every index value to an Integer type at the time of reference:
MyStringList[Integer(ZERO)] := 'abc';
Another solution (to hide all the casting) is to create a subclass of TStringList and defer all the casting to this subclass's subroutines that access the inherited Strings property:
type
TIndexValue = (ZERO = 0, ONE, TWO, THREE, FOUR);
type
TEIStringList = class(TStringList)
private
function GetString(ItemIndex: TIndexValue): String;
procedure SetString(ItemIndex: TIndexValue; ItemValue: String);
public
property Strings[ItemIndex: TIndexValue]: String
read GetString write SetString; default;
end;
function TEIStringList.GetString(ItemIndex: TIndexValue): String;
begin
Result := inherited Strings[Integer(ItemIndex)];
end;
procedure TEIStringList.SetString(ItemIndex: TIndexValue; ItemValue: String);
begin
inherited Strings[Integer(ItemIndex)] := ItemValue;
end;
This works fine for a single implementation that uses the enumerated type TIndexValue.
However, I would like to re-use this same logic or subclass for several different TStringList objects that are indexed by different enumerated types, without having to define TStringList subclasses for each possible enumerated type.
Is something like this possible? I suspect I may have to depend on Delphi's Generics, but I would be very interested to learn that there are simpler ways to achieve this.
I think that generics would be by far the most elegant solution. Using them would be as simple as rewriting your class above as:
TEIStringList<T> = class(TStringList)
and then replacing all TIndexValue references with T. Then you could create it just as any other generic:
var
SL: TEIStringList<TIndexValue>;
begin
SL:=TEIStringList<TIndexValue>.Create;
(...)
ShowMessage(SL[ZERO])
(...)
end;
If you insist on avoiding generics, maybe operator overloading would be of use. Something like the following should work:
type
TIndexValueHolder = record
Value : TIndexValue;
class operator Implicit(A: TMyRecord): integer;
end;
(...)
class operator TIndexValueHolder.Implicit(A: TMyRecord): integer;
begin
Result:=Integer(A);
end;
Then use with:
var
Inx : TIndexValueHolder;
begin
Inx.Value:=ZERO;
ShowMessage(SL[Inx]);
end
UPDATE:
You could adapt TIndexValueHolder for use in a for or while loop by adding Next, HasNext, etc. methods. This might end defeating the purpose, though. I'm still not sure what the purpose is, or why this would be useful, but here's some ideas for how to do it, anyways.
You probably can use a class helper and declare the default property index as Variant:
type
TEnum1 = (Zero = 0, One, Two, Three, Four);
TEnum2 = (Nul = 0, Een, Twee, Drie, Vier);
TEnum3 = (Gds = 0, Psajs, Oeroifd, Vsops, Wowid);
TStringListHelper = class helper for TStringList
private
function GetString(Index: Variant): String;
procedure SetString(Index: Variant; const Value: String);
public
property Strings[Index: Variant]: String read GetString write SetString;
default;
end;
function TStringListHelper.GetString(Index: Variant): String;
begin
Result := inherited Strings[Index];
end;
procedure TStringListHelper.SetString(Index: Variant; const Value: String);
begin
inherited Strings[Index] := Value;
end;
Testing code:
procedure TForm1.Button1Click(Sender: TObject);
var
Strings: TStringList;
begin
Strings := TStringList.Create;
try
Strings.Add('Line 1');
Strings.Add('Second line');
Strings[Zero] := 'First line';
Memo1.Lines.Assign(Strings);
Caption := Strings[Psajs];
finally
Strings.Free;
end;
end;
See edit history for a previous less successful attempt.
I'd like to make a record as an object's property. The problem is that when I change one of the fields of this record, the object isn't aware of the change.
type
TMyRecord = record
SomeField: Integer;
end;
TMyObject = class(TObject)
private
FSomeRecord: TMyRecord;
procedure SetSomeRecord(const Value: TMyRecord);
public
property SomeRecord: TMyRecord read FSomeRecord write SetSomeRecord;
end;
And then if I do...
MyObject.SomeRecord.SomeField:= 5;
...will not work.
So how do I make the property setting procedure 'catch' when one of the record's fields is written to? Perhaps some trick in how to declare the record?
More Info
My goal is to avoid having to create a TObject or TPersistent with an OnChange event (such as the TFont or TStringList). I'm more than familiar with using objects for this, but in an attempt to cleanup my code a little, I'm seeing if I can use a Record instead. I just need to make sure my record property setter can be called properly when I set one of the record's fields.
Consider this line:
MyObject.SomeRecord.SomeField := NewValue;
This is in fact a compile error:
[DCC Error]: E2064 Left side cannot be assigned to
Your actual code is probably something like this:
MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;
What happens here is that you copy the value of the record type to the local variable MyRecord. You then modify a field of this local copy. That does not modify the record held in MyObject. To do that you need to invoke the property setter.
MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;
MyObject.SomeRecord := MyRecord;
Or switch to using a reference type, i.e. a class, rather than a record.
To summarise, the problem with your current code is that SetSomeRecord is not called and instead you are modifying a copy of the record. And this is because a record is a value type as opposed to being a reference type.
Ultimately you will want to access the record's fields, yet as you propose, a record is often a suitable abstraction choice within a class. A class can neatly access the properties of a record as follows:
type
TMyRec = record
SomeRecInteger: integer;
SomeRecString: string;
end;
TMyClass = class(TObject)
private
FMyRec: TMyRec;
procedure SetSomeString(const AString: string);
public
property SomeInteger: integer read FMyRec.SomeRecInteger write FMyRec.SomeRecInteger;
property SomeString: string read FMyRec.SomeRecString write SetSomeString;
end;
procedure TMyClass.SetSomeRecString(const AString: string);
begin
If AString <> SomeString then
begin
// do something special if SomeRecString property is set
FMyRec.SomeRecString := AString;
end;
end;
Note:
The direct access to the record property SomeRecInteger
The use of SetSomeRecString to implement some special processing on this field only.
Hope this helps.
How about using a TObject instead of a Record?
type
TMyProperties = class(TObject)
SomeField: Integer;
end;
TMyObject = class(TObject)
private
FMyProperties: TMyProperties;
public
constructor Create;
destructor Destroy; override;
property MyProperties: TMyRecord read FMyProperties;
end;
implementation
constructor TMyObject.Create;
begin
FMyProperties := TMyProperties.Create;
end;
destructor TMyObject.Destroy;
begin
FMyProperties.Free;
end;
You can now read and set the properties of TMyProperties like this:
MyObject.MyProperties.SomeField := 1;
x := MyObject.MyProperties.SomeField;
Using this method, you don't need to individually get/set the values to/from the record. If you need to catch changes in FMyProperties, you can add a 'set' procedure in the property declaration.
Why not make the setter/getter part of the record?
TMyRecord = record
fFirstname: string;
procedure SetFirstName(AValue: String);
property
Firstname : string read fFirstname write SetFirstName;
end;
TMyClass = class(TObject)
MyRecord : TMyRecord;
end;
procedure TMyRecord.SetFirstName(AValue: String);
begin
// do extra checking here
fFirstname := AValue;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
try
MyClass.MyRecord.Firstname := 'John';
showmessage(MyClass.MyRecord.Firstname);
finally
MyClass.Free;
end;
end;
You're passing the record by value, so a copy of the record is stored by the object. From that point on there are effectively two objects; the original one and the copy held by the object. Changing one won't change the other.
You need to pass the record by reference.
type
TMyRecord = record
SomeField: Integer;
end;
PMyRecord = ^TMyRecord;
TMyObject = class(TObject)
private
FSomeRecord: PMyRecord;
procedure SetSomeRecord(const Value: PMyRecord);
public
property SomeRecord: PMyRecord read FSomeRecord write SetSomeRecord;
end;
this is an alternative to #SourceMaid's answer.
You could use a record (not a pointer to a record) inside your object and have a read only property which returns a pointer to your record.
the class:
type
TMyRecord = record
I:Integer;
end;
PMyRecord = ^TMyRecord;
TMyObject = class
private
FMyRecord:TMyRecord;
function GetMyRecordPointer: PMyRecord;
public
property MyRecord: PMyRecord read GetMyRecordPointer;
end;
the getter:
function TMyObject.GetMyRecordPointer: PMyRecord;
begin
result := #FMyRecord;
end;
usage:
o := TMyObject.Create;
o.MyRecord.I := 42;
ShowMessage(o.MyRecord.I.ToString);
o.MyRecord.I := 23;
ShowMessage(o.MyRecord.I.ToString);
o.Free;
you dont need a setter because you get a reference and work with. that mean that you cannot change the entire record by assigning a new one.
but you can manipulate the elements of the record directly without getting the error "Left side cannot be assigned to".
type
TStaticArray = array[1..10] of integer;
TDynamicArray = array of integer;
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
FDynamicArray: TDynamicArray;
published
property staticArray: TStaticArray read FStaticArray write FStaticArray; //compiler chokes on this
property dynamicArray: TDynamicArray read FDynamicArray write FDynamicArray; //compiler accepts this one just fine
end;
What's going on here? A static array gives the error, "published property 'staticArray' cannot be of type ARRAY" but dynamic arrays are just fine? I'm confused. Anyone know the reasoning behind this, and how I can work around it? (And no, I don't want to redeclare all my static arrays as dynamic. They're the size they are for a reason.)
Published declaration tells the compiler to store information in the virtual method table. Only certain kinds of information can be stored.
The type of a published property cannot be a pointer, record, or array. If it is a set type, it must be small enough to be stored in an integer.
(O'REILLY, DELPHÄ° IN A NUTSHELL)
You have to have getters and setters. Under D2009 (didn't check other versions), the parameters to the getters/setters can't, for some reason, be const. ?
This works fine under D2009:
type
TMyArray = array[0..20] of string;
type
TMyClass=class(TObject)
private
FMyArray: TMyArray;
function GetItem(Index: Integer): String;
procedure SetItem(Index: Integer; Value: string);
public
property Items[Index: Integer]: string read GetItem write SetItem;
end;
implementation
function TMyClass.GetItem(Index: Integer): string;
begin
Result := '';
if (Index > -1) and (Index < Length(FMyArray)) then
Result := FMyArray[Index];
end;
procedure TMyClass.SetItem(Index: Integer; Value: string);
begin
if (Index > -1) and (Index < Length(FMyArray)) then
FMyArray[Index] := Value;
end;
NOTE: I would not typically just ignore Index values out of range, obviously. This was a quick example of how to make static array properties in a class definition; IOW, it's a compilable example only.
The reason why you can publish a dynamic array property, is that dynamic arrays is implemented as references, or 'implicitly pointer'. They work more like strings, really.
The reason why you can't publish a static array, I don't know. It's just the way it's made, I guess..
For more details on how dynamic arrays work, take a look at DrBobs site
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
FIdx: Integer;
function GetFoo(Index: Integer): Integer;
procedure SetFoo(Index: Integer; Value: Integer);
public
property Foo[Index: Integer] : Integer read GetFoo write SetFoo;
published
property Idx: Integer read fidx write fidx;
property AFoo: Integer read GetAFoo writte SetAFoo;
end;
implementation
function TMyClass.GetAFoo: Integer;
begin
result := FStaticArray[FIdx];
end;
procedure TMyClass.SetAFoo(Value: Integer);
begin
FStaticArray[FIdx] := Value;
end;
Array properties cannot be published.
So the following code does not work. The work around probably is to make it public.
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
function GetFoo(Index: Integer): Integer;
procedure SetFoo(Index: Integer; Value: Integer);
public
property Foo[Index: Integer] : Integer read GetFoo write SetFoo;
end;