
通用异步返回类型 -这是C#7的一项新功能,不仅允许将Task作为异步方法的返回类型使用,还可以使用满足某些特定要求的其他类型(类或结构)。
同时, async / await是在某些上下文中调用一组“ continuation”函数的方法,这是另一种设计模式Monad的本质。 因此,我们可以使用async / await编写代码的行为与使用monad时相同吗? 事实证明-是的(有些保留)。 例如,下面的代码是可编译的并且可以正常工作:
async Task Main() { foreach (var s in new[] { "1,2", "3,7,1", null, "1" }) { var res = await Sum(s).GetMaybeResult(); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); }
此外,我将解释代码的工作原理...
广义异步返回类型
首先,让我们弄清楚使用我们自己的类型(例如MyAwaitable <T> )作为某些异步函数的结果类型需要什么。 文档说这种类型必须具有:
GetAwaiter()方法,它返回一个实现INotifyCompletion接口并具有bool IsCompleted属性和T GetResult()方法的类型的对象;
[AsyncMethodBuilder(Type)]属性指向“方法生成器”类( 或结构 ),例如,使用以下方法的MyAwaitableTaskMethodBuilder <T> :
- 静态Create()
- 开始(stateMachine)
- SetResult(结果)
- SetException(异常)
- SetStateMachine(stateMachine)
- AwaitOnCompleted(awaiter,stateMachine)
- AwaitUnsafeOnCompleted(等待者,stateMachine)
- 工作任务
这是MyAwaitable和MyAwaitableTaskMethodBuilder的简单实现 [AsyncMethodBuilder(typeof(MyAwaitableTaskMethodBuilder<>))] public class MyAwaitable<T> : INotifyCompletion { private Action _continuation; public MyAwaitable() { } public MyAwaitable(T value) { this.Value = value; this.IsCompleted = true; } public MyAwaitable<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public T Value { get; private set; } public Exception Exception { get; private set; } public T GetResult() { if (!this.IsCompleted) throw new Exception("Not completed"); if (this.Exception != null) { ExceptionDispatchInfo.Throw(this.Exception); } return this.Value; } internal void SetResult(T value) { if (this.IsCompleted) throw new Exception("Already completed"); this.Value = value; this.IsCompleted = true; this._continuation?.Invoke(); } internal void SetException(Exception exception) { this.IsCompleted = true; this.Exception = exception; } void INotifyCompletion.OnCompleted(Action continuation) { this._continuation = continuation; if (this.IsCompleted) { continuation(); } } } public class MyAwaitableTaskMethodBuilder<T> { public MyAwaitableTaskMethodBuilder() => this.Task = new MyAwaitable<T>(); public static MyAwaitableTaskMethodBuilder<T> Create() => new MyAwaitableTaskMethodBuilder<T>(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); public void SetStateMachine(IAsyncStateMachine stateMachine) { } public void SetException(Exception exception) => this.Task.SetException(exception); public void SetResult(T result) => this.Task.SetResult(result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => awaiter.OnCompleted(stateMachine.MoveNext); public MyAwaitable<T> Task { get; } }
现在我们可以将MyAwaitable用作异步方法的结果类型:
private async MyAwaitable<int> MyAwaitableMethod() { int result = 0; int arg1 = await this.GetMyAwaitable(1); result += arg1; int arg2 = await this.GetMyAwaitable(2); result += arg2; int arg3 = await this.GetMyAwaitable(3); result += arg3; return result; } private async MyAwaitable<int> GetMyAwaitable(int arg) { await Task.Delay(1);
该代码可以正常工作,但是要了解MyAwaitable需求的目的,让我们看一下C#预处理程序对MyAwaitableMethod所做的工作 。 如果您运行一些反编译工具(例如dotPeek),则会看到原始方法已更改如下:
private MyAwaitable<int> MyAwaitableMethod() { var stateMachine = new MyAwaitableMethodStateMachine(); stateMachine.Owner = this; stateMachine.Builder = MyAwaitableTaskMethodBuilder<int>.Create(); stateMachine.State = 0; stateMachine.Builder.Start(ref stateMachine); return stateMachine.Builder.Task; }
MyAwaitableMethodStateMachine实际上,这是一个简化的代码,在这里我省略了很多优化以使编译器生成的代码可读
sealed class MyAwaitableMethodStateMachine : IAsyncStateMachine { public int State; public MyAwaitableTaskMethodBuilder<int> Builder; public BuilderDemo Owner; private int _result; private int _arg1; private int _arg2; private int _arg3; private MyAwaitableAwaiter<int> _awaiter1; private MyAwaitableAwaiter<int> _awaiter2; private MyAwaitableAwaiter<int> _awaiter3; private void SetAwaitCompletion(INotifyCompletion awaiter) { var stateMachine = this; this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine); } void IAsyncStateMachine.MoveNext() { int finalResult; try { label_begin: switch (this.State) { case 0: this._result = 0; this._awaiter1 = this.Owner.GetMyAwaitable(1).GetAwaiter(); this.State = 1; if (!this._awaiter1.IsCompleted) { this.SetAwaitCompletion(this._awaiter1); return; } goto label_begin; case 1:
查看生成的代码,我们可以看到“方法生成器”具有以下职责:
- 在完成子异步操作后安排状态机的MoveNext()方法调用(在最简单的情况下,我们只需将MoveNext()传递到异步操作等待者的OnCompleted()中)。
- 创建异步操作上下文对象(
public MyAwaitable<T> Task { get; }
) - 对生成的状态机的最终状态做出反应: SetResult或SetException 。
换句话说,通过“方法构建器”,我们可以控制异步方法的执行方式,并且它看起来像一项有助于我们实现目标的功能-Maybe monad行为的实现。 但是那个单子有什么好处呢? 好吧...您可以在Internet上找到很多有关该monad的文章,因此在这里我仅介绍基本知识。
也许单子
简而言之, 也许 monad是一种设计模式,如果该函数调用链中的某些函数无法产生有价值的结果(例如,解析错误),则该模式允许中断该函数调用链。
从历史上看,命令式编程语言一直通过两种方式解决该问题:
- 很多条件逻辑
- 例外情况
两种方法都有明显的缺点,因此发明了第三种方法:
- 创建一个可以有2种状态的类型:“某些值”和“无”-我们称其为“可能”
- 创建一个函数(让我们将其称为“ SelectMany”)以检索2个参数:
2.1。 “也许”类型的对象
2.2。 调用集中的下一个函数-该函数还应该返回一个“ Maybe”对象,如果无法评估其结果(例如,函数参数的格式不正确),则该对象将包含结果或“ Nothing” - “ SelectMany”函数检查“ Maybe”是否具有某个值,然后使用该值(从“ Maybe”中提取)作为参数调用下一个函数,然后返回其结果,否则返回“ Nothing”状态的“ Maybe”对象。

在C#中,可以这样实现:
public struct Maybe<T> { public static implicit operator Maybe<T>(T value) => Value(value); public static Maybe<T> Value(T value) => new Maybe<T>(false, value); public static readonly Maybe<T> Nothing = new Maybe<T>(true, default); private Maybe(bool isNothing, T value) { this.IsNothing = isNothing; this._value = value; } public readonly bool IsNothing; private readonly T _value; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } public static class MaybeExtensions { public static Maybe<TRes> SelectMany<TIn, TRes>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func) => source.IsNothing ? Maybe<TRes>.Nothing : func(source.GetValue()); }
和用法:
static void Main() { for (int i = 0; i < 10; i++) { var res = Function1(i).SelectMany(Function2).SelectMany(Function3); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Maybe<int> Function1(int acc) => acc < 10 ? acc + 1 : Maybe<int>.Nothing; Maybe<int> Function2(int acc) => acc < 10 ? acc + 2 : Maybe<int>.Nothing; Maybe<int> Function3(int acc) => acc < 10 ? acc + 3 : Maybe<int>.Nothing; }
为什么选择“ SelectMany”?我认为有些人可能会问一个问题:“作者为什么要调用函数“ SelectMany”?这真是个奇怪的名字”。 是的,但这是有原因的。 在C#中,预处理器使用SelectMany支持查询表示法 ,从而简化了调用链的使用(您可以在上一篇文章中找到更多详细信息 )。
实际上,我们可以如下更改呼叫链:
var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4)));
这样我们就可以访问所有中间结果,这很方便,但是代码很难阅读。
这里的查询符号可以帮助我们:
var res = from x2 in Function1(i) from x3 in Function2(x2) from x4 in Function3(x3) select x2 + x3 + x4;
为了使代码可编译,我们需要增强的“选择很多”版本
public static Maybe<TJ> SelectMany<TIn, TRes, TJ>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func, Func<TIn, TRes, TJ> joinFunc) { if (source.IsNothing) return Maybe<TJ>.Nothing; var res = func(source.GetValue()); return res.IsNothing ? Maybe<TJ>.Nothing : joinFunc(source.GetValue(), res.GetValue()); }
让我们使用这种经典的“也许”实现从文章标题中实现程序 static void Main() { foreach (var s in new[] {"1,2", "3,7,1", null, "1"}) { var res = Sum(s); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } static Maybe<int> Sum(string input) => Split(input).SelectMany(items => Acc(0, 0, items));
由于C#不是作为一种功能语言设计的,所以代码看起来不太好,但是在像Haskell这样的“真正”功能语言中,这种方法非常普遍
异步也许
Maybe monad的本质是控制函数调用链,但这正是“异步/等待”的作用。 因此,让我们尝试将它们组合在一起。 首先,我们需要使Maybe类型与异步函数兼容,并且我们已经知道如何做到这一点:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : INotifyCompletion { ... public Maybe<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public void OnCompleted(Action continuation){...} public T GetResult() =>... }
现在,让我们看一下如何将“经典的Maybe”重写为状态机,以发现任何相似之处:
static void Main() { for (int i = 0; i < 10; i++) { var stateMachine = new StateMachine(); stateMachine.state = 0; stateMachine.i = i; stateMachine.MoveNext(); var res = stateMachine.Result; Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } class StateMachine { public int state = 0; public int i; public Maybe<int> Result; private Maybe<int> _f1; private Maybe<int> _f2; private Maybe<int> _f3; public void MoveNext() { label_begin: switch (this.state) { case 0: this._f1 = Function1(this.i); this.state = Match ? -1 : 1; goto label_begin; case 1: this._f2 = Function2(this._f1.GetValue()); this.state = this._f2.IsNothing ? -1 : 2; goto label_begin; case 2: this._f3 = Function3(this._f2.GetValue()); this.state = this._f3.IsNothing ? -1 : 3; goto label_begin; case 3: this.Result = this._f3.GetValue(); break; case -1: this.Result = Maybe<int>.Nothing; break; } } }
如果我们将此状态机与C#预处理程序之一生成的状态机匹配(请参见上文-'MyAwaitableMethodStateMachine'),我们会注意到也许可以在内部实现状态检查:
this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine);
其中ref awaiter
是Maybe类型的对象。 唯一的问题是我们无法将机器设置为“最终”(-1)状态。 这是否意味着我们无法控制执行流程? 实际上,事实并非如此。 事实是,对于每个异步动作,C#都会通过INotifyCompletion接口设置一个回调动作,因此,如果我们想中断执行流程,我们可以在无法继续执行流程的情况下调用该回调动作。
这里的另一个挑战是生成的状态机传递当前流的下一个动作(作为继续回调),但是我们需要初始流的继续回调,这将允许绕过其余的异步操作:

因此,我们需要以某种方式将子异步操作与其祖先联系起来。 我们可以使用我们的“方法生成器”来做到这一点,该方法链接到当前的异步操作-Task 。 所有子异步操作的链接都将传递给AwaitOnCompleted(ref awaiter
称为awaiter ,所以我们只需要检查该参数是否为Maybe的实例,然后将当前Maybe设置为该子代的父代即可:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private IMaybe _parent; void IMaybe.SetParent(IMaybe parent) => this._parent = parent; ... } public class MaybeTaskMethodBuilder<T> { ... private void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IMaybe maybe) { maybe.SetParent(this.Task); } awaiter.OnCompleted(stateMachine.MoveNext); } ... }
现在,所有Maybe对象都可以连接到树中,因此,我们可以从任何后代节点访问根Maybe的延续( Exit方法):
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private Action _continuation; private IMaybe _parent; ... public void OnCompleted(Action continuation) { ... this._continuation = continuation; ... } ... void IMaybe.Exit() { this.IsCompleted = true; if (this._parent != null) { this._parent.Exit(); } else { this._continuation(); } } ... }
当(在移动树上时)我们发现一个已经解析的Maybe对象处于Nothing状态时,应调用Exit方法。 这样的Maybe对象可以通过如下方法返回:
Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing();
为了存储已解决状态, 也许我们引入一个新的单独结构:
public struct MaybeResult { ... private readonly T _value; public readonly bool IsNothing; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } [AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; ... internal Maybe() { }
当异步状态机(通过方法构建器)调用已解析的Maybe实例的OnCompleted方法并且它处于Nothing状态时,我们将能够中断整个流程:
public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result)
现在唯一剩下的就是-如何获得可能超出其范围的异步Maybe结果(任何返回类型不是Maybe的异步方法)。 如果您尝试将Maya实例仅使用await关键字,则由于以下代码将引发异常:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; public T GetResult() => this._result.Value.GetValue(); } ... public struct MaybeResult { ... public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; }
为了解决这个问题,我们只需要添加一个新的Awaiter即可返回完整的MaybeResult结构,我们将可以编写如下代码:
var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... };
现在就这些了。 在代码示例中,我忽略了一些细节,仅将重点放在最重要的部分上。 您可以在github上找到完整版本。
但是 ,我不建议在任何生产代码中使用此版本,因为它存在严重的问题-当我们通过调用root的延续来制动执行流时, 也许我们将绕过一切! 包括所有的finally块(这是对“是否总是调用finally块?”这个问题的回答),因此所有使用运算符的操作将无法按预期工作,并且可能导致资源泄漏。 如果我们不是抛出初始的继续回调,而是抛出一个内部处理的特殊异常( 可以在此处找到版本 ),则可以解决此问题,但是此解决方案显然具有性能上的模仿(在某些情况下可以接受)。 使用当前版本的C#编译器,我看不到任何其他解决方案,但将来可能会更改。
这些限制并不意味着本文中描述的所有技巧都是完全没有用的,它们可用于实现不需要更改执行流程的其他monad,例如“ Reader”。 我将在下一篇文章中介绍如何通过异步/等待来实现“阅读器” monad。