首页 文章

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

提问于
浏览
8

希望我只是遗漏了一些明显的东西,但我似乎发现在使用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时,没有什么不好的 . )

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 回答

  • 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;
    

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

  • 2

    为了扩展Marco的评论,自从引入 const 参数以来,在参数上使用 const 的这个问题一直存在于Delphi中并且不是一个bug,而是一个例子,你的例子是一个案例的例子不应该使用它 .

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

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

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

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

  • 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 ""
    

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

  • 4

    我会说这是一个错误 . 它是公开的,Embarcadero的研发团队将对其进行调查 .

相关问题