现在,如果我尝试编译以下示例,编译器将显示
[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.
解决方法
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).因为成员方法是一个object(object method)类型的过程,而是将它分配给一个对过程(匿名方法)类型的引用,这相当于(我怀疑编译器正在将其扩展为):
class function TDemo.Construct: TDemo; begin result := TDemo.Create(); result.FFoo := procedure begin result.foo; end; end;
编译器不能直接分配方法引用(因为它们是不同类型的),因此(我猜想)必须将它包装成一个隐式需要捕获结果变量的匿名方法.函数返回值不能被匿名方法捕获,但只有局部变量可以.
在你的情况下(或者,实际上,对于任何函数类型),由于匿名包装器隐藏了结果变量,所以不能表达等价物,但我们可以想象在理论上是相同的:
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,TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;
尝试捕获结果的另一个示例将失败:
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.
这个不起作用的根本原因是因为一个方法的返回值实际上是一个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设置为零后,自然会产生访问冲突,甚至认为在分配作业时引用的对象实例仍然存在.
这与我们简单地将TFoo定义为对象的过程而不是引用过程的行为截然不同.如果我们这样做,那么上面的代码就像一个可能天真地期待的(输出foobar到控制台).