【问题标题】:Delphi: Records in ClassesDelphi:课堂记录
【发布时间】:2009-12-17 08:00:47
【问题描述】:

以下情况:

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;
  • 好吧,如果 TRec 是一个类,它会工作。所以,这两个事实在这里都很重要(它是一个值类型的事实和它通过属性访问的事实)
  • 该变通方法使用起来并不安全。考虑未来对 Rec 属性的更改,以便它从 getter 而不是字段读取:您的 hack 将意味着它将修改一个临时的并且不会对基础字段产生影响。这就是属性不允许修改返回值类型的原因。
  • 除了问题之外:不需要 with 语句,这应该以相同的方式工作:PRec(@t.Rec).Member := 0;
  • @Remko:我知道。原始代码使用with,我希望将代码更改保持在最低限度。

标签: delphi delphi-2010 delphi-2007 records


【解决方案1】:

那个

MyClass.Rec.Member := 0;

不编译是设计使然。曾经编译过的两个“with”结构都只是(AFAICT)的疏忽。所以两者都“可以安全使用”。

两个安全的解决方案是:

  1. MyClass.Rec 分配给您操作并分配回MyClass.Rec 的临时记录。
  2. TMyClass.Rec.Member 单独公开为属性。

【讨论】:

  • 谢谢!这就是我害怕的......你的第二个解决方案意味着大量的工作,并且基本上消除了在这里使用记录的优势。第一个解决方案当然会起作用。我得考虑一下。
  • 另一个问题:为什么我的问题中的解决方法甚至有效?我希望该物业在这里返回记录的副本...
  • 解决方法有效的原因是因为您正在破解类型系统。类型系统试图阻止您写入属性,因为未来的更改可能意味着属性返回一个副本(例如 getter 的返回值),而不是底层字段。
  • 类型系统试图阻止它编译,但是一旦该检查被绕过,代码生成器会执行明显的实现并简单地将对Ref 属性的引用替换为基础FRef 字段。您的新解决方法也不应该真正起作用。我很确定您不应该能够获取指向属性的指针。
【解决方案2】:

在某些需要“直接操作”类记录的情况下,我经常采用以下方法:

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 可以使用这种方法 - 我尽量不使用它! 布赖恩

【讨论】:

  • 为了避免混淆 MyRec 是指针还是 var(从名称上看不明显),我强烈建议使用 MyObject.MyRec^.MyNum 如果使用它...
  • 一旦我们超越了with 被使用的事实,我对这段代码没有任何问题。它有一些好处: 1. 所有直接字段访问 (MyRec.MyNum) 在整个程序中仍然有效,无需任何更改。 2. 对现有with 语句的更改很少:在末尾添加^。 3. 编译器会告诉你每个with 语句的确切位置,因为它们不会在没有在末尾添加^ 的情况下进行编译。因此,没有危险进行更改,然后在 with 语句中的标识符默默地开始引用其他变量而不是记录字段。
  • Ken:这有什么可怕的?
  • @Remko - 无需担心“混乱”。 Delphi 自动取消引用记录指针的方式与自动取消引用对象引用的方式完全相同。请记住 obj: TObject 是一种引用类型(即指针),但您可能永远不会因为在这些情况下缺少 ^ 而感到困惑。 Delphi/Pascal 是强类型的——属性的声明将其标识为指针。如果它在未来被改变(想象一下它变成了一个只读值类型,由于使用),你就不必通过你的代码来删除所有的 ^。
  • @Ken 除了我对 WITH 的评论,我在哪些方面会冒犯你的敏感?
【解决方案3】:

不能直接赋值的原因是here.
至于 WITH,它仍然可以在 D2009 中使用,我希望它也可以在 D2010 中使用(我现在无法测试)。
更安全的方法是直接暴露记录属性,正如艾伦在上述 SO post 中建议的那样:

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

【讨论】:

  • 不幸的是,这污染了主类。那时我们可以完全放弃记录。并且创建这些属性意味着大量工作。
  • 使用记录仍有优势。它们在 TPersistent.AssignTo 中很有用。此外,如果您有一个内存密集型应用程序,您可以使用打包记录来减少内存使用量(性能成本)
【解决方案4】:

记录是,它们不是实体。

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

他们的问题是,为什么你需要一个值(你的 TRec)来表现得像一个对象/实体?

如果“TRec”是你使用它的目的,那么将它作为一个类不是更合适吗?

我的观点是,当您开始使用超出其意图的语言功能时,您很容易发现自己处于必须在每米使用工具的情况下。

【讨论】:

  • 你可能是对的。正如我所说,我在这里谈论的是现有代码。当某些东西在 D2007 中有效(我的意思是编译)时,我不希望它在下一个版本中被破坏。
  • 好吧,您依赖于一个要求修复很长时间的错误。好像终于修好了。 ;-)
  • @Smasher:编译并不意味着它可以工作。在这种情况下,这是编译器中长期存在的错误,最终得到了修复。它用于编译的事实是错误,它工作的事实是因为那个错误。
  • @Ken:我同意,虽然不是每个人都知道这是一个错误。而且由于它使用起来很舒服,所以我们的遗留代码中有很多这样的东西。
  • @Smasher。然后,一旦有人在类上放置了 getter 方法,您的遗留代码就会被破坏。它会编译,但不工作。
【解决方案5】:

它被改变的原因是它是一个编译器错误。它编译的事实并不能保证它会起作用。 一旦将 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;

【讨论】:

【解决方案6】:

另一种解决方案是使用辅助函数:

procedure SetValue(i: Integer; const Value: Integer);
begin
  i := Value;
end;
SetValue(MyClass.Rec.Member, 10);

但它仍然不安全(参见 Barry Kelly 关于 Getter/Setter 的评论)

/Edit:下面是最丑陋的黑客(也可能也是最不安全的),但它太有趣了,我不得不发布它:

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 大小的,否则会导致 AV 崩溃。

  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;

【讨论】:

  • 那肯定行不通。赋值只会改变局部变量,不会改变记录。
  • 哎呀,你是对的!所以唯一的方法是使用:procedure SetValue(const i: PInteger; const Value: Integer);开始 i^ := 价值;结尾;这与您的解决方法实际上相同,您也可以使用类助手: TMyClassHelper = TMyClass 公共过程的类助手 SetInteger(const i: PInteger; const Value: Integer);结尾;过程 TMyClassHelper.SetInteger(const i: PInteger; const Value: Integer);开始 i^ := 价值;结尾; MyClass.SetInteger(@t.Rec.Member, 10);
  • 这与有人在课堂上放置 Getter 后会发生的情况相同!
猜你喜欢
  • 2019-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-26
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多