题
我很好奇为什么 Delphi 将记录类型属性视为只读:
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;
如果我尝试将值分配给 Rec 属性的任何成员,我将收到“左侧无法分配给”错误:
procedure TForm1.DoSomething(ARec: TRec);
begin
Rec.A := ARec.A;
end;
同时允许对底层字段执行相同的操作:
procedure TForm1.DoSomething(ARec: TRec);
begin
FRec.A := ARec.A;
end;
这种行为有什么解释吗?
解决方案
自“Rec”以来是一个属性,编译器对它的处理方式略有不同,因为它必须首先评估“读取”属性。属性decl。考虑一下,这在语义上等同于您的示例:
...
property Rec: TRec read GetRec write FRec;
...
如果你这样看,你可以看到第一次引用“Rec”。 (在点'。'之前),必须调用GetRec,它将创建Rec的临时本地副本。这些临时设计是按照设计“只读”的。这就是你遇到的。
您可以在这里做的另一件事是将记录的各个字段分解为包含类的属性:
...
property RecField: Integer read FRec.A write FRec.A;
...
这将允许您通过属性直接分配给类实例中该嵌入记录的字段。
其他提示
是的,这是一个问题。但问题可以通过记录属性来解决:
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;
这可以毫无问题地编译和工作。
我经常使用的解决方案是将属性声明为指向记录的指针。
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;
通过这种方式,直接分配 Form1.Rec.A:= MyInteger
将起作用,而 Form1.Rec:= MyRec
将通过复制<中的所有值来工作代码> MyRec 到 FRec
字段按预期方式。
这里唯一的缺陷是,当您希望实际检索要使用的记录副本时,您将需要类似 MyRec:= Form1.Rec ^
编译器阻止您分配临时。允许使用C#中的等价物,但它没有效果; Rec属性的返回值是基础字段的副本,并且分配给副本上的字段是一个nop。
因为你有隐式的getter和setter函数,你不能修改函数的Result,因为它是一个const参数。
(注意:如果你在一个对象中转换记录,结果实际上是一个指针,因此相当于一个var参数)。
如果要保留Record,则必须使用中间变量(或Field变量)或使用WITH语句。
使用显式的getter和setter函数查看以下代码中的不同行为:
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;
这是因为属性实际上被编译为函数。属性仅返回或设置值。它不是引用或指向记录的指针
所以:
Testing.TestRecord.I := 10; // error
与调用这样的函数相同:
Testing.getTestRecord().I := 10; //error (i think)
你能做的是:
r := Testing.TestRecord; // read
r.I := 10;
Testing.TestRecord := r; //write
这种架构有点混乱但是固有的。
正如其他人所说 - 读取属性将返回记录的副本,因此字段的分配不会作用于 TForm1 拥有的副本。
另一种选择是这样的:
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 将为您取消引用 PRec 指针,因此这样的事情仍然有效:
Form1.Rec.A := 1234;
不需要该属性的写入部分,除非您想要交换 FRec 指向的 PRec 缓冲区。无论如何,我真的不建议通过属性进行此类交换。
最简单的方法是:
procedure TForm1.DoSomething(ARec: TRec);
begin
with Rec do
A := ARec.A;
end;