【问题标题】:Scope of anonymous methods匿名方法的范围
【发布时间】:2010-10-22 12:24:18
【问题描述】:

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

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

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

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

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

... (List : TList)

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

【问题讨论】:

  • 使用指针代替引用参数。

标签: delphi scope delphi-2009 anonymous-methods


【解决方案1】:

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 为什么要谈论堆栈?捕获的变量不存储在堆栈中,而是存储在实现接口的隐藏对象中。 IE。 var M, N:整数; - 如果在匿名方法中只使用 N,则 M 进入堆栈,N 将是隐藏对象的字段。它不会出现在堆栈上。我是不是误解了什么?
  • @Alexander:Barry 描述了当它被允许捕获和 var 参数和函数结果时会发生什么情况。由于不允许,因此不会发生堆栈覆盖的情况。
  • Alexander, out 和 var 参数是通过引用传递的。这意味着捕获变量只会捕获对存储位置的引用,而不是位置本身。编译器无法捕获 var 或 out 参数后面的位置,因为变量捕获是通过将存储从堆栈移动到堆来实现的,这需要在内部重写声明该位置的方法。由于任何代码都可以调用带有 var 或 out 参数的方法,包括来自其他语言的方法,因此重写该方法为时已晚。它必须在编译时发生。
【解决方案2】:

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

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

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

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

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

还有一个C# question about circumventing your problem

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • 关于您的第一点:我使用的每个局部变量都是如此,不是吗?第二点:如果我仍然希望匿名方法产生函数结果怎么办?我可以使用局部变量轻松模拟这一点,在匿名方法中使用该局部变量,然后将其分配给 Result aftwerwards。
    • 只是为了澄清我的观点:如果我使用 anonmouy 方法作为委托,那会导致同样的问题,不是吗?
    • 不,对于方法本身中的局部变量来说不是这样。它们将(无论如何假设 Delphi 就像 C#)通过将它们放在堆上来捕获 - 对局部变量的任何引用实际上将通过该范围内所有局部变量的“容器”。编译器能够对方法中的局部变量执行此操作,因为它知道将捕获哪些变量 - 但它不能对代码中的局部变量执行此操作调用此方法。
    【解决方案4】:

    我将其放在单独的答案中,因为您的 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。 您的编辑也是如此:当底层实例死亡时,您不应该调用匿名方法。

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

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

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

    --杰罗恩

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-11-19
      • 2013-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多