我认为这不是一个bug。关键是,你将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)。由于成员方法是对象过程
(对象方法)类型,并且您正在将其分配给过程引用
(匿名方法)类型,因此这等效于(我怀疑编译器正在扩展此操作):
class function TDemo.Construct: TDemo;
begin
result := TDemo.Create();
result.FFoo := procedure
begin
result.foo;
end;
end;
编译器无法直接分配方法引用(因为它们是不同类型的),因此(我想)必须将其包装在匿名方法中,该方法会隐式地要求捕获“result”变量。
函数返回值不能被匿名方法捕获,只有局部变量可以被捕获。
在您的情况下(或者任何
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);
end;
end;
正如David所示,引入一个可以被捕获的局部变量是一种正确的解决方案。另外,如果您不需要TConstFunc
类型是匿名的,可以将其声明为常规对象方法:
TConstFunc<T1, T2, TResult> = function(const Arg1: T1
另一个尝试捕获“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.
这个不起作用的根本原因在于,方法的返回值实际上是一个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 错误] Project1.dpr(18): E2555 无法捕获符号“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
输出变量x
的值,这是Bar
上次被调用时变量x
的值。
回到第一个例子,考虑以下内容:
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();
end;
var
LDemo:TDemo;
begin
LDemo := TDemo.Construct;
end.
以下是明确的内容:
result.FFoo := LDemo.foo;
我们没有为存储在LDemo中的TDemo实例所属的方法foo分配一个正常的引用,而是实际捕获了变量LDemo本身,而不是它在那时所包含的值。之后将LDemo设置为nil会产生访问冲突,即使在进行赋值时所引用的对象实例仍然存在。
如果我们只是将TFoo定义为对象方法而不是引用到过程,那么上述代码的行为将与人们最初期望的相差很大(输出"foobar"到控制台)。
TConstFunc<T1, T2, TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;
,那么代码就可以编译并正常工作。 - ventiseis