匿名方法的范围
匿名方法的一大好处是我可以使用调用上下文中的本地变量。 这对于输出参数和函数结果不起作用有什么原因吗?
function ReturnTwoStrings (out Str1 : String) : String;
begin
ExecuteProcedure (procedure
begin
Str1 := 'First String';
Result := 'Second String';
end);
end;
当然,这是非常人为的例子,但我遇到了一些情况,这会很有用。
当我尝试编译它时,编译器抱怨他“无法捕获符号”。 另外,当我尝试执行此操作时,我遇到了一次内部错误。
编辑我刚刚意识到它适用于正常参数,例如
... (List : TList)
这不是像其他情况一样有问题吗? 谁保证每当执行匿名方法时引用仍然指向活动对象?
One nice thing about anonymous methods is that I can use variables that are local in the calling context. Is there any reason why this does not work for out-parameters and function results?
function ReturnTwoStrings (out Str1 : String) : String;
begin
ExecuteProcedure (procedure
begin
Str1 := 'First String';
Result := 'Second String';
end);
end;
Very artificial example of course, but I ran into some situations where this would have been useful.
When I try to compile this, the compiler complains that he "cannot capture symbols". Also, I got an internal error once when I tried to do this.
EDIT I just realized that it works for normal parameters like
... (List : TList)
Isn't that as problematic as the other cases? Who guarantees that the reference is still pointing to an alive object whenever the anonymous method is executed?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
无法捕获 Var 和 out 参数以及 Result 变量,因为无法静态验证此操作的安全性。 当 Result 变量是托管类型(例如字符串或接口)时,存储实际上由调用者分配,并且对此存储的引用作为隐式参数传递; 换句话说,Result 变量,根据其类型,就像一个输出参数。
由于乔恩提到的原因,安全性无法得到验证。 由匿名方法创建的闭包可以比创建它的方法激活的寿命长,并且同样可以比调用它创建的方法的方法的激活的寿命长。 因此,捕获的任何 var 或 out 参数或 Result 变量最终都可能成为孤立变量,并且将来从闭包内部对它们的任何写入都会破坏堆栈。
当然,Delphi 并不在托管环境中运行,并且它没有与 C# 等相同的安全限制。 该语言可以让你做你想做的事。 然而,在出现问题的情况下,这会导致很难诊断错误。 不良行为将表现为例程中的局部变量改变值,而没有明显的直接原因; 如果从另一个线程调用方法引用,情况会更糟。
这将很难调试。 即使硬件内存断点也是一个相对较差的工具,因为堆栈会被频繁修改。 人们需要在命中另一个断点时(例如,在方法进入时)有条件地打开硬件内存断点。 Delphi 调试器可以做到这一点,但我大胆猜测大多数人不了解这项技术。
更新:关于问题的补充,按值传递实例引用的语义在包含闭包的方法(并捕获 paramete0 和不包含闭包的方法)之间几乎没有什么不同。任一方法都可以保留对按值传递的参数的引用;不捕获参数的方法可能只是将引用添加到列表中,或者将其存储在私有字段中,
因为调用者的期望,情况与按引用传递的参数不同。这样做的程序员是不同的:
如果 GetSomeString 保留对传入的
s
变量的引用,他会感到非常惊讶。另一方面:AddObject
保留一个引用,因为它的名称意味着它将参数添加到某个有状态存储中,无论该有状态存储是否采用闭包的形式都是AddObject
方法的实现细节。Var and out parameters and the Result variable cannot be captured because the safety of this operation cannot be statically verified. When the Result variable is of a managed type, such as a string or an interface, the storage is actually allocated by the caller and a reference to this storage is passed as an implicit parameter; in other words, the Result variable, depending on its type, is just like an out parameter.
The safety cannot be verified for the reason Jon mentioned. The closure created by an anonymous method can outlive the method activation where it was created, and can similarly outlive the activation of the method that called the method where it was created. Thus, any var or out parameters or Result variables captured could end up orphaned, and any writes to them from inside the closure in the future would corrupt the stack.
Of course, Delphi does not run in a managed environment, and it doesn't have the same safety restrictions as e.g. C#. The language could let you do what you want. However, it would result in hard to diagnose bugs in situations where it went wrong. The bad behaviour would manifest itself as local variables in a routine changing value with no visible proximate cause; it would be even worse if the method reference were called from another thread.
This would be fairly hard to debug. Even hardware memory breakpoints would be a relatively poor tool, as the stack is modified frequently. One would need to turn on the hardware memory breakpoints conditionally upon hitting another breakpoint (e.g. upon method entry). The Delphi debugger can do this, but I would hazard a guess that most people don't know about the technique.
Update: With respect to the additions to your question, the semantics of passing instance references by value is little different between methods that contain a closure (and capture the paramete0 and methods that don't contain a closure. Either method may retain a reference to the argument passed by value; methods not capturing the parameter may simply add the reference to a list, or store it in a private field.
The situation is different with parameters passed by reference because the expectations of the caller are different. A programmer doing this:
would be extremely surprised if GetSomeString were to keep a reference to the
s
variable passed in. On the other hand:It is not surprising that
AddObject
keeps a reference, since the very name implies that it's adding the parameter to some stateful store. Whether that stateful store is in the form of a closure or not is an implementation detail of theAddObject
method.问题是您的 Str1 变量不属于 ReturnTwoStrings,因此您的匿名方法无法捕获它。
它无法捕获它的原因是编译器不知道最终所有者(在调用 ReturnTwoStrings 的调用堆栈中的某个位置),因此它无法确定从哪里捕获它。
编辑:(在Smasher的评论后添加)
匿名方法的核心是它们捕获变量(不是它们的值)。
Allen Bauer (CodeGear) 进一步解释了关于变量捕获的内容 在他的博客中。
有一个 C# 问题有关规避您的问题也是如此。
The problem is that your Str1 variable is not "owned" by ReturnTwoStrings, so that your anonymous method cannot capture it.
The reason it cannot capture it, is that the compiler does not know the ultimate owner (somewhere in the call stack towards calling ReturnTwoStrings) so it cannot determine where to capture it from.
Edit: (Added after a comment of Smasher)
The core of anonymous methods is that they capture the variables (not their values).
Allen Bauer (CodeGear) explains a bit more about variable capturing in his blog.
There is a C# question about circumventing your problem as well.
函数返回后,out 参数和返回值不再相关 - 如果您捕获匿名方法并稍后执行它,您会期望匿名方法如何表现? (特别是,如果您使用匿名方法创建委托但从未执行它,则在函数返回时不会设置 out 参数和返回值。)
Out 参数特别困难 - out 参数所代表的变量当您稍后调用委托时,别名甚至可能不存在。 例如,假设您能够捕获 out 参数并返回匿名方法,但 out 参数是调用函数中的局部变量,并且位于堆栈上。 如果调用方法在将委托存储在某处(或返回它)之后返回,那么当最终调用委托时会发生什么? 当设置了 out 参数的值时,它会写入哪里?
The out parameter and return value are irrelevant after the function returns - how would you expect the anonymous method to behave if you captured it and executed it later? (In particular, if you use the anonymous method to create a delegate but never execute it, the out parameter and return value wouldn't be set by the time the function returned.)
Out parameters are particularly difficult - the variable that the out parameter aliases may not even exist by the time you later call the delegate. For example, suppose you were able to capture the out parameter and return the anonymous method, but the out parameter is a local variable in the calling function, and it's on the stack. If the calling method then returned after storing the delegate somewhere (or returning it) what would happen when the delegate was finally called? Where would it write to when the out parameter's value was set?
我将其放在一个单独的答案中,因为您的编辑使您的问题非常不同。
我可能会稍后扩展这个答案,因为我有点急于去找客户。
您的编辑表明您需要重新考虑值类型、引用类型以及 var、out、const 和无参数标记的效果。
让我们先做值类型的事情。
值类型的值存在于堆栈中并且具有赋值时复制的行为。
(稍后我将尝试包含一个示例)。
当没有参数标记时,传递给方法(过程或函数)的实际值将被复制到方法内该参数的本地值。 因此该方法不会对传递给它的值进行操作,而是对副本进行操作。
当你有 out、var 或 const 时,就不会发生复制:该方法将引用传递的实际值。 对于 var,它将允许更改实际值,对于 const,它将不允许这样做。 对于 out,您将无法读取实际值,但仍然能够写入实际值。
引用类型的值存在于堆上,因此对于它们来说,是否有 out、var、const 或没有参数标记并不重要:当您更改某些内容时,您会更改堆上的值。
对于引用类型,当没有参数标记时,您仍然会获得一个副本,但那是仍然指向堆上的值的引用的副本。
这就是匿名方法变得复杂的地方:它们进行变量捕获。
(巴里可能可以更好地解释这一点,但我会尝试一下)
在您编辑的情况下,匿名方法将捕获列表的本地副本。 匿名方法将在本地副本上运行,从编译器的角度来看,一切都很好。
然而,编辑的关键是“它适用于普通参数”和“谁保证每当执行匿名方法时引用仍然指向活动对象”的组合。
无论您是否使用匿名方法,这始终是引用参数的问题。
例如:
谁保证当有人调用 DoSomething 时,FValue 指向的实例仍然存在?
答案是,当 FValue 实例死亡时,您必须通过不调用 DoSomething 来保证这一点。
这同样适用于您的编辑:当底层实例死亡时,您不应该调用匿名方法。
这是引用计数或垃圾收集解决方案使生活变得更轻松的领域之一:实例将保持活动状态,直到对它的最后一个引用消失(这可能会导致实例的寿命比您最初预期的更长!)。
因此,通过您的编辑,您的问题实际上从匿名方法更改为使用引用类型参数和一般生命周期管理的含义。
希望我的回答对您进入该领域有所帮助。
——杰罗恩
I'm putting this in a separate answer because your EDIT makes your question really different.
I'll probably extend this answer later as I'm in a bit of a hurry to get to a client.
Your edit indicates you need to rethink about value types, reference types and the effect of var, out, const and no parameter marking at all.
Let's do the value types thing first.
The values of value types live on the stack and have a copy-on-assignment behaviour.
(I'll try to include an example on that later).
When you have no parameter marking, the actual value passed to a method (procedure or function) will be copied to the local value of that parameter inside the method. So the method does not operate on the value passed to it, but on a copy.
When you have out, var or const, then no copy takes place: the method will refer to the actual value passed. For var, it will allow to to change that actual value, for const it will not allow that. For out, you won't be able to read the actual value, but still be able to write the actual value.
Values of reference types live on the heap, so for them it hardly matters if you have out, var, const or no parameter marking: when you change something, you change the value on the heap.
For reference types, you still get a copy when you have no parameter marking, but that is a copy of a reference that still points to the value on the heap.
This is where anonymous methods get complicated: they do a variable capture.
(Barry can probably explain this even better, but I'll give it a try)
In your edited case, the anonymous method will capture the local copy of the List. The anonymous method will work on that local copy, and from a compiler perspective everything is dandy.
However, the crux of your edit is the combination of 'it works for normal parameters' and 'who guarantees that the reference is still pointing to an alive object whenever the anonymous method is executed'.
That is always a problem with reference parameters, no matter if you use anonymous methods or not.
For instance this:
Who guarantees that when someone calls DoSomething, that the instance where FValue points to still exists?
The answer is that you must guarantee this yourself by not calling DoSomething when the instance to FValue has died.
The same holds for your edit: you should not call the anonymous method when the underlying instance has died.
This is one of the areas where reference counted or garbage collected solutions make life easier: there the instance will be kept alive until the last reference to it has gone away (which might cause instance to live longer than you originally anticipated!).
So, with your edit, your question actually changes from anonymous methods to the implications of using reference typed parameters and lifetime management in general.
Hopefully my answer helps you going in that area.
--jeroen