11

匿名方法的一个好处是我可以在调用上下文中使用本地变量。有什么理由为什么这不适用于参数和函数结果?

function ReturnTwoStrings (out Str1 : String) : String;
begin
  ExecuteProcedure (procedure
                    begin
                      Str1 := 'First String';
                      Result := 'Second String';
                    end);
end;

当然是非常人为的例子,但我遇到了一些这很有用的情况。

当我尝试编译它时,编译器抱怨他“无法捕获符号”。此外,当我尝试这样做时,我遇到了一个内部错误。

编辑我刚刚意识到它适用于正常参数,如

... (List : TList)

这不是和其他情况一样有问题吗?谁保证每当执行匿名方法时引用仍然指向活动对象?

4

4 回答 4

21

由于无法静态验证此操作的安全性,因此无法捕获var和out参数以及Result变量。当 Result 变量是托管类型时,例如字符串或接口,存储实际上是由调用者分配的,并且对该存储的引用作为隐式参数传递;换句话说,Result 变量,取决于它的类型,就像一个输出参数。

由于乔恩提到的原因,无法验证安全性。由匿名方法创建的闭包可以比创建它的方法激活的寿命更长,并且同样可以比调用它创建的方法的方法的激活寿命更长。因此,捕获的任何 var 或 out 参数或 Result 变量最终都可能成为孤立的,并且将来从闭包内部对它们的任何写入都会破坏堆栈。

当然,Delphi 并不在托管环境中运行,它也没有像 C# 那样的安全限制。语言可以让你做你想做的事。但是,在出错的情况下,这将导致难以诊断错误。不良行为将表现为例行更改值中的局部变量,而没有明显的近因;如果方法引用是从另一个线程调用的,情况会更糟。

这将很难调试。即使是硬件内存断点也将是一个相对较差的工具,因为堆栈经常被修改。需要在遇到另一个断点时(例如在方法入口时)有条件地打开硬件内存断点。Delphi 调试器可以做到这一点,但我会冒险猜测大多数人不了解该技术。

更新:关于您的问题的补充,按值传递实例引用的语义在包含闭包的方法之间几乎没有什么不同(并捕获 paramete0 和不包含闭包的方法。任何一种方法都可以保留对按值传递的参数;不捕获参数的方法可以简单地将引用添加到列表中,或者将其存储在私有字段中。

通过引用传递参数的情况不同,因为调用者的期望不同。这样做的程序员:

procedure GetSomeString(out s: string);
// ...
GetSomeString(s);

如果 GetSomeString 要保留对传入变量的引用,将会非常惊讶s。另一方面:

procedure AddObject(obj: TObject);
// ...
AddObject(TObject.Create);

保留一个引用并不奇怪AddObject,因为这个名字暗示它将参数添加到一些有状态的存储中。该有状态存储是否采用闭包的形式是该AddObject方法的实现细节。

于 2009-04-29T09:15:57.220 回答
6

问题是您的 Str1 变量不是 ReturnTwoStrings“拥有”的,因此您的匿名方法无法捕获它。

它无法捕获它的原因是编译器不知道最终所有者(在调用堆栈中调用 ReturnTwoStrings 的某个位置),因此它无法确定从哪里捕获它。

编辑:(在Smasher的评论后添加)

匿名方法的核心是它们捕获变量(而不是它们的值)。

Allen Bauer (CodeGear)在他的博客中解释了更多关于变量捕获的内容。

还有一个关于规避问题的 C# 问题

于 2009-04-29T06:47:13.007 回答
4

函数返回后,out 参数和返回值无关紧要 - 如果您捕获匿名方法并稍后执行它,您希望它如何表现?(特别是,如果您使用匿名方法创建委托但从不执行它,则在函数返回时不会设置 out 参数和返回值。)

输出参数特别困难 - 当您稍后调用委托时,输出参数别名的变量甚至可能不存在。例如,假设您能够捕获 out 参数并返回匿名方法,但 out 参数是调用函数中的局部变量,并且它在堆栈上。如果调用方法在将委托存储在某处(或返回它)之后返回,当委托最终被调用时会发生什么?当设置了 out 参数的值时它会写入哪里?

于 2009-04-29T06:44:40.843 回答
1

我将其放在单独的答案中,因为您的 EDIT 使您的问题变得非常不同。

稍后我可能会扩展这个答案,因为我有点急于联系客户。

您的编辑表明您需要重新考虑值类型、引用类型以及 var、out、const 和无参数标记的影响。

让我们先做值类型的事情。

值类型的值存在于堆栈中,并具有赋值时复制的行为。(稍后我将尝试包含一个示例)。

当您没有参数标记时,传递给方法(过程或函数)的实际值将被复制到方法内该参数的本地值。因此该方法不会对传递给它的值进行操作,而是对副本进行操作。

当你有 out、var 或 const 时,不会发生复制:该方法将引用传递的实际值。对于 var,它允许更改实际值,对于 const,它不允许。对于 out,您将无法读取实际值,但仍然可以写入实际值。

引用类型的值存在于堆上,因此对于它们来说,是否有 out、var、const 或没有参数标记几乎无关紧要:当您更改某些内容时,您会更改堆上的值。

对于引用类型,当您没有参数标记时,您仍然会得到一个副本,但那是仍然指向堆上的值的引用的副本。

这就是匿名方法变得复杂的地方:它们进行变量捕获。(巴里可能会更好地解释这一点,但我会试一试)在您编辑的情况下,匿名方法将捕获列表的本地副本。匿名方法将在该本地副本上工作,从编译器的角度来看,一切都是花花公子。

但是,您编辑的关键是“它适用于普通参数”和“谁保证在执行匿名方法时引用仍然指向活动对象”的组合。

无论您是否使用匿名方法,这始终是引用参数的问题。

例如这个:

procedure TMyClass.AddObject(Value: TObject);
begin
  FValue := Value;
end;

procedure TMyClass.DoSomething();
begin
  ShowMessage(FValue.ToString());
end;

谁保证当有人调用 DoSomething 时,FValue 指向的实例仍然存在?答案是您必须自己保证这一点,当 FValue 的实例死亡时不调用 DoSomething。您的编辑也是如此:当底层实例死亡时,您不应该调用匿名方法。

这是引用计数或垃圾收集解决方案使生活更轻松的领域之一:实例将保持活动状态,直到对它的最后一个引用消失(这可能导致实例的寿命比您最初预期的要长!)。

因此,通过您的编辑,您的问题实际上从匿名方法变为使用引用类型参数和生命周期管理的含义。

希望我的回答可以帮助您进入该领域。

——杰伦

于 2009-05-01T08:22:43.930 回答