希望我只是遗漏了一些明显的东西,但我似乎发现在使用Delphi XE5 Android编译器时,常量字符串参数被破坏了 . 测试代码:
1)创建一个新的空白移动应用程序项目 .
2)在表单中添加 TButton
,并为其创建 OnClick
处理程序 .
3)像这样填写处理程序:
procedure TForm1.Button1Click(Sender: TObject);
begin
GoToDirectory(PathDelim + 'alpha' + PathDelim + 'beta');
GoToDirectory(FParentDir);
end;
4)在表单类声明中,添加两个字段和一个方法,如下所示:
FCurrentPath, FParentDir: string;
procedure GoToDirectory(const Dir: string);
5)像这样实现 Foo
和 GoToDirectory
:
function Foo(const S: string): Boolean;
begin
Result := (Now <> 0);
end;
procedure TForm1.GoToDirectory(const Dir: string);
begin
FCurrentPath := IncludeTrailingPathDelimiter(Dir);
FParentDir := ExcludeTrailingPathDelimiter(ExtractFilePath(Dir));
ShowMessageFmt('Prior to calling Foo, Dir is "%s"', [Dir]);
Foo(FParentDir);
ShowMessageFmt('After calling Foo, Dir is "%s"', [Dir]);
end;
6)编译并在设备上运行 .
当我这样做时,前两个消息框没有指出任何错误,但是 Dir
然后在第三和第四个提示之间被清除 . 有没有其他人得到这个,或者我只是做一些愚蠢的事情? (当我为测试目的而定位Win32时,没有什么不好的 . )
Update
对于无FMX版本,请再次创建一个新的空白移动应用程序,但这次从项目中删除该表单 . 然后,进入项目源并添加以下代码:
program Project1;
uses
System.SysUtils,
Androidapi.Log;
type
TTest = class
private
FCurrentPath, FParentDir: string;
procedure GoToDirectory(const Dir: string);
public
procedure Execute;
end;
function Foo(const S: string): Boolean;
begin
Result := (Now <> 0);
end;
procedure TTest.GoToDirectory(const Dir: string);
var
M: TMarshaller;
begin
FCurrentPath := IncludeTrailingPathDelimiter(Dir);
FParentDir := ExcludeTrailingPathDelimiter(ExtractFilePath(Dir));
LOGE(M.AsUtf8(Format('Prior to calling Foo, Dir is "%s"', [Dir])).ToPointer);
Foo(FParentDir);
LOGE(M.AsUtf8(Format('After to calling Foo, Dir is "%s"', [Dir])).ToPointer);
end;
procedure TTest.Execute;
begin
GoToDirectory(PathDelim + 'alpha' + PathDelim + 'beta');
GoToDirectory(FParentDir);
end;
var
Test: TTest;
begin
Test := TTest.Create;
Test.Execute;
end.
要查看结果,请先在Android SDK tools
文件夹中运行 monitor.bat
;通过树木看木头,只是因为我使用了 LOGE
来调试错误 . 虽然不是每次我运行这个修改过的测试应用程序时参数都会被破坏,但有时候它仍然会...这表明一个相当讨厌的编译器错误...
Update 2
特别是第二个测试用例,我将'm convincing myself even more, so I'记录为QC 121312 .
Update 3
下面接受的答案中的解释的代码而不是散文版本(接口类型使用与字符串基本相同的引用计数机制,只有能够轻松跟踪对象何时被销毁):
program CanaryInCoalmine;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
type
ICanary = interface
function GetName: string;
property Name: string read GetName;
end;
TCanary = class(TInterfacedObject, ICanary)
strict private
FName: string;
function GetName: string;
public
constructor Create(const AName: string);
destructor Destroy; override;
end;
TCoalmine = class
private
FCanary: ICanary;
procedure ChangeCanary(const Arg: ICanary);
public
procedure Dig;
end;
constructor TCanary.Create(const AName: string);
begin
inherited Create;
FName := AName;
WriteLn(FName + ' is born!');
end;
destructor TCanary.Destroy;
begin
WriteLn(FName + ' has tweeted its last song');
inherited;
end;
function TCanary.GetName: string;
begin
Result := FName;
end;
procedure TCoalmine.ChangeCanary(const Arg: ICanary);
var
OldName: string;
begin
Writeln('Start of ChangeCanary - reassigning FCanary...');
OldName := Arg.Name;
FCanary := TCanary.Create('Yellow Meanie');
Writeln('FCanary reassigned - is ' + OldName + ' still alive...?');
Writeln('Exiting ChangeCanary...');
end;
procedure TCoalmine.Dig;
begin
FCanary := TCanary.Create('Tweety Pie');
ChangeCanary(FCanary);
end;
var
Coalmine: TCoalmine;
begin
Coalmine := TCoalmine.Create;
Coalmine.Dig;
ReadLn;
end.
输出是这样的:
Tweety Pie is born!
Start of ChangeCanary - reassigning FCanary...
Yellow Meanie is born!
Tweety Pie has tweeted its last song
FCanary reassigned - is Tweety Pie still alive...?
Exiting ChangeCanary...
因此,重新分配字段会丢弃前一个对象的引用计数,如果没有其他强引用,则在那里销毁它,然后在 ChangeCanary
过程完成之前 .
4 回答
我们做了一些内部调查,事实证明这取决于编写代码的方式,编译器无法真正做到这一点 . 它有点复杂,但简而言之,GoToDirectory方法接收一个引用字符串的const字符串参数(Dir) . 但是,在方法的代码中,用新的字符串替换字符串(可能位于相同或不同的内存位置) . 鉴于const参数不会增加引用计数,如果减少代码中相同字符串的引用计数,则删除该字符串 . 所以你有一个指向未定义内存位置的参数,实际输出是随机的 . 在所有平台上发生(可能发生)同样的问题,而不是移动特定的 .
有许多变通方法:
1)没有const参数(因此引用计数更高,您更改引用的字符串,但param现在是对单独字符串的引用
2)传递字符串的别名:
3)将“const String”参数分配给临时局部变量:
我知道这远不是一个清晰的描述,我不得不重做几次才能掌握它,但这是一个编译器无法真正自动处理的场景,因此我们将按照“设计”关闭错误报告 .
为了扩展Marco的评论,自从引入
const
参数以来,在参数上使用const
的这个问题一直存在于Delphi中并且不是一个bug,而是一个例子,你的例子是一个案例的例子不应该使用它 .const
修饰符是对调用者的一个承诺,即作为参数传递的变量无法作为调用的副作用进行修改 . 保证这一点的最简单方法是永远不要使用const
参数修改函数或过程中的全局可访问变量 . 这允许被调用者依赖调用者的引用计数,避免复制语义等 . 换句话说,它告诉编译器该值是否更有效地作为var
传递,并且可以将其视为var
参数(即,有一个左值)然后传递它作为var
而不是值 . 如果它是托管类型,就像字符串一样,它也可以依赖调用者的引用来保持内存的存活 .当它修改全局可访问字符串时,
GoToDirectory
违反了此 Contract (在此上下文中,任何堆访问都应被视为全局访问,即使它是对象的字段) .GoToDirectory
不应该有一个const
参数,因为它违反了const
隐含的 Contract .请注意,这与其他语言(如C)中
const
隐含的 Contract 有很大不同 . 遗憾的是,当时没有更好的词汇可供使用 . 实际上它的含义是函数或过程对于与const
参数的形式类型兼容的变量是纯粹的,而不是它不会修改参数 . 更容易记住,不要将const
应用于具有副作用的函数或过程的任何参数 .当对全局写入的副作用对程序或函数或其调用的任何过程或函数不可见时,可能会违反该经验法则 . 这通常很难保证外部的简单情况(例如简单的属性设置器),并且只应在性能约束无法承担值复制的开销时使用 . 换句话说,你最好有手头的性能痕迹来证明它的合理性,或者对于不经意的观察者来说,更好的是它副本会很昂贵 .
FWIW,我无法在使用非FMX版本的Nexus 7上使用XE5 Update 2,Android 4.4.2在本地重现此问题 . 项目是使用您的分步说明(复制/粘贴代码)构建的,并在设备上以调试模式运行 . 日志输出是:
为了确保我无法重现它,我使用相同的结果多次构建并运行应用程序 .
但是,FMX版本的结果不一致 . 我第一次运行并构建它时,它在第三次
ShowMessageFmt
之后产生了访问冲突,并且必须停止 . 然后我再次构建它,运行它,并且能够看到所有四个ShowMessageFmt
对话框,但最后一个显示的值不正确:第三次和第四次构建和运行重复产生与第二次相同的输出 .
我会说这是一个错误 . 它是公开的,Embarcadero的研发团队将对其进行调查 .