22
votes

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?

8

8 Answers

38
votes

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.

22
votes

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.

10
votes

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^

8
votes

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.

4
votes

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;
3
votes

This is because property are actually complied as a function. Properties only return or set a value. It is not a reference or a pointer to the record

so :

Testing.TestRecord.I := 10;  // error

is same as calling a function like this:

Testing.getTestRecord().I := 10;   //error (i think)

what you can do is:

r := Testing.TestRecord;    // read
r.I := 10;
Testing.TestRecord := r;    //write

It is a bit messy but inherent in this type of architecture.

2
votes

Like others have said - the read property will return a copy of the record, so the assignment of fields isn't acting on the copy owned by TForm1.

Another option is something like:

  TRec = record
    A : integer;
    B : string;
  end;
  PRec = ^TRec;

  TForm1 = class(TForm)
  private
    FRec : PRec;
  public
    constructor Create;
    destructor Destroy; override;

    procedure DoSomething(ARec: TRec);
    property Rec : PRec read FRec; 
  end;

constructor TForm1.Create;
begin
  inherited;
  FRec := AllocMem(sizeof(TRec));
end;

destructor TForm1.Destroy;
begin
  FreeMem(FRec);

  inherited;
end;

Delphi will dereference the PRec pointer for you, so things like this will still work:

Form1.Rec.A := 1234; 

There's no need for a write part of the property, unless you want to swap the PRec buffer that FRec points at. I really wouldn't suggest to do such swapping via a property anyway.

2
votes

The simplest approach is:

procedure TForm1.DoSomething(ARec: TRec);
begin
  with Rec do
    A := ARec.A;
end;