"Left side cannot be assigned to" for record type properties in Delphi
I'm curious to know why Delphi treats record type properties as read only:
TRec = record A : integer; B : string; end; TForm1 = class(TForm) private FRec : TRec; public procedure DoSomething(ARec: TRec); property Rec : TRec read FRec write FRec; end;
If I try to assign a value to any of the members of Rec property, I'll get "Left side cannot be assigned to" error:
procedure TForm1.DoSomething(ARec: TRec); begin Rec.A := ARec.A; end;
while doing the same with the underlying field is allowed:
procedure TForm1.DoSomething(ARec: TRec); begin FRec.A := ARec.A; end;
Is there any explanation for that behavior?
Since "Rec" is a property, the compiler treats it a little differently because it has to first evaluate the "read" of the property decl. Consider this, which is semantically equivalent to your example:
... property Rec: TRec read GetRec write FRec; ...
If you look at it like this, you can see that the first reference to "Rec" (before the dot '.'), has to call GetRec, which will create a temporary local copy of Rec. These temporaries are by design "read-only." This is what you're running into.
Another thing you can do here is to break out the individual fields of the record as properties on the containing class:
... property RecField: Integer read FRec.A write FRec.A; ...
This will allow you to directly assign through the property to the field of that embedded record in the class instance.
Yes this is a problem. But the problem can be solved using record properties:
type TRec = record private FA : integer; FB : string; procedure SetA(const Value: Integer); procedure SetB(const Value: string); public property A: Integer read FA write SetA; property B: string read FB write SetB; end; procedure TRec.SetA(const Value: Integer); begin FA := Value; end; procedure TRec.SetB(const Value: string); begin FB := Value; end; TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private FRec : TRec; public property Rec : TRec read FRec write FRec; end; procedure TForm1.Button1Click(Sender: TObject); begin Rec.A := 21; Rec.B := 'Hi'; end;
This compiles and workes without problem.
A solution I frequently use is to declare the property as a pointer to the record.
type PRec = ^TRec; TRec = record A : integer; B : string; end; TForm1 = class(TForm) private FRec : TRec; function GetRec: PRec; procedure SetRec(Value: PRec); public property Rec : PRec read GetRec write SetRec; end; implementation function TForm1.GetRec: PRec; begin Result := @FRec; end; procedure TForm1.SetRec(Value: PRec); begin FRec := Value^; end;
With this, directly assigning Form1.Rec.A := MyInteger
will work, but also Form1.Rec := MyRec
will work by copying all the values in MyRec
to the FRec
field as expected.
The only pitfall here is that when you wish to actually retrieve a copy of the record to work with, you will have to something like MyRec := Form1.Rec^
The compiler is stopping you from assigning to a temporary. The equivalent in C# is permitted, but it has no effect; the return value of the Rec property is a copy of the underlying field, and assigning to the field on the copy is a nop.
Because you have implicit getter and setter functions and you cannot modify the Result of a function as it is a const parameter.
(Note: In case you transform the record in an object, the result would actually be a pointer, thus equivalent to a var parameter).
If you want to stay with a Record, you have to use an intermediate variable (or the Field variable) or use a WITH statement.
See the different behaviors in the following code with the explicit getter and setter functions:
type TRec = record A: Integer; B: string; end; TForm2 = class(TForm) private FRec : TRec; FRec2: TRec; procedure SetRec2(const Value: TRec); function GetRec2: TRec; public procedure DoSomething(ARec: TRec); property Rec: TRec read FRec write FRec; property Rec2: TRec read GetRec2 write SetRec2; end; var Form2: TForm2; implementation {$R *.dfm} { TForm2 } procedure TForm2.DoSomething(ARec: TRec); var LocalRec: TRec; begin // copy in a local variable LocalRec := Rec2; LocalRec.A := Arec.A; // works // try to modify the Result of a function (a const) => NOT ALLOWED Rec2.A := Arec.A; // compiler refused! with Rec do A := ARec.A; // works with original property and with! end; function TForm2.GetRec2: TRec; begin Result:=FRec2; end; procedure TForm2.SetRec2(const Value: TRec); begin FRec2 := Value; end;
Comments
- +1 Note your solution is not bad, but users of it need to remember that if they ever change the property to "property Rec : TRec read GetRec write FRec;", the assignment trick will fail miserably (because GetRec will return a copy as records are value types).
- The Rec property in TForm1 can be read only if only read/write access to the record's properties are required. The key part to this solution are the setter methods in the record's properties.
- I think you're right - there's no point using properties for records, it seems like a lot of work...just have a procedure that does something to a record: SetSomething(var ARec: TRec)