在Delphi中的记录类型属性中,左侧无法被赋值。

26

我很想知道为什么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;

这种行为有什么解释吗?


相关:https://dev59.com/ibLma4cB1Zd3GeqPYkoI - Gabriel
8个回答

41

由于"Rec"是一个属性(property),编译器对其进行了一些特殊处理,因为它必须先计算属性声明的“读取”操作。考虑下面的示例代码,与你的示例代码在语义上是等效的:

...
property Rec: TRec read GetRec write FRec;
...

通过这种方式观察,你会发现在点号'.'之前对"Rec"的第一个引用必须调用GetRec方法,该方法将创建Rec的临时本地副本。这些临时变量按设计是"只读"的。这就是你遇到的问题。

另一件事是,在包含类上将记录的各个字段作为属性拆分出来:

...
property RecField: Integer read FRec.A write FRec.A;
...

这将允许你通过属性直接为类实例中嵌入记录的字段进行赋值。


那么,您的意思是 "property Rec: TRec read FRec write FRec" 有一个“秘密”的 getter 和 setter 吗? - Gabriel

23

是的,这是一个问题。但可以使用记录属性解决此问题:

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;

这段代码可以编译并且没有问题地运行。


11
注意,你的解决方案还不错,但使用它的用户需要记住,如果他们将属性更改为“property Rec:TRec read GetRec write FRec;”,赋值的技巧将彻底失败(因为GetRec将返回一个复制品,因为记录是值类型)。 - Jeroen Wiert Pluimers
如果只需要对记录属性进行读/写访问,则可以仅读取TForm1中的Rec属性。这个解决方案的关键部分是记录属性中的setter方法。 - Griffyn
我不明白为什么我需要分开的设置器,而不能直接在“写入”语句中设置字段... - WENDYN

11

我经常使用的解决方案是将属性声明为指向记录的指针。

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^的操作。


8
编译器阻止您将值分配给临时变量。在C#中相当于被允许,但是它没有效果;Rec属性的返回值是基础字段的副本,在副本上对该字段进行赋值是nop操作。

4
因为您有隐式的getter和setter函数,并且无法修改函数的结果,因为它是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;

3
最简单的方法是:
procedure TForm1.DoSomething(ARec: TRec);
begin
  with Rec do
    A := ARec.A;
end;

1
我认为你是对的 - 对于记录来说使用属性没有意义,这似乎是很多工作...只需编写一个过程来处理记录:SetSomething(var ARec: TRec)。 - sergeantKK

3
这是因为属性实际上被编译为函数。属性只返回或设置一个值,它不是记录的引用或指针。
所以:
Testing.TestRecord.I := 10;  // error

与调用以下函数相同:

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

您可以做的是:
r := Testing.TestRecord;    // read
r.I := 10;
Testing.TestRecord := r;    //write

这有点混乱,但这是这种架构固有的特点。


2

就像其他人所说的那样 - read属性将返回记录的副本,因此字段的赋值不会作用于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缓冲区,否则不需要编写属性的部分。我真的不建议通过属性进行这种交换。


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