C#语言的设计一直(历史)紧紧围绕解决特定问题而不是寻找到解决根本的一般问题:例如见http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/迭代器块部分,one.aspx为“IEnumerable的协同程序主场迎战”:
我们可以使人们更普遍。 我们的迭代器块可以被看作是一种弱协程。 我们可以选择实现全面协同程序和刚做迭代块协同程序的一种特殊情况。 当然,协同程序又比一流的延续一般较小; 我们可以在协同程序方面已经实现了延续,在延续方面实现协同程序和迭代。
或http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx为作为的SelectMany的(某种)单子替代:
C#的类型系统是没有强大到足以创建单子广义抽象这是用于创建扩展方法的主要动力和“查询模式”
我不想问,为什么一直这么(多好的答案已经被给予,尤其是在Eric的博客,这可能适用于所有这些设计决策:从性能到增加的复杂性,既为编译器和程序员)。
我试图了解是哪个“一般构造”异步/等待关键字涉及到(我最好的猜测是延续单子 - 毕竟,F#异步使用的工作流程,这在我的理解是延续单子来实现),和它们如何与它(它们之间的区别?缺什么?为什么还有一定的差距,如果有的话?)
我在寻找类似埃里克利珀文章中,我挂一个答案,但涉及到异步/等待,而不是IEnumerable的/产量。
编辑 :除了伟大的答案,一些有用的链接到相关的问题,并建议地方博客文章,我编辑我的问题一一列举:
- 甲起点
bind
使用AWAIT - 状态机的实现细节背后等待
- 如何等待其他细节被编译/改写
- 替代方案中,假设执行使用continuation(呼叫/ cc)的
C#中的异步编程模型非常类似于在F# 异步工作流 ,这是一般的单子图案的一个实例。 事实上,C#语法迭代器也是这种模式的一个实例,尽管它需要一些额外的结构,所以它不只是简单的单子。
解释这是远远超出一个SO答复的范围,但让我解释的主要观点。
单子操作。 C#的异步基本上由两个基本操作。 您可以await
异步计算,你可以return
从异步计算的结果(在第一种情况下,这是使用新的关键字完成的,而在第二种情况下,我们正在重新使用的关键字已经在语言)。
如果你是以下的一般模式( 单子 ),那么你会转化异步代码到调用下面两个步骤:
Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);
它们都可以使用标准的API任务很容易实现-第一个是基本的组合ContinueWith
和Unwrap
,第二个简单的创建将立即返回值的任务。 我将使用上述两种操作,因为他们更好地捕捉想法。
翻译。 关键的是异步代码翻译成使用上述操作正常码。
让我们看的情况下,当我们avait表达e
然后将结果分配给一个变量x
和评估表达(或语句块) body
(在C#中,你可以在里面表达等待,但你总是那意思就是代码,第一受让人结果到一个变量):
[| var x = await e; body |]
= Bind(e, x => [| body |])
我使用的符号是很常见的编程语言。 的意义[| e |] = (...)
[| e |] = (...)
是我们的表达式转换e
(在“语义括号”)的一些其他表达式(...)
在上述情况下,当你有一个表达式await e
,它被转换为Bind
操作和主体(以下等待代码的其余部分)被压入被作为第二参数传递lambda函数Bind
。
这是有趣的事情发生在哪里! 而不是立即评估代码的其余部分(或阻塞线程在等待)的,所述Bind
操作可以运行异步操作(由下式表示e
它的类型的Task<T>
和,在操作完成时,它可以最终调用) lambda函数(续)跑尸体的其余部分。
翻译的想法是,它变成一个返回一些类型的普通代码R
到异步返回值的任务-也就是Task<R>
在上述公式中,返回类型Bind
,着实是一个任务。 这也是为什么我们需要翻译return
:
[| return e |]
= Return(e)
这是很简单 - 当你有一个结果值,你想退货,你只需将它放在立即完成的任务。 这听起来似乎无用,但请记住,我们需要返回一个Task
,因为Bind
操作(和我们的整个翻译)要求。
更大的示例。 如果你看一下中包含多个更大的示例await
S:
var x = await AsyncOperation();
return await x.AnotherAsyncOperation();
该代码将被转换为这样的事:
Bind(AsyncOperation(), x =>
Bind(x.AnotherAsyncOperation(), temp =>
Return(temp));
关键诀窍是,每一个Bind
转动代码的其余部分成延续(这意味着它可以当完成一个异步操作来评价)。
延续单子。 在C#中,异步机制实际上并没有使用上述转换实施。 其原因是,如果你只关注异步,你可以做一个更高效的编译时(这是C#那样),直接产生一个状态机。 然而,上面的是相当多的工作流程在F#如何异步工作。 这也是在F#额外的灵活性源-你可以定义自己的Bind
,并Return
意味着其他的东西-比如操作与序列工作,跟踪记录,创造可恢复计算,甚至有序列结合异步计算(异步序列可以产生多个结果,也可以等待)。
的F#的实现是基于延续单子这意味着Task<T>
实际上, Async<T>
在F#被粗略的定义是这样的:
Async<T> = Action<Action<T>>
也就是说,一个异步计算是有所行动。 当你给它Action<T>
延续)作为参数,它会开始做一些工作,然后当它最终结束时,它会调用你指定的这个动作。 如果您搜索延续单子,那么我敢肯定,你可以找到在C#和F#的这个更好的解释,所以我会在这里停...
托马斯的回答是非常好的。 要添加一些更多的东西:
C#语言的设计一直(历史)紧紧围绕解决特定问题而不是寻找到解决问题的根本一般问题
虽然有一定的道理,我不认为这是一个完全公平或者准确的表征,所以我要去否认你的问题的前提下,开始我的答案。
这是千真万确的存在与一端“非常具体”和“很一般”,另一方面,和具体问题的解决方案落在频谱频谱 。 C#设计作为一个整体是一个伟大的许多具体问题,高度通用的解决方案; 这是一个通用编程语言是什么。 您可以使用C#写一切从Web服务到Xbox 360个游戏。
由于C#的设计是一个通用的编程语言,当设计团队标识特定用户的问题,他们总是考虑更一般的情况。 LINQ是在点优异的情况下。 在LINQ的设计的最早期,它比一的方式把SQL语句在C#程序多一点,因为这是问题的空间被确定。 但在设计过程相当快的团队意识到,排序,筛选,分组和连接数据的概念应用于不仅仅是表格数据在关系数据库中,但在XML中也是分层的数据,并在内存中临时对象。 因此,他们决定去今天,我们有更通用的解决方案。
设计的诀窍是搞清楚的频谱是有意义的停止。 设计团队可能会说,好吧,查询理解问题实际上是结合单子更普遍的问题只是一个特例。 和结合单子问题实际上是对高种类型定义操作的更普遍的问题只是一个特例。 而且肯定有一些在抽象类型系统......和适可而止。 当我们去解决的bind-AN-任意单子的问题时,解决的办法是现在太笼统,谁是在首位的功能动机的业务线-SQL程序员完全丧失,我们的避风港“T实际上解决了他们的问题。
因为C#1.0中加入的真正主要功能 - 泛型,匿名函数,迭代器块,LINQ的,动态的,异步 - 都有他们是高度在许多不同领域的有用的一般功能属性。 它们都可以被视为一个更一般的问题的具体例子,但这是任何解决任何问题的真实; 你总是可以使其更加普遍。 每个功能的设计想法是找到他们不能做出更一般不混淆他们的用户点。
现在,我已经拒绝了你的问题的前提下,让我们来看看实际的问题:
我试图了解是哪个“一般构造”异步/等待关键字涉及到
这取决于你怎么看了。
该异步等待功能是围绕构建Task<T>
类型,这是因为你注意到,一个单子。 当然,如果你谈过这与埃里克·梅杰的他会立即指出, Task<T>
实际上是一个comonad; 你可以得到T
值回了另一端。
看功能的另一种方法是把你引述有关“迭代”迭代块而代以“异步”的段落。 异步方法,如迭代方法,一种协程。 你能想到的Task<T>
如果你喜欢的只是协程机制的实现细节。
看功能的第三种方式是说,它是一种呼叫与电流延续(通常简称呼叫/ CC)的。 这不是呼叫/ cc的完整实现,因为它并不需要在这个延续是签署了考虑时间的调用堆栈的状态。 详情请参见这个问题:
怎么能在C#5.0新功能的异步调用与/ cc的实施?
我会等待,看看是否有人(埃里克·乔恩?也许你?)可以在C#中如何实际生成代码来实现有待于更多的详细信息填写,
重写本质上只是对迭代器块如何改写的变化。 MADS经历在他的MSDN杂志文章的所有细节:
http://msdn.microsoft.com/en-us/magazine/hh456403.aspx
文章来源: How does C# async/await relates to more general constructs, e.g. F# workflows or monads?