tags:

views:

1330

answers:

5

Hi,

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?

Regards

+8  A: 

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.

Allen Bauer
+2  A: 

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;
François
+6  A: 

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
  { Private declarations }
  FRec : TRec;
public
  { Public declarations }
  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.

Gamecat
+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*).
Jeroen Pluimers
+4  A: 

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.

Barry Kelly
A: 

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.

otherchirps