Delphi中,记录类型属性、记录字段赋值:预期将赋值给本地副本记录

10

在Delphi中针对记录类型属性,“左侧无法分配”的问题(Left side cannot be assigned to” for record type properties in Delphi),有Toon Krijthe提供的一个答案,展示了如何通过声明记录的属性来完成对记录属性字段的赋值。为了更方便地参考,这里是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;

在vcldeveloper的原始代码中,如果记录中没有setter,则很明显为什么会引发“无法分配给左侧”错误。同样,如果像上面的代码一样为属性TRec.A定义了一个setter,那么对于赋值Rec.A := 21;不会引发错误也是很清楚的。
我不理解的是,为什么赋值Rec.A := 21;将值21分配给TForm1FRec.FA字段。我原本以为该值将被分配给FRec的一个局部临时副本FA字段,而不是FRec.FA本身。请问有人能解释一下这里发生了什么吗?
1个回答

10

这是一个很好的问题!

您看到的行为是属性实现细节的结果。编译器实现属性的方式对于直接字段属性getter和函数属性getter有所不同。

当您编写以下内容时

Rec.A := 21;
编译器遇到 Rec ,便知道它是一个属性。由于getter是直接字段getter,编译器会简单地将 Rec 替换为 FRec 并将代码编译成完全相同的形式,就好像你自己写了这样一行代码一样。
FRec.A := 21;
编译器随后遇到了 A 属性并使用了设置器方法,因此您的赋值变为:
FRec.SetA(21);
因此,您观察到的行为。
假设您拥有一个函数获取器,而非直接字段获取器。
property Rec: TRec read GetRec;
....
function TForm1.GetRec: TRec;
begin
  Result := FRec;
end;
在这种情况下,对于

的处理。
Rec.A := 21;

变量声明被省略了。编译器会隐式地声明一个本地变量,代码将被编译成这样:

var
  __local_rec: TRec;
....
__local_rec := GetRec;
__local_rec.A := 21;
我认为这样的程序行为不应该依赖于属性获取器是直接字段获取器还是函数获取器,这似乎是属性特性和增强记录特性之间交互的设计缺陷。
以下是一个完整程序,非常简洁地演示了这个问题:
{$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.

输出

21
0

@Rudy OK,我当时集中精力处理了Rec属性的处理方式,没有涉及到扩展第二个A属性。但我的陈述仍然是正确的,因为Rec.A := 21FRec.A := 21编译方式相同。它们都被转换为FRec.SetA(21)。我会详细说明这一点。很抱歉让你感到困惑。 - David Heffernan
谢谢!我的理解是你在上面的评论中提到,你会避免编写明确依赖于该行为的代码。个人而言,我会将其归类为“了解即可,但最好不要使用”的范畴。 - J. Hauser
就我所知,虽然我最近没有阅读过它,但在早期(Delphi 1和2)的时代,Object Pascal语言指南中包含了一个警告,即应始终将此类值视为临时值,即将其分配给本地变量,更改该本地变量的字段,然后将该值写回。这并不是由编译器强制执行的,只是在文档中高度推荐。 - Rudy Velthuis
1
@RudyVelthuis 可变值类型确实有缺点,我相信您也知道。允许方法改变它们会导致这里讨论的问题以及其他类似的问题。避免记录方法改变记录是我避免这些问题的方式。 - David Heffernan
@David:我同意最好使用不可变的值类型。生成新值的方法应该是返回新实例的函数,而不是修改自身的过程。如果您自己编写类,可以强制执行此操作,但不能在其他人编写的记录上执行。 - Rudy Velthuis
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接