delphi - Delphi 记录分配
问题描述
我遇到了 delphi XE3 编译器的一些奇怪行为(我为 x86 架构编译)。
想象一下,我有一个字段的类 - 具有多个简单类型字段的自定义记录:
TPage = class
type
TParagraph = record
public
FOwner: TPage;
FFirst: Integer;
FSecond: Integer;
procedure Select;
end;
public
FSelected: TParagraph;
end;
procedure TPage.TParagraph.Select;
begin
FOwner.FSelected:=Self;
end;
逻辑是我的页面可以包含多个段落,并且在某些时候我希望这些段落中的一个被选中(以便能够在我的程序的其他部分中对其进行一些操作):
procedure TMainForm.Button1Click(Sender: TObject);
var
lcPage: TPage;
lcParagraph: TPage.TParagraph;
begin
lcPage:=TPage.Create;
try
<...>
lcParagraph.FOwner:=lcPage;
lcParagraph.FFirst:=1;
lcParagraph.FSecond:=2;
lcParagraph.Select;
<...>
finally
lcPage.Free;
end;
当我的记录不超过某个大小时,一切都很好。一个引用和两个整数是可以的,在这种情况下,我得到如下汇编指令:
MainUnit.pas.350: FOwner.FSelected:=Self;
00C117B3 8B45FC mov eax,[ebp-$04]
00C117B6 8B00 mov eax,[eax]
00C117B8 8B55FC mov edx,[ebp-$04]
00C117BB 8B0A mov ecx,[edx]
00C117BD 894804 mov [eax+$04],ecx
00C117C0 8B4A04 mov ecx,[edx+$04]
00C117C3 894808 mov [eax+$08],ecx
00C117C6 8B4A08 mov ecx,[edx+$08]
00C117C9 89480C mov [eax+$0c],ecx
我可以看到三个正确的 mov 将内存从本地记录复制到类`。
但!如果我在我的记录中添加更多字段,生成的 asm 代码会更改并且记录分配不再正确执行。
TParagraph = record
public
FOwner: TPage;
FFirst: Integer;
FSecond: Integer;
FThird: Integer;
procedure Select;
end;
MainUnit.pas.350: FOwner.FSelected:=Self;
00C117C9 8B45FC mov eax,[ebp-$04]
00C117CC 8B55FC mov edx,[ebp-$04]
00C117CF 8B12 mov edx,[edx]
00C117D1 8BF2 mov esi,edx
00C117D3 8D7A04 lea edi,[edx+$04]
00C117D6 A5 movsd
00C117D7 A5 movsd
00C117D8 A5 movsd
00C117D9 A5 movsd
在 lea 指令之后,CPU 状态是这样的:
在此示例中,02D37280 是我的 lcPage 类的地址,因此 02D37284 应包含其字段的开头 - FSelected 记录。但是 movsd 指令将内存从 ESI 复制到 EDI,从 02D37280 复制到 02D37284,这完全没有意义!如果我将 ESI 寄存器更改为 EAX (19F308) 的值,这是我的本地 lcParagraph 变量的开始,则正确执行复制:
我描述的是已知的错误吗?还是我错过了有关德尔福的一些基本知识?这是分配记录的好方法吗?我可以很容易地解决这个问题,例如,通过更改FOwner.FSelected:=Self;
为CopyMemory(@FOwner.FSelected, @Self, SizeOf(Self));
in procedure TPage.TParagraph.Select;
。但我想弄清楚出了什么问题。
最小的可重现示例:
program RecordAssignmentIssue;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
TPage = class
type
TParagraph = record
public
FOwner: TPage;
FFirst: Integer;
FSecond: Integer;
FThird: Integer;
procedure Select;
end;
public
FSelected: TParagraph;
end;
procedure TPage.TParagraph.Select;
begin
FOwner.FSelected:=Self;
end;
var
lcPage: TPage;
lcParagraph: TPage.TParagraph;
begin
try
lcPage:=TPage.Create;
try
lcParagraph.FOwner:=lcPage;
lcParagraph.FFirst:=1;
lcParagraph.FSecond:=2;
lcParagraph.FThird:=3;
lcParagraph.Select;
Assert(CompareMem(@lcPage.FSelected, @lcParagraph, SizeOf(lcParagraph)));
// get rid of FThird and assertion will pass
finally
lcPage.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
解决方案
这是一个仍然存在于 Delphi 11 中的错误(感谢LU RD 确认)。您应该向Quality Portal提交错误报告。
同时,我认为您可以通过在TPage
而不是TParagraph
. 像这样:
program RecordAssignmentIssue;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
type
TPage = class
type
TParagraph = record
public
FOwner: TPage;
FFirst: Integer;
FSecond: Integer;
FThird: Integer;
procedure Select;
end;
private
procedure Select(const Paragraph: TParagraph);
public
FSelected: TParagraph;
end;
procedure TPage.TParagraph.Select;
begin
FOwner.Select(Self);
end;
{ TPage }
procedure TPage.Select(const Paragraph: TParagraph);
begin
FSelected:=Paragraph;
end;
var
lcPage: TPage;
lcParagraph: TPage.TParagraph;
begin
try
lcPage:=TPage.Create;
try
lcParagraph.FOwner:=lcPage;
lcParagraph.FFirst:=1;
lcParagraph.FSecond:=2;
lcParagraph.FThird:=3;
lcParagraph.Select;
Assert(CompareMem(@lcPage.FSelected, @lcParagraph, SizeOf(lcParagraph)));
// get rid of FThird and assertion will pass
finally
lcPage.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
或者另一个非常简单的解决方法是引入一个额外的局部指针变量来保存指向的指针Self
:
procedure TPage.TParagraph.Select;
var
P: ^TParagraph;
begin
P := @Self;
FOwner.FSelected := P^;
end;
推荐阅读
- php - 如何支持在 PHP 中返回 $this 的方法的 IDE 语言服务器类型提示
- python - Bokeh 中的堆积条形图;交互式绘制分组数据
- elasticsearch - 如何根据字段值条件在 Elasticsearch 脚本得分中执行数学公式?
- reactjs - 在 onInput React 之后更新 div var
- go - Go And Nested Interfaces
- r - 用 R 读取 docx 文件并将值转换为数字
- javascript - Div 覆盖对话框不会在 Firefox 中显示
- bash - 在单行命令中插入密码
- linux - 为什么 libtool 为静态库安装运行 ranlib?
- sql - 使用 1 个 where 子句联合选择 2 个 SQL 表