能否等待空字面量?

10

只要类型有一个名为.GetAwaiter()的函数返回特殊对象或具有执行相同操作的扩展方法,您就可以在C# 5+中等待任何内容

Unity3d在其下一个主要版本中将支持async/await。目前在Unity3d中,您可以在协程中使用yield return null;表示“等待下一帧”。

我想知道是否可能创建一个扩展方法,允许您执行await null;以获得相同的行为。由于没有像System.Void那样的System.Null,因此我无法想到要放入扩展方法的类型。


这不就是 await Task.Yield() 吗? - spender
@spender 是的,整个扩展方法应该是 public static YieldAwaitable GetAwaiter(this ???? temp) { return Task.Yield(); } 我只是不知道 ???? 部分是否有可能出现问题。 - Scott Chamberlain
2
根据规范(自3.0版本起),字面上的null没有类型,因此我认为无法定义扩展方法。 - Blorgbeard
抱歉评论专家的问题。只是我的想法 await Task<object>.CompletedTask() 或 FromResult<T>。 - M.kazem Akhgary
2
@M.kazemAkhgary:等待一个已知值的完成任务是没有意义的;这只是将该值装箱并立即取消装箱;它甚至不会将控制权返回给调用者!如果您认为此操作在您的工作流程中有意义,则说明您对工作流程存在错误的信念。 - Eric Lippert
显示剩余3条评论
2个回答

11
你说得对,yield和await是密切相关的。它们都是工作流程中的一个点,在这个点上,当前方法被暂停,控制权被返回给调用者,并且该方法在未来的某个不确定的时间点恢复执行,从yield/await发生的点开始。
但它们在操作运算对象方面非常不同。实际上,它们是彼此的对偶。当代码迭代序列时,yield会在需要时提供新值。而当异步执行任务产生值时,await会提取该值。
空值是完全有效的值,因此yield将其提供给调用者是有意义的。但空值不是一个有效的任务,因此await试图从任务中提取值是没有意义的。
在Unity3d中,您可以在协程中使用yield return null;表示“等待下一帧”。
在async-await异步工作流程中,yield return null;的类比就是return null;。这意味着“通过提供空引用来结束此部分异步工作流程”。如果您打算生成结果为null的任务,为什么不简单地返回null呢?
让我换一种更清晰的方式来表达。显然,以下内容是没有意义的:
foreach(var x in null)
   Console.WriteLine(x);

这是毫无意义的说法:
var x = await null;
Console.WriteLine(x);

这两个逻辑上是一样的。Foreach 的意思是“从序列中提取值,只要有值可用”,但 null 不是序列。同样,“await”的意思是“从任务中提取值,只要有值可用”,但 null 不是任务。
这就是关键:异步的等待模式与 “yield return" 不同,而是使用 "foreach"。这是从 IEnumerable<T> 或 Task<T> 中提取 T 的机制。将 T 放入单子类型中的方法 是 使用 IEnumerable<T> 的 "yield return" 和 Task<T> 的 "return" 。

回应你的最后一段话,这是因为在 yield return 之后通常会有代码。例如,如果您希望循环每帧重复一次直到满足条件,可以使用 while(_condition) { Foo(); yield return null; }。Unity3d 滥用了 IEnumerator 来创建自己的异步/await 系统 - 协同程序(在 async/await 出现之前就被创造出来了)。 - Scott Chamberlain
它同样可以是 yield return new WaitForSeconds(5);,这相当于 await Task.Delay(TimeSpan.FromSeconds(5)); - Scott Chamberlain
1
@ScottChamberlain:await和yield并不完全是对偶的,因为task和enumerable也并不完全是对偶的。一个task代表了未来获取的单个值;而一个sequence则代表了按需获取的多个值。Task的对偶实际上是func,而Enumerable的对偶是observable。所以,如果你想要一个推送值序列,那么你不需要async await;你需要IObservable<T>,你需要响应式扩展。 - Eric Lippert

3
无法为字面空值定义扩展方法,因为字面量null没有类型

我们意识到 null 类型非常奇怪。它是一个只有一个值的类型——或者说它是吗?null 可空 int 的值是否真的与 null 字符串的值相同?可空值类型的值已经有了类型,即可空值类型本身。

并且:

因此,我们在 C# 3.0 规范中删除了对无用的“null类型”的引用。


虽然 null 字面量确实没有类型,而是与多种类型兼容,但这个事实并不一定会激发出设计上不在 null 字面量上查找扩展方法的决策。换句话说,假设我说,不,我们错了,应该说 null 字面量有“null 字面量类型”。那么这会激发改变关于扩展方法的设计决策吗?可能不会。 - Eric Lippert
1
我想这是正确的 - 我使用缺少空类型作为证据,表明在当前的扩展方法语法下,没有办法表达“此扩展方法适用于字面上的null”。但也许这并不是完全可靠的推理。 - Blorgbeard
1
我们可以轻松地说static Whatever Foo(this object x) { ... } ... null.Foo();是完全合法的;null可以转换为object,这就是所需的全部。现在,你会遇到static Whatever Foo<T>(this T x)的问题,因为现在null.Foo()无法为T推断类型,但无论如何,我们已经用Foo(null);解决了这个问题。不,null.Foo()被禁止是因为它闻起来很糟糕;如果你试图在空字面量上放置扩展方法,你几乎肯定做错了什么。 - Eric Lippert
没错,全都是真的。我同意这很糟糕。我将抵制编辑我的答案并引用该评论作为证据而不是您的博客文章的冲动。 - Blorgbeard

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