异步方法中的 ref 和 out 参数

106

有人知道为什么async方法不允许使用refout参数吗?我已经做了一些调查,但唯一找到的是这与堆栈展开有关。


我认为你可以在这里找到答案 https://dev59.com/5GMl5IYBdhLWcg3wDzZG - Alessio
请查看servy42来自http://social.msdn.microsoft.com/Forums/vstudio/en-US/df2b3fb6-000b-438f-a32d-a0504ee66e77/whats-the-reason-that-async-methods-cant-have-outref-parameters的答案。 - Soner Gönül
http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have-ref-or-out-parameters?forum=async - santosh singh
2个回答

156
有人知道为什么异步方法不允许有ref和out参数吗?
异步方法通常会在大部分实际逻辑被执行之前几乎立即返回,这是异步完成的。因此,任何out参数都必须在第一个await表达式之前被赋值,而且可能需要对ref参数施加某些限制,以防止它们在第一个await表达式之后被使用,因为此后它们可能已经无效。
考虑使用本地变量作为参数调用具有out和ref参数的异步方法:
int x;
int y = 10;
FooAsync(out x, ref y);

FooAsync返回之后,该方法本身可以返回 - 因此那些局部变量在逻辑上不再存在...但异步方法仍然可以在其继续运行时有效地使用它们。这是一个大问题。编译器可以创建一个新类来捕获变量,就像对lambda表达式一样,但这会引起其他问题...除此之外,当继续运行在不同的线程上时,你可能会看到一个本地(local)变量在方法中任意点处改变。最好不要这样做。

基本上,由于涉及时间问题,对于async方法来说,使用outref参数是没有意义的。相反,应该使用包含您感兴趣的所有数据的返回类型。

如果您只关心outref参数在第一个await表达式之前是否更改,您可以将方法拆分成两个:

public Task<string> FooAsync(out int x, ref int y)
{
    // Assign a value to x here, maybe change y
    return FooAsyncImpl(x, y);
}

private async Task<string> FooAsyncImpl(int x, int y) // Not ref or out!
{
}

编辑:使用 Task<T> 可以使用 out 参数,并直接在方法中分配值,就像返回值一样。尽管这可能有点奇怪,并且对于 ref 参数不起作用。


1
@hvd:是的,它有可能这样做 - 将编辑以包括这种可能性。虽然它不能将一个out参数作为另一个out参数的参数传递... - Jon Skeet
当然,这是一个很好的观点。它可以传递引用,但如果那个引用的是未包装在类中的局部变量,你仍然会遇到同样的问题。 - user743382
我认为我错了,如果没有对CIL进行扩展,这是不可能的。 - user743382
我不明白,你是不是指的 Action<T> 而不是 Task<T>?如果是,那是个好主意。 - user743382
@hvd:不,我指的是任务。把它看作另一个返回值——任务在整个方法完成之前不会完成,此时该值将是方法中分配的最后一个值。有点奇怪,但可行。 - Jon Skeet
啊,我以为你的意思是实际上应该是 await FooAsyncImpl(val => x = val, val => y = val) 而不是 await FooAsyncImpl(out x, out y),后者只适用于 out,且仅适用于 out(而且您在设置后不会再读取它)。感谢澄清,我仍然不确定我是否理解了,但我会考虑一下。 :) - user743382

30

C#被编译为公共中间语言(CIL),而CIL不支持此功能。

CIL本身不支持asyncasync方法被编译为类,所有(使用的)参数和局部变量都存储在类字段中,因此当调用该类的特定方法时,代码会知道如何继续执行以及变量具有哪些值。

refout参数使用托管指针实现,并且不允许托管指针类型的类字段,因此编译器无法保留传递的引用。

类字段上的托管指针限制防止了一些荒谬的代码,正如Jon Skeet的答案中所解释的那样,因为类字段中的托管指针可能指向已经返回的函数的局部变量。但是,这种限制太严格了,即使安全和其他正确的用法也被拒绝。 如果ref/out字段引用另一个类字段,并且编译器确保始终将使用ref/out传递的局部变量包装在类中(就像它已经知道如何做的那样),则其可以工作。

所以,C# 简单地没有任何方法可以绕过 CIL 强加的限制。即使 C# 设计师想要允许它(我并不是说他们这样做),他们也不能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接