
在上一篇文章中,我描述了如何使用async / await语句实现Maybe Monad模式。 这次,我将告诉您如何使用相同的技术来实现另一个流行的设计模板“ Monad Reader”。
该模板允许您在不使用参数或类字段的情况下,将特定上下文隐式传输到函数调用的层次结构,并且可以将其视为实现依赖注入的另一种方法。 例如:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
经典的“读者”
首先,让我们看看我们如何能够实现无需运营商异步/ AWAIT这种模式:
public class Config { public string Template; } public static class ClassicReader { public static void Main() { var greeter = GreetGuys(); Console.WriteLine(greeter.Apply(new Config{Template = "Hello, {0}"}));
(读者)
public class Reader<T, TCtx> { private readonly Func<TCtx, T> _exec; public Reader(Func<TCtx, T> exec) => this._exec = exec; public T Apply(TCtx ctx) => this._exec(ctx); } public static class Reader { public static Reader<TJoin, TCtx> SelectMany<TIn, TOut, TCtx, TJoin>( this Reader<TIn, TCtx> source, Func<TIn, Reader<TOut, TCtx>> bind, Func<TIn, TOut, TJoin> join) => new Reader<TJoin, TCtx>(ctx => { var inValue = source.Apply(ctx); var outValue = bind(inValue).Apply(ctx); return join(inValue, outValue); }); }
该代码有效,但是不使用查询语法(本身并不总是很方便),其可读性急剧下降,这并不奇怪,因为monad来自功能语言,这些语言看起来自然且可读性很好(尽管在Haskelel中也提出了“ do”)符号以提高可读性)。 但是,经典实现有助于理解模式的本质-而不是立即执行一些代码,而是将其放在仅在获得其上下文时才被调用的函数中。
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany可以将其中一些功能捆绑在一起,因此您可以创建一个完整的例程,该例程将延迟到应用其上下文之前。 另一方面,这种方法类似于编写异步代码,其中,如果需要某些异步操作的结果,则将停止程序执行。 操作结果准备好后,程序将继续。 有一个假设,旨在实现异步操作( async / await )的C#基础结构可以在实现“阅读器” monad时以某种方式使用,并且……这个假设是正确的! 如果函数需要访问上下文,则可以“暂停”其执行,直到在外部指定此上下文为止。
异步阅读器
在上一篇文章中,我展示了如何使用通用异步返回类型获得对异步/等待语句的控制。 这次将使用相同的方法。 让我们从Reader类开始,它将被用作异步操作的结果类型:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
此类有两个任务(理论上,我们可以创建两个不同的类):
- 从上下文中检索值。
- 创建Reader类的实例的链接列表,该列表将用于在整个调用层次结构中分发上下文。
对于这些任务,我们将创建一个单独的构造函数:
private readonly Func<object, T> _extractor;
当Reader类的实例用作await运算符的参数时,该实例将获得对将继续程序的委托的引用。 在从程序继续执行所必需的数据提取中接收到上下文之后,应调用该委托。

要链接Reader类的实例,让我们创建SetChild方法:
private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } }
这将在ReaderTaskMethodBuilder内部调用 :
public class ReaderTaskMethodBuilder<T> { ... public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IReader reader) { this.Task.SetChild(reader); } awaiter.OnCompleted(stateMachine.MoveNext); } public Reader<T> Task { get; } }
在SetChild方法内部,我们调用SetCtx函数在调用层次结构中扩展上下文。 如果在这个级别的层次SetCtx呼叫被赋予功能_ 提取 (第一klsssa 阅读器的构造函数),直接提取数据断章取义,但现在它可以被调用,以获取必要的数据和完成当前的异步操作chrerez挑战的setResult:
public void SetCtx(object ctx) { this._ctx = ctx; if (this._ctx != null) { this._child?.SetCtx(this._ctx); if (this._extractor != null) { this.SetResult(this._extractor(this._ctx)); } } }
SetResult存储从上下文中检索到的值,并调用委托以继续执行程序:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
如果Reader类实例没有启动_ 提取器函数( Reader类的第二个构造函数),则当生成的状态机进入最终状态时, ReaderTaskMethodBuilder应调用SetResult 。
SetCtx也可以在Apply方法中调用,以在层次结构的根节点中设置上下文:
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
完整的GitHub代码
现在,您可以看一下使用异步Reader -a的更实际的示例:
单击以展开示例。 public static class ReaderTest { public class Configuration { public readonly int DataBaseId; public readonly string GreetingTemplate; public readonly string NameFormat; public Configuration(int dataBaseId, string greetingTemplate, string nameFormat) { this.DataBaseId = dataBaseId; this.GreetingTemplate = greetingTemplate; this.NameFormat = nameFormat; } } public static async Task Main() { int[] ids = { 1, 2, 3 }; Configuration[] configurations = { new Configuration(100, "Congratulations, {0}! You won {1}$!", "{0} {1}"), new Configuration(100, "¡Felicidades, {0}! Ganaste {1} $", "{0}"), }; foreach (var configuration in configurations) { foreach (var userId in ids) {
该程序向某些用户显示问候语,但我们事先不知道他们的名字,因为我们只有他们的标识符,因此我们需要从“数据库”中读取他们的名字。 要连接到此“数据库”,您需要知道连接参数,并生成问候语,我们还需要这些问候语的模板。 所有这些信息都通过异步Reader隐式传递。
通过异步“阅读器”进行依赖注入
与经典实现相比,异步Reader具有一个缺点-我们无法指定要传输的上下文的类型。 此限制来自以下事实:C#编译器在ReaderTaskMethodBuilder类中仅允许使用一种参数化的数据类型(通用类型)(在将来的版本中可能已修复)。
另一方面,我认为这不是至关重要的,因为在现实生活中,最有可能将依赖注入的容器作为上下文传递:
public static class Reader { public static Reader<TService> GetService<TService>() => Reader<TService>.Read<IServiceProvider>(serviceProvider => (TService)serviceProvider .GetService(typeof(TService))); }
... private static async Reader<string> Greet(string userName) { var service = await Reader.GetService<IGreater>(); return service.GreetUser(userName); } ...
( 在这里您可以找到完整版本... )
与我不建议在任何工业代码中不建议使用异步“ Maybe”不同,我会考虑在某些实际项目中使用异步Reader -a来替代(或添加)传统的依赖项注入机制(当所有依赖项都作为构造函数参数传递时) 读者因为-a具有许多优点:
- 不需要其类存储指向嵌入式资源的链接的字段。 实际上,根本不需要真正的类,因为所有逻辑都可以在静态方法中实现。
- 使用Reader -a会导致编写非阻塞代码,因为所有方法都是异步的,并且不会干扰库函数的异步版本的使用。
- 该代码更具可读性,因为每次我们将Reader作为某种方法的返回类型时,我们都会知道它需要访问某些隐式上下文
- 异步阅读器不使用反射。
当然,使用此Reader -a可能会有异议,但是无论如何,这些文章的主要任务是展示如何为命令性编程语言改编最初为功能语言创建的模板,这被大多数人认为更易于理解。 。