Delphi, record type property, record field assignment: Assignment to local copy of record expected
Asked Answered
B

1

10

Over at the question “Left side cannot be assigned to” for record type properties in Delphi, there is an answer from Toon Krijthe demonstrating how assignments to fields of a record property can be done by using properties in the declaration of the record. For easier reference, here is the code snippet published by Toon Krijthe.

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;

It is clear to me why the "Left side cannot be assigned to" error is raised in the original code of vcldeveloper without the setter in the record. It is also clear to me why no error is raised for the assignment Rec.A := 21; if a setter is defined for the property TRec.A like in the case of the code above.

What I do not understand is why the assignment Rec.A := 21; assigns the value 21 to the field FRec.FA of TForm1. I would have expected that the value is assigned to the field FA of a local temporary copy of FRec but not FRec.FA itself. Could anyone please shed some light on what is happening here?

Binford answered 5/3, 2019 at 9:17 Comment(0)
B
10

This is a great question!

The behaviour you see is a consequence of the implementation details for properties. The way the compiler implements properties differs for direct field property getters and for function property getters.

When you write

Rec.A := 21;

The compiler sees Rec and knows that it is a property. Since the getter is a direct field getter, the compiler simply replaces Rec with FRec and compiles the code exactly as if you had written

FRec.A := 21;

The compiler then encounters the A property and uses the setter method, and so your assignment becomes

FRec.SetA(21);

Hence the behaviour that you observed.

Suppose that instead of a direct field getter you had a function getter

property Rec: TRec read GetRec;
....
function TForm1.GetRec: TRec;
begin
  Result := FRec;
end;

In that scenario the handling of

Rec.A := 21;

changes. The compiler instead declares an implicit local variable and the code is compiled like this:

var
  __local_rec: TRec;
....
__local_rec := GetRec;
__local_rec.A := 21;

It seems obvious to me that the behaviour of such a program should not depend on whether the property getter is a direct field getter or a function getter. This seems like a design flaw in the interaction between the property feature and the enhanced records feature.


Here is a complete program that demonstrates the issue very succinctly:

{$APPTYPE CONSOLE}

type
  TRec = record
  private
    FA: Integer;
    procedure SetA(const Value: integer);
  public
    property A: integer read FA write SetA;
  end;

procedure TRec.SetA(const Value: integer);
begin
  FA := Value;
end;

type
  TMyClass = class
  private
    FRec: TRec;
    function GetRec: TRec;
  public
    property RecDirect: TRec read FRec;
    property RecFunction: TRec read GetRec;
  end;

var
  Obj: TMyClass;

function TMyClass.GetRec: TRec;
begin
  Result := FRec;
end;

begin
  Obj := TMyClass.Create;
  Obj.RecDirect.A := 21;
  Writeln(Obj.FRec.FA);

  Obj := TMyClass.Create;
  Obj.RecFunction.A := 21;
  Writeln(Obj.FRec.FA);
end.

Output

21
0
Biedermeier answered 5/3, 2019 at 9:28 Comment(11)
Er... no: "and compiles the code exactly as if you had written FRec.A := 21;". It uses the setter and that is SetA, so it calls FRec.SetA(21);. Don't confuse the reader that much.Thumbtack
@Rudy OK, I was concentrating on how the Rec property was handled, and did not get into expanding the second A property. But my statement is still true in the sense that Rec.A := 21 is compiled in the same was as FRec.A := 21 is compiled. It happens that both are converted into FRec.SetA(21). I'll expand on that. I'm sorry that you found it confusing.Biedermeier
Thanks! Am I correclty deducing that you would avoid writing code that explicitely depends on that behavior from your remark "It seems obvious to me that..." above? Personally, I would file this under "nice to know, be aware of, but better not use".Binford
Yeah, you'd definitely want to avoid writing code that relied on this. One day you may innocently change a getter from direct field getter to function getter and find a very surprising consequence. Really, this issue comes about from having enhanced records whose state is modified by their methods. Without that I don't think this issue can arise. And properties were implemented before enhanced methods existed. I personally try very hard to avoid writing methods on enhanced records that modify the record state.Biedermeier
FWIW, I didn't read it recently, but in the olden days (Delphi 1 and 2), the Object Pascal Language Guide contained a warning that one should always treat such a value as temporary, i.e. assign it to a local variable, change the fields of that local variable and then write the value back. It was not enforced by the compiler, just highly recommended in the docs.Thumbtack
@RudyVelthuis Compiler now blocks that, but the game changer here are enhanced records with methods that can modify the record. Compiler doesn't block them and so we are back where we started.Biedermeier
@David: So the old recommendation is still valid.Thumbtack
@RudyVelthuis Personally my recommendation is not to have methods that modify the recordBiedermeier
@David: but that is one of the main purposes of methods, isn't it? If you want to avoid that a copy is modified, have properties that return pointers to the actual records. Or just document the problem and hand the responsibility over to the user of your class.Thumbtack
@RudyVelthuis Mutable value types do have downsides as I am sure you know. Allowing methods to mutate them leads to the problem discussed here, and other similar issues. Avoiding record methods that mutate the record is my way of avoiding these issues.Biedermeier
@David: I agree that it is best to have immutable value types. Methods that produce a new value should be functions that return a new instance, and not procedures that modify self. And you can enforce that if you write the classes yourself, but not on records wirtten by others.Thumbtack

© 2022 - 2024 — McMap. All rights reserved.