Title: Troubleshooting Delphi's "Left Side Cannot Be Assigned To" when using Record as a Property
According to the "Proper using of a Record Field/Property in a Class" poll, only 20+% of Delphi developers know what usage of a Record Delphi type as a Property in a Class declaration will produce a compile time error while trying to assign a value to a record's field.
Ok, I'll presume many answered without trying to actually compile the code. If after compiling the code you asked yourself: "huh, why does it not compile?!?", here's the answer:
A few Class Declaration using a Record
Take a look at the following declarations:
type
TInnerRecord = record
Name : string;
end;
//VERSION A) record as field
TTestClassA = class
InnerRecord : TInnerRecord;
end;
//VERSION B) record as read-only property
TTestClassB = class
private
fInnerRecord : TInnerRecord;
public
property InnerRecord : TInnerRecord read fInnerRecord;
end;
//VERSION C) record as property
TTestClassC = class
private
fInnerRecord : TInnerRecord;
public
property InnerRecord : TInnerRecord read fInnerRecord write fInnerRecord;
end;
TTestClassA exposes InnerRecord as a public field.
TTestClassB exposes InnerRecord as a read-only public property.
TTestClassC exposes InnerRecord as a read-write public property.
"Left Side Cannot Be Assigned To"?
The following code will produce a compile-time "Left Side Cannot Be Assigned To" error:
var
testB : TTestClassB;
testC : TTestClassC;
begin
//presume testB and testC are properly created
//and will be freed testB.InnerRecord.Name := 'Delphi'; //ERROR!
testC.InnerRecord.Name := 'Delphi'; //ERROR!
end;
Therefore, in the mentioned poll, the correct answer is "Versions B and C".
If you think version "testB" does not compile because InnerRecord is a read-only property, your are *wrong*!
The code above (for testB) does not try to assign a value to the InnerRecord property, but to the "Name" field of the InnerRecord property that is not read-only!
The "left side cannot be assigned to" error message is given by the Delphi compiler when you try to modify a read-only object like a constant, a constant parameter, or the return value of function.
We are not modifying a read-only object, InnerRecord is not a constant so it must be that we are trying to modify the return value of a function! But what function?!
What we have in versions B and C, is a call to "test.InnerRecord" that calls the getter function for the InnerRecord property - so it must be that we are trying to modify the return value of a function - "function GetInnerRecord : TInnerRecord"!
test.InnerRecord.Name := 'Delphi' //is equivalent to test.InnerRecord_GETTER.Name := 'Delphi'
Even though you have not explicitly created the getter method yourself - Delphi does that internally.
The "test.InnerRecord" property is returning a copy of the property value. Referring to a property is then referring to a property on the *copy*, and if the compiler allowed it, it would be a meaningless statement.
Another problem compiler has is "are you trying to call the property getter or the property setter"? Delphi compiler does not allow calling the setter implicitly. This is why you cannot write: "Inc(form1.Left)" since this would require calling the getter and setter for the property.
Proposed Solution: Transform a Record into a Class
If InnerRecord was declared as
type TInnerRecord = class (TObject)
Therefore an object not a record, all three versions would compile!
However, you would, of course, have to create and free the object to make sure to prevent memory leaks.
For object-properties a getter and a setter return a pointer to the object - not the object itself.
For value-properties (record is a value type) calling the getter is like working on a temporary copy.
But I want to have a Record as a Property!
If you expose a record type variable as a property, and want to modify its fields, you need to either use the with statement or mark it read-write and assign a value to the entire InnerRecord at once (using some "private" TInnerRecord variable):
var
testB : TTestClassC;
testC : TTestClassC; ir : TInnerRecord;
begin
ir.Name := 'Delphi';
//will NOT copile - InnerRecord in READ-ONLY
testB.InnerRecord := ir;
// will compile
testC.InnerRecord := ir;
// InnerRecord.Name = 'Delphi'!
Interestingly, the "with" version of the code WILL compile for both the TTestClassB and the TTestClassC:
with testB.InnerRecord do //or "testC"
begin
Name := 'Delphi';
end;