Delphi:类中的记录

3

以下情况:

type
  TRec = record
    Member : Integer;
  end; 

  TMyClass = class
  private
    FRec : TRec;
  public
    property Rec : TRec read FRec write FRec;
  end;

以下代码无法正常工作(左侧无法赋值),这是可以理解的,因为TRec是值类型:
MyClass.Rec.Member := 0;

在 D2007 中,以下内容是有效的:
with MyClass.Rec do
  Member := 0;

很遗憾,它在D2010中不起作用(我认为在D2009中也不起作用)。第一个问题:为什么会这样?它是故意改变的吗?还是只是某些其他变化的副作用?D2007的解决方法只是一个“错误”吗?

第二个问题:您认为以下解决方法如何?使用它是否安全?

with PRec (@MyClass.Rec)^ do
  Member := 0;

我在这里讨论的是现有的代码,因此需要进行的更改应该是最小的。 谢谢!

MyClass.Rec.Member := 0; 不被接受,因为 Rec 是一个属性,而不是值类型。尝试直接使用 Field,它也是一个值类型,并且可以工作:MyClass.FRec.Member := 0; - Francesca
1
如果TRec是一个类,它就可以工作。因此,这里有两个重要的事实(它是一个值类型和它通过属性访问)。 - jpfollenius
6
这种解决方法使用起来不安全。考虑未来更改Rec属性的情况,可能会改为从getter而非字段读取:你的hack意味着它将修改一个临时值,并且不会对底层字段产生影响。这就是为什么属性不允许修改返回值类型的原因。 - Barry Kelly
除了问题之外,不需要使用with语句,这应该可以以同样的方式工作: PRec(@t.Rec).Member := 0; - Remko
@Remko:我知道。原始代码使用了 with,我希望尽量减少代码更改。 - jpfollenius
6个回答

6

That

MyClass.Rec.Member := 0;

无法编译是有意设计的。我认为两个“with”结构能够编译只是一个轻微的疏忽。所以两者都 "安全使用"。

两个安全的解决方案是:

  1. MyClass.Rec赋值给一个临时记录,您可以操纵该记录并重新赋值给MyClass.Rec
  2. TMyClass.Rec.Member公开为它自己的属性。

谢谢!这正是我担心的...你的第二个解决方案意味着很多工作,基本上消除了在这里使用记录的优势。第一个解决方案当然可以工作。我得好好想想。 - jpfollenius
另一个问题:为什么我的问题中的解决方法甚至有效?我本来期望该属性在此处返回记录的副本... - jpfollenius
2
解决方法有效的原因是因为你正在破解类型系统。类型系统试图阻止你写入属性,因为未来的更改可能意味着该属性返回一个副本(例如getter的返回值),而不是底层字段。 - Barry Kelly
类型系统试图防止编译,但一旦该检查被绕过,代码生成器就会执行显然的实现,并简单地将对Ref属性的引用替换为底层的FRef字段。你的新解决方法也不应该真正起作用;我很确定你不能够获取属性的指针。 - Rob Kennedy

3
在一些需要对类的记录进行“直接操作”的情况下,我经常使用以下方法:
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;

这样做的好处是,您可以利用Delphi的自动取消引用功能,使代码访问每个记录元素更加可读,如下所示:

MyObject.MyRec.MyNum := 123;

我记不清了,但也许WITH关键字可以与此方法一起使用——我尽量不使用它! Brian


为了避免混淆MyRec是指针还是变量(从名称上不明显),我强烈建议在使用时写成MyObject.MyRec^.MyNum。 - Remko
1
一旦我们超越了使用“with”的事实,我对这段代码没有任何问题。它有一些好的东西:1. 所有直接字段访问(MyRec.MyNum)在整个程序中仍然可以正常工作,不需要任何更改。2. 对现有“with”语句的更改很小:在末尾添加“^”。3. 编译器将告诉您每个“with”语句确切的位置,因为它们不会编译​​没有在末尾添加“^”。因此,没有危险进行更改,然后使“with”语句中的标识符静默地开始引用其他变量而不是记录字段。 - Rob Kennedy
肯: 它有什么可怕的? - Gerry Coll
@Remko - 不用担心“混淆”问题。 Delphi会自动以完全相同的方式取消引用记录指针,就像对象引用会自动取消引用一样。 请记住,obj:TObject是引用类型(即指针),但您可能从这些情况中永远不会因缺少^而感到困惑。 Delphi / Pascal是强类型的 - 属性声明将其标识为指针。 如果在将来更改了它(假设由于使用而变成只读值类型),则无需再查看代码并删除所有^。 - Deltics
1
@Ken 除了我关于 WITH 的评论之外,我还以何种方式冒犯了你那敏感的感官? - Brian Frost

1
无法直接赋值的原因在这里
至于WITH,在D2009中仍然有效,我也希望它在D2010中也能正常工作(但我现在无法测试)。
更安全的方法是像Allen在上面的SO post中建议的那样直接公开记录属性:

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

不幸的是,这会污染主类。我们可以完全放弃该记录。而且这意味着创建这些属性需要大量的工作。 - jpfollenius
仍然可以使用记录的优点。它们在 TPersistent.AssignTo 中非常有用。此外,如果您有一个内存密集型应用程序,可以使用紧凑记录来减少内存使用量(以牺牲性能为代价)。 - Gerry Coll

0

更改的原因是编译器出现了错误。仅仅能够通过编译并不能保证它能够正常工作。一旦添加了 Getter 到该属性中,它就会失败。

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.

你的代码突然崩溃,你会责怪 Delphi/Borland 允许它在第一位。

如果不能直接分配属性,请不要使用hack来分配它 - 它终将反噬。

使用Brian的建议返回指针,但不使用with - 你可以轻松地执行Point.X := 10;


使用 with 只是一种个人喜好,没有必要作为一般规则而放弃它。 - jpfollenius
实际上,Smasher,有几个原因要放弃使用 with。这不仅仅是品味问题。https://dev59.com/d3VD5IYBdhLWcg3wJIEK - Rob Kennedy

0

记录是,它们不是实体。

它们甚至具有复制赋值语义!这就是为什么您不能原地更改属性值的原因。因为这将违反FRec的值类型语义,并破坏依赖于其不可变性或至少安全副本的代码。

问题在于,为什么您需要一个值(您的TRec)来像对象/实体一样行事呢?

如果您确实需要这样做,那么“TRec”成为类会更加合适,不是吗?

我的观点是,当您开始使用语言功能超出其意图时,您很容易发现自己处于必须在每个位置上与工具进行斗争的情况中。


你可能是正确的。就像我所说,我谈论的是现有的代码。当某个东西在D2007中运行(也就是编译通过)时,我不希望它在下一个版本中出现问题。 - jpfollenius
1
嗯,你一直依赖于一个被要求修复很长时间的漏洞。看起来它终于被修复了。;-) - Robert Giesecke
@Smasher:仅仅因为一些代码编译通过并不意味着它能正常工作。在这种情况下,这是一个长期存在的编译器错误最终被修复了。之前的编译通过实际上就是那个错误,而它现在能正常工作也是由于那个错误。 - Ken White
@Ken:我同意,尽管并不是每个人都知道这是一个bug。而且由于使用起来相当方便,在我们的遗留代码中有很多这样的情况。 - jpfollenius
@Smasher。如果有人在类上放置了一个getter方法,那么您的遗留代码将会被破坏。它可以编译,但是无法正常工作。 - Gerry Coll

-2
另一种解决方案是使用辅助函数:
procedure SetValue(i: Integer; const Value: Integer);
begin
  i := Value;
end;
SetValue(MyClass.Rec.Member, 10);

虽然还不够安全(请参阅Barry Kelly有关Getter/Setter的评论)

/编辑:以下是最丑陋的黑客攻击方式(可能也是最不安全的),但它非常有趣,我必须发布:

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;

它假设记录的每个成员都是(P)Integer大小的,如果不是,将会崩溃。

  MyClass.RecByPointer[0] := 10;  // Set Member
  MyClass.RecByPointer[1] := 11;  // Set Member2

你甚至可以将偏移量硬编码为常量,并通过偏移量直接访问

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;

那肯定不行。这个赋值语句只会改变局部变量,而不是记录中的任何内容。 - jpfollenius
哦,你是对的! 所以唯一的方法是使用: procedure SetValue(const i: PInteger; const Value: Integer); begin i^ := Value; end;这实际上与您的解决方法相同,您还可以使用类助手: TMyClassHelper = class helper for TMyClass public procedure SetInteger(const i: PInteger; const Value: Integer); end;procedure TMyClassHelper.SetInteger(const i: PInteger; const Value: Integer); begin i^ := Value; end;MyClass.SetInteger(@t.Rec.Member, 10); - Remko
这与某人在类上放置Getter所发生的情况相同! - Gerry Coll

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