代码之家  ›  专栏  ›  技术社区  ›  jpfollenius Rob Kennedy

匿名方法的范围

  •  10
  • jpfollenius Rob Kennedy  · 技术社区  · 15 年前

    匿名方法的一个好处是,我可以使用调用上下文中的局部变量。对于out参数和函数结果,是否有任何原因不适用?

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

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

    当我试图编译这个时,编译器抱怨他“不能捕获符号”。另外,当我尝试这样做的时候,我有一次遇到了一个内部错误。

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

    ... (List : TList)
    

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

    4 回复  |  直到 15 年前
        1
  •  20
  •   Barry Kelly    15 年前

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

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

    当然,Delphi不在托管环境中运行,而且它没有与C_相同的安全限制。语言可以让你随心所欲。但是,在出错的情况下,它会导致很难诊断错误。不良行为将在没有明显近因的例行更改值中表现为局部变量;如果从另一个线程调用方法引用,情况会更糟。

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

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

    由于调用者的期望值不同,使用引用传递的参数时情况不同。这样做的程序员:

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

    如果getSomeString保留对 s 变量传入。另一方面:

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

    这并不奇怪 AddObject 保留一个引用,因为这个名称意味着它正在向某个有状态存储添加参数。状态存储是否以闭包的形式存在是 附加对象 方法。

        2
  •  6
  •   Community CDub    7 年前

    问题是您的str1变量不属于returnthostrings,因此匿名方法无法捕获它。

    它无法捕获它的原因是编译器不知道最终的所有者(调用堆栈中调用returnthostrings的某个地方),所以它无法确定从何处捕获它。

    编辑: (在评论 Smasher )

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

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

    有一个 C# question about circumventing your problem 也。

        3
  •  4
  •   Jon Skeet    15 年前

    out参数和返回值在函数返回之后是不相关的-如果捕获匿名方法并在稍后执行它,您希望它的行为如何?(尤其是,如果使用匿名方法创建委托但从不执行委托,则函数返回时不会设置out参数和返回值。)

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

        4
  •  0
  •   Jeroen Wiert Pluimers    15 年前

    我把它放在一个单独的答案里,因为你的编辑会让你的问题与众不同。

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

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

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

    值类型的值位于堆栈上,并具有分配行为的副本。 (稍后我将尝试包括一个例子)。

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

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

    引用类型的值存在于堆中,因此对于它们来说,如果有out、var、const或no参数标记,则几乎不重要:当您更改某个值时,您将更改堆中的值。

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

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

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

    不管您是否使用匿名方法,引用参数总是有问题的。

    例如:

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

    谁能保证当有人调用dosomething时,fValue指向的实例仍然存在? 答案是,当fValue的实例死亡时,您必须通过不调用doSomething来保证这一点。 对您的编辑同样适用:当基础实例停止时,不应调用匿名方法。

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

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

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

    ——杰罗恩