Delphi: Records in Classes
Following situation:
type
TRec = record
Member : Integer;
end;
TMyClass = cla开发者_StackOverflowss
private
FRec : TRec;
public
property Rec : TRec read FRec write FRec;
end;
The following doesn't work (left side cannot be assigned to), which is okay since TRec
is a value type:
MyClass.Rec.Member := 0;
In D2007 though the following DOES work:
with MyClass.Rec do
Member := 0;
Unfortunately, it doesn't work in D2010 (and I assume that it doesn't work in D2009 either). First question: why is that? Has it been changed intentionally? Or is it just a side effect of some other change? Was the D2007 workaround just a "bug"?
Second question: what do you think of the following workaround? Is it safe to use?
with PRec (@MyClass.Rec)^ do
Member := 0;
I'm talking about existing code here, so the changes that have to be made to make it work should be minimal.
Thanks!
That
MyClass.Rec.Member := 0;
doesn't compile is by design. The fact that the both "with"-constructs ever compiled was (AFAICT) a mere oversight. So both are not "safe to use".
Two safe solution are:
- Assign
MyClass.Rec
to a temporary record which you manipulate and assign back toMyClass.Rec
. - Expose
TMyClass.Rec.Member
as a property on its own right.
In some situtations like this where a record of a class needs 'direct manipulation' I've often resorted to the following:
PMyRec = ^TMyRec;
TMyRec = record
MyNum : integer
end;
TMyObject = class( TObject )
PRIVATE
FMyRec : TMyRec;
function GetMyRec : PMyRec;
PUBLIC
property MyRec : PMyRec << note the 'P'
read GetMyRec;
end;
function TMyObject.GetMyRec : PMyRec; << note the 'P'
begin
Result := @FMyRec;
end;
The benefit of this is that you can leverage the Delphi automatic dereferencing to make readable code access to each record element viz:
MyObject.MyRec.MyNum := 123;
I cant remember, but maybe WITH works with this method - I try not to use it! Brian
The reason why it can't be directly assigned is here.
As for the WITH, it still works in D2009 and I would have expected it to work also in D2010 (which I can't test right now).
The safer approach is exposing the record property directly as Allen suggesed in the above SO post:
property RecField: Integer read FRec.A write FRec.A;
Records are values, they aren't meant to be entities.
They even have assignment-by-copy semantics! Which is why you can't change the property value in-place. Because it would violate the value type semantics of FRec and break code that relied on it being immutable or at least a safe copy.
They question here is, why do you need a value (your TRec) to behave like an object/entity?
Wouldn't it be much more appropriate for "TRec" to be a class if that is what you are using it for, anyways?
My point is, when you start using a language feature beyond its intent, you can easily find yourself in a situation where you have to fight your tools every meter on the way.
The reason it has been changed is that it was a compiler bug. The fact that it compiled didn't guarantee that it would work. It would fail as soon as a Getter was added to the property
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm2 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
FPoint: TPoint;
function GetPoint: TPoint;
procedure SetPoint(const Value: TPoint);
{ Private declarations }
public
{ Public declarations }
property Point : TPoint read GetPoint write SetPoint;
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
procedure TForm2.Button1Click(Sender: TObject);
begin
with Point do
begin
X := 10;
showmessage(IntToStr(x)); // 10
end;
with Point do
showmessage(IntToStr(x)); // 0
showmessage(IntToStr(point.x)); // 0
end;
function TForm2.GetPoint: TPoint;
begin
Result := FPoint;
end;
procedure TForm2.SetPoint(const Value: TPoint);
begin
FPoint := Value;
end;
end.
You code would suddenly break, and you'd blame Delphi/Borland for allowing it in the first place.
If you can't directly assign a property, don't use a hack to assign it - it will bite back someday.
Use Brian's suggestion to return a pointer, but drop the With - you can eaisly do Point.X := 10;
Another solution is to use a helper function:
procedure SetValue(i: Integer; const Value: Integer);
begin
i := Value;
end;
SetValue(MyClass.Rec.Member, 10);
It's still not safe though (see Barry Kelly's comment about Getter/Setter)
/Edit: Below follows the most ugly hack (and probably the most unsafe as well) but it was so funny I had to post it:
type
TRec = record
Member : Integer;
Member2 : Integer;
end;
TMyClass = class
private
FRec : TRec;
function GetRecByPointer(Index: Integer): Integer;
procedure SetRecByPointer(Index: Integer; const Value: Integer);
public
property Rec : TRec read FRec write FRec;
property RecByPointer[Index: Integer] : Integer read GetRecByPointer write SetRecByPointer;
end;
function TMyClass.GetRecByPointer(Index: Integer): Integer;
begin
Result := PInteger(Integer(@FRec) + Index * sizeof(PInteger))^;
end;
procedure TMyClass.SetRecByPointer(Index: Integer; const Value: Integer);
begin
PInteger(Integer(@FRec) + Index * sizeof(PInteger))^ := Value;
end;
It assumes that every member of the record is (P)Integer sized and will crash of AV if not.
MyClass.RecByPointer[0] := 10; // Set Member
MyClass.RecByPointer[1] := 11; // Set Member2
You could even hardcode the offsets as constants and access directly by offset
const
Member = 0;
Member2 = Member + sizeof(Integer); // use type of previous member
MyClass.RecByPointer[Member] := 10;
function TMyClass.GetRecByPointer(Index: Integer): Integer;
begin
Result := PInteger(Integer(@FRec) + Index)^;
end;
procedure TMyClass.SetRecByPointer(Index: Integer; const Value: Integer);
begin
PInteger(Integer(@FRec) + Index)^ := Value;
end;
MyClass.RecByPointer[Member1] := 20;
精彩评论