为什么我不能将我的函数引用分配给匹配的变量? E2555 升起

Why can't I assign my function reference to a matching variable? E2555 is raised

我正在尝试构建一个自定义比较器,它允许将比较函数分配给内部字段。为了简化比较器的创建,我尝试添加一个类似构造函数的 class 函数 Construct 来初始化比较器。

现在,如果我尝试编译以下示例,编译器会显示

[dcc32 Fehler] ConsoleDemo1.dpr(37): E2555 Symbol 'Result' cannot be tracked

我有以下示例代码:

program ConsoleDemo1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Generics.Collections, Generics.Defaults,
  System.SysUtils;

type

  TConstFunc<T1, T2, TResult> = reference to function(const Arg1: T1; const Arg2: T2): TResult;

  TDemo = class(TComparer<string>)
  private
    FVar: TConstFunc<string, string, Integer>;
    function CompareInternal(const L, R: string): Integer;
  public
    class function Construct(): TDemo;
    function Compare(const L, R: string): Integer; override;
  end;

function TDemo.Compare(const L, R: string): Integer;
begin
  Result := FVar(L, R);
end;

function TDemo.CompareInternal(const L, R: string): Integer;
begin
  Result := AnsiCompareStr(L, R);
end;

class function TDemo.Construct: TDemo;
begin
  Result := TDemo.Create();
  Result.FVar := Result.CompareInternal;
end;

end.

我的英文 Delphi 上的编译器错误是:

[dcc32 Error] E2555 Cannot capture symbol 'Result'

这是设计缺陷造成的。根本没有理由在这里进行任何变量捕获。赋值的右侧是实例方法而不是匿名方法。但是编译器通过将方法包装在匿名方法中来处理这个问题。编译器翻译

Result.FVar := Result.CompareInternal;

Result.FVar := 
  function(const Arg1, Arg2: string): Integer
  begin
    InnerResult := OuterResult.CompareInternal(Arg1, Arg2);
  end;

撇开两个单独的结果变量的混淆不谈,编译器拒绝了这一点,因为外部结果变量不是局部的,它是一个 var 参数。所以不能被捕获。

但在我看来整个设计是错误的。不需要任何变量捕获。当您编写 Result.CompareInternal 时,您打算引用一个普通的 of object 方法。有了更好的设计,编译器将允许这种赋值而无需创建匿名方法。

您可以像这样解决问题:

class function TDemo.Construct: TDemo;
var
  Demo: TDemo;
begin
  Demo := TDemo.Create();
  Demo.FVar := Demo.CompareInternal;
  Result := Demo;
end;

这里可以捕获局部变量Demo

或者像我建议的那样:

program ConsoleDemo1;

{$APPTYPE CONSOLE}

uses
  Generics.Defaults,
  System.SysUtils;

type
  TConstFunc<T1, T2, TResult> = reference to function(const Arg1: T1; 
    const Arg2: T2): TResult;

  TDemo = class(TComparer<string>)
  private
    FVar: TConstFunc<string, string, Integer>;
    function CompareInternal(const L, R: string): Integer;
  public
    constructor Create;
    function Compare(const L, R: string): Integer; override;
  end;

constructor TDemo.Create;
begin
  inherited;
  FVar := CompareInternal;
end;

function TDemo.Compare(const L, R: string): Integer;
begin
  Result := FVar(L, R);
end;

function TDemo.CompareInternal(const L, R: string): Integer;
begin
  Result := AnsiCompareStr(L, R);
end;

end.

我不认为这是一个错误。重要的是,您已将 TConstFunc 定义为 匿名方法 类型。这些是托管的、引用计数的、非常特殊的类型,与常规对象方法截然不同。通过编译器的魔法,它们通常是赋值兼容的,但有几个重要的警告。考虑更简洁的:

program Project1;

{$APPTYPE CONSOLE}

type
  TFoo = reference to procedure;

  TDemo = class
  private
    FFoo : TFoo;
    procedure Foo;
  public
    class function Construct(): TDemo;
  end;

procedure TDemo.Foo;
begin
  WriteLn('foo');
end;

class function TDemo.Construct: TDemo;
begin
  result := TDemo.Create();
  result.FFoo := result.foo;
end;

end.

这也会产生相同的编译器错误 (E2555)。因为成员方法是 procedure of object (对象方法)类型,而您将其分配给 reference to procedure (匿名方法)类型,所以这相当于(我怀疑编译器将其扩展为) :

class function TDemo.Construct: TDemo;
begin
  result := TDemo.Create();
  result.FFoo := procedure
                 begin
                   result.foo;
                 end;
end;

编译器无法直接分配方法引用(因为它们属于不同类型),因此(我猜)必须将其包装在匿名方法中,这隐式需要捕获 result 变量。 函数 return 值不能被匿名方法捕获,但是 - 只有局部变量可以。

在你的情况下(或者,实际上,对于任何 function 类型),由于隐藏了 result 变量的匿名包装器,甚至无法表达等价物,但我们可以想象在理论为:

class function TDemo.Construct: TDemo;
begin
  Result := TDemo.Create();
  Result.FVar := function(const L, R : string) : integer
                 begin
                   result := result.CompareInternal(L,R);  // ** can't do this
                 end;
end;

正如 David 所展示的那样,引入局部变量(可以捕获)是一种正确的解决方案。或者,如果您不需要 TConstFunc 类型是匿名的,您可以简单地将其声明为常规对象方法:

TConstFunc<T1, T2, TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;


另一个尝试捕获 result 失败的示例:

program Project1;

{$APPTYPE CONSOLE}

type
  TBar = reference to procedure;
  TDemo = class
  private
    FFoo : Integer;
    FBar : TBar;
  public
    class function Construct(): TDemo;
  end;

class function TDemo.Construct: TDemo;
begin
  result := TDemo.Create();
  result.FFoo := 1;
  result.FBar := procedure
                 begin
                   WriteLn(result.FFoo);
                 end;
end;

end.

这不起作用的根本原因是方法的 return 值实际上是一个 var 参数并且匿名闭包捕获 变量 ,而不是。这是一个关键点。同样,这也是不允许的:

program Project1;

{$APPTYPE CONSOLE}

type
  TFoo = reference to procedure;

  TDemo = class
  private
    FFoo : TFoo;
    procedure Bar(var x : integer);
  end;

procedure TDemo.Bar(var x: Integer);
begin
  FFoo := procedure
          begin
            WriteLn(x);
          end;
end;

begin
end.

[dcc32 Error] Project1.dpr(18): E2555 Cannot capture symbol 'x'

在引用类型的情况下,就像在原始示例中一样,您实际上只对捕获引用的 而不是 变量感兴趣 包含它。这不会使其在语法上等效,并且编译器为此目的为您创建一个新变量是不合适的。

我们可以将上面的重写为这样,引入一个变量:

procedure TDemo.Bar(var x: Integer);
var
  y : integer;
begin
  y := x;
  FFoo := procedure
          begin
            WriteLn(y);
          end;
end;

这是允许的,但预期的行为会大不相同。在捕获 x(不允许)的情况下,我们期望 FFoo 总是将作为参数 x 传入的任何变量的当前值写入 Bar,无论在何时何地可能在此期间发生了更改。我们还希望闭包能够使变量保持活动状态,即使它脱离了创建它的任何范围。

然而,在后一种情况下,我们期望 FFoo 输出 y 的值,这是变量 x 的值,因为它是 最后一次 Bar 被调用


回到第一个例子,考虑这个:

program Project1;    
{$APPTYPE CONSOLE}    
type
  TFoo = reference to procedure;    
  TDemo = class
  private
    FFoo : TFoo;
    FBar : string;
    procedure Foo;
  public
    class function Construct(): TDemo;
  end;

procedure TDemo.Foo;
begin
  WriteLn('foo' + FBar);
end;

class function TDemo.Construct: TDemo;
var
  LDemo : TDemo;
begin
  result := TDemo.Create();
  LDemo := result;
  LDemo.FBar := 'bar';
  result.FFoo := LDemo.foo;
  LDemo := nil;
  result.FFoo();  // **access violation
end;

var
 LDemo:TDemo;
begin
  LDemo := TDemo.Construct;
end.

这里很清楚:

result.FFoo := LDemo.foo;

我们还没有为 LDemo 中存储的 TDemo 实例 的方法 foo 分配一个正常引用,但是实际上已经捕获了变量 LDemo本身,而不是它当时包含的。之后将 LDemo 设置为 nil 自然会产生访问冲突,即使它在进行分配时引用的对象实例仍然存在。

与我们简单地将 TFoo 定义为 procedure of object 而不是 reference to procedure 相比,这是 根本 不同的行为。如果我们这样做了,上面的代码会像人们天真地期望的那样工作(输出 foobar 到控制台)。

这不是一个完整的答案,而是对 David 的回答和 topicstarter 的问题的注释。

使用回答模式发布源代码片段。

class function TDemo.Construct: TDemo;
begin
  Result := TDemo.Create();
  Result.FVar := Result.CompareInternal;
end;

class function TDemo.Construct: TDemo;
var
  Demo: TDemo;
begin
  Demo := TDemo.Create();
  Demo.FVar := Demo.CompareInternal;
  Result := Demo;
end;

这两个片段使用相同的模板:

  1. 创建一个对象(以及附加到它的内存管理职责)
  2. 调整对象
  3. 将对象传递给外部世界(并m/m对其负责)

当然,这里的p.2只是一行,还是

  1. 它有一个函数调用,可能容易出错。两次如果函数将通过继承子类被虚拟覆盖。
  2. 模式不是在最简单的情况下起作用,而是在最困难的情况下起作用。

所以我认为我们应该假设 p.2 有运行时错误的风险,抛出异常的风险。那么就是教科书式的内存泄漏。本地函数仍然承担内存管理职责,因为它没有将结果传递给外部。但它也没有完成所需的清理工作。

从我的角度来看,正确的模式 - 以及比仅仅 Result/Result 编译器混淆更能激励人们使用专用局部变量的模式 - 应该是

class function TDemo.Construct: TDemo;
var
  Demo: TDemo;
begin

  Demo := TDemo.Create();  // stage 1: creating an object
  try                      // stage 1: accepting M/M responsibilities

     Demo.FVar := Demo.CompareInternal; // stage 2: tuning and facing
     // Demo.xxx := yyy;                //   ...potential risks of exceptions
     // Demo.Connect(zzz);  etc

     Result := Demo;   // stage 3: passing the object outside
     Demo := nil;      // stage 3: abandoning M/M responsibilities
     //  function exit should follow this line immediately, without other fault-risky statements
  finally
    Demo.Free;         // proceeding with M/M in case of faults in stage 2
  end;
end;                   // stage 3: passing the object outside - immediately after the assignments!

UPD:ventiseis:并且作为一个侧节点:我将尝试仅实例化配置的比较器 TDemo 一次。比较函数应该是无状态函数

  TDemo = class(TComparer<string>)
  private
    class var FVar: TConstFunc<string, string, Integer>;
   // function CompareInternal(const L, R: string): Integer; STATIC; // also possible
    class constructor InitComp;
  ...
  end;

  // would only be called once, if the class is actually used somewhere in the project
  class constructor TDemo.InitComp; 
  begin
    FVar := function(const L, R: string): Integer
    begin
      Result := StrToInt(R) - StrToInt(L)
    end 
  end;