常量字符串参数和适用于Android的Delphi XE5编译器

时间:2013-12-22 17:36:40

标签: android delphi delphi-xe5

希望我只是遗漏了一些显而易见的东西,但我似乎发现在使用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)如此实施FooGoToDirectory

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时,没有什么不好的。)

更新

对于无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 monitor.bat文件夹中运行tools;通过树木查看木材,仅在我使用LOGE调用时过滤错误。虽然不是每次我运行这个修改过的测试应用程序时参数都会被破坏,但有时候它仍然会...这表明一个相当讨厌的编译器错误......

更新2

第二个测试用例尤其令我说服自己更多,所以我把它记录为QC 121312

更新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 个答案:

答案 0 :(得分:5)

我们做了一些内部调查,事实证明这取决于编写代码的方式,编译器无法真正做到这一点。它有点复杂,但简而言之,GoToDirectory方法接收一个引用字符串的const字符串参数(Dir)。但是,在方法的代码中,用新的字符串替换字符串(可能位于相同或不同的内存位置)。鉴于const参数不会增加引用计数,如果减少代码中相同字符串的引用计数,则删除该字符串。所以你有一个指向未定义内存位置的参数,实际输出是随机的。在所有平台上发生(可能发生)同样的问题,而不是特定于移动设备。

有许多解决方法:

1)没有const参数(因此引用计数更高,您更改引用的字符串,但param现在是对单独字符串的引用

2)传递字符串的别名:

  Tmp := FParentDir;
  GoToDirectory(Tmp);

3)将“const String”参数分配给临时局部变量:

procedure TForm1.GoToDirectory(const Dir: string);
var
  TmpDir: String;
begin
  TmpDir := Dir;

我知道这远不是一个清晰的描述,我不得不重做几次以掌握它,但这是一个编译器无法真正自动处理的场景,因此我们将关闭错误报告“按设计”

答案 1 :(得分:4)

为了扩展Marco的评论,自引入const参数以来,在const参数上使用const的这个问题已经存在,并且不是错误,而是一个功能,您的示例是不应使用的案例示例。

const修饰符是对调用者的一个承诺,即作为参数传递的变量无法作为调用的副作用进行修改。保证这一点的最简单方法是永远不要使用var参数修改函数或过程中的全局可访问变量。这允许被调用者依赖调用者的引用计数,避免复制语义等。换句话说,它告诉编译器该值是否更有效地作为var传递,并且可以将其视为{{ 1}}参数(即具有左值)然后将其作为var而不是值传递。如果它是托管类型,就像字符串一样,它也可以依赖调用者的引用来保持内存的存活。

GoToDirectory修改全局可访问字符串时违反了此契约(在此上下文中,任何堆访问都应被视为全局访问,即使它是对象的字段)。 GoToDirectory不应该有const参数,因为它违反了const隐含的合同。

请注意,这与const在其他语言(如C ++)中隐含的合同有很大不同。遗憾的是,当时没有更好的词汇可供使用。实际上它的含义是函数或过程对于与const参数的形式类型兼容的变量是纯粹的,而不是它不会修改参数。记住更容易,不要将const应用于具有副作用的函数或过程的任何参数。

当对过程或函数或其调用的任何过程或函数看不到写入全局的副作用时,可能违反了经验法则。这通常很难保证外部的简单情况(例如简单的属性设置器),并且只应在性能约束无法承担值复制的开销时使用。换句话说,你最好有手头的性能痕迹来证明它的合理性,或者对于不经意的观察者来说,更好的是它副本会很昂贵。

答案 2 :(得分:2)

FWIW,我无法在使用非FMX版本的Nexus 7上使用XE5 Update 2,Android 4.4.2在本地重现此问题。项目是使用您的分步说明(复制/粘贴代码)构建的,并在设备上以调试模式运行。日志输出为:

Capture of Android Debug Monitor window

为了确保无法重现它,我使用相同的结果多次构建并运行应用程序。

但是,FMX版本的结果不一致。我第一次运行并构建它时,它在第三次ShowMessageFmt之后产生了访问冲突并且必须停止。然后我再次构建它,运行它,并且能够看到所有四个ShowMessageFmt对话框,但最后一个显示的值不正确:

Prior to calling foo, Dir is "/alpha/beta"
After to calling foo, Dir is "/alpha/beta"
Prior to calling foo, Dir is "/alpha"
After to calling foo, Dir is ""

第三次和第四次构建和运行重复产生与第二次相同的输出。

答案 3 :(得分:2)

我会说这是一个错误。它是公开的,Embarcadero的R&amp; D团队将对其进行调查。