匿名方法的范围

时间:2009-04-29 06:41:17

标签: delphi scope delphi-2009 anonymous-methods

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

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

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

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

编辑我刚刚意识到它适用于普通参数,例如

... (List : TList)

这不像其他案件那样有问题吗?谁保证只要执行匿名方法,引用仍指向活动对象?

4 个答案:

答案 0 :(得分:20)

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

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

当然,Delphi不在托管环境中运行,并且它没有与例如安全限制相同的安全限制。 C#。这种语言可以让你做你想做的事。但是,在出错的情况下,很难诊断出错误。不良行为将表现为常规变化值中的局部变量,没有可见的近因;如果方法引用是从另一个线程调用的话会更糟。

调试起来相当困难。即使硬件内存断点也会是一个相对较差的工具,因为堆栈经常被修改。人们需要在遇到另一个断点时有条件地打开硬件存储器断点(例如,在方法输入时)。 Delphi调试器可以做到这一点,但我猜想大多数人都不知道这种技术。

更新:关于问题的添加,按值传递实例引用的语义在包含闭包的方法之间略有不同(并捕获参数0和不包含的方法)两个方法都可以保留对value传递的参数的引用;不捕获参数的方法可以简单地将引用添加到列表中,或者将其存储在私有字段中。

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

procedure GetSomeString(out s: string);
// ...
GetSomeString(s);
如果GetSomeString保持对传入的s变量的引用,那么

会非常惊讶。另一方面:

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

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

答案 1 :(得分:6)

问题是你的Str1变量不是由ReturnTwoStrings“拥有”的,所以你的匿名方法无法捕获它。

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

修改(在Smasher评论后添加)

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

Allen Bauer(CodeGear)解释了更多about variable capturing in his blog

还有一个C# question about circumventing your problem

答案 2 :(得分:4)

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

Out参数特别困难 - 在您稍后调用委托时,out参数别名的变量甚至可能不存在。例如,假设您能够捕获out参数并返回匿名方法,但out参数是调用函数中的局部变量,它位于堆栈中。如果调用方法然后在某个地方存储委托(或返回它)后返回,最后调用委托时会发生什么?何时设置out参数的值会写入什么?

答案 3 :(得分:0)

我将这个问题放在一个单独的答案中,因为你的编辑让你的问题变得非常不同。

我可能会稍后延长这个答案,因为我有点急于找到客户。

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

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

值类型的值存在于堆栈中并具有赋值复制行为。 (我稍后会尝试在其中加入一个例子。)

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

如果有out,var或const,则不会发生复制:该方法将引用传递的实际值。对于var,它将允许更改该实际值,对于const,它将不允许这样做。对于out,您将无法读取实际值,但仍能够写出实际值。

引用类型的值存在于堆上,因此对于它们来说,如果你有out,var,const或没有参数标记就很难了:当你改变某些东西时,你改变堆上的值。

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

这是匿名方法变得复杂的地方:它们执行变量捕获。 (Barry可能会更好地解释这一点,但我会试一试) 在您编辑的案例中,匿名方法将捕获List的本地副本。匿名方法将在该本地副本上工作,从编译器的角度来看,一切都很花哨。

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

无论您是否使用匿名方法,参数参数始终存在问题。

例如:

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

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

谁保证当有人调用DoSomething时,FValue指向的实例仍然存在? 答案是,当FValue的实例已经死亡时,你必须自己保证不要调用DoSomething。 编辑也是如此:当基础实例死亡时,不应该调用匿名方法。

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

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

希望我的回答可以帮助你进入那个领域。

- 的Jeroen