现在,如果我尝试编译以下示例,编译器将显示
[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到控制台).