在上一篇文章中,我描述了如何使用异步/等待操作符实现“也许”单子行为。 这次,我将展示如何使用相同的技术来实现另一个流行的设计模式“ Reader Monad”。
该模式允许在不使用函数参数或共享全局对象的情况下将某些上下文隐式传递到某个函数中,并且可以将其视为实现依赖项注入的另一种方法。 例如:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
经典的“读者”
首先,让我们看一下如何在没有异步/等待操作符的情况下实现monad:
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); }); }
该代码可以工作,但对于C#开发人员来说似乎并不自然。 难怪,因为monad来自功能语言,可以用更简洁的方式编写类似的代码。 但是,经典实现有助于低估模式的本质-而不是立即执行某些代码,而是将其放入一个函数中,该函数在其上下文就绪时将被调用。
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany可以将多个这样的功能组合为一个功能,因此您可以创建一个子例程,该子例程的执行将推迟到应用其上下文之前。 另一方面,该方法类似于编写异步代码,其中如果正在运行某些异步操作,则程序执行将停止。 操作结果准备就绪后,程序将继续执行。 出现这样一种假设,即设计用于异步操作( async / await )的C#基础结构可以在实现“ Reader” monad的过程中以某种方式利用,并且...这种假设是正确的!
异步“阅读器”
在上一篇文章中,我演示了如何使用通用异步返回类型来控制异步/等待操作符。 这次将使用相同的技术。 让我们从“ Reader”类开始,该类将用作异步操作的结果类型:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
这个类有两个不同的职责(理论上我们可以创建两个类):
- 应用上下文时从上下文中提取一些值
- 创建Reader实例的链接列表,该列表将用于在调用层次结构上分发上下文。
对于每种职责,我们将使用单独的构造函数:
private readonly Func<object, T> _extractor;
当Reader类的实例用作await运算符的参数时,该实例将收到指向延续委托的链接,只有在解析执行上下文时,才可以调用该链接,并且我们可以从上下文中提取一些数据,在延续中使用。

要在父级和子级“阅读器”之间创建连接,请创建方法:
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 ,它将上下文传播到层次结构并调用提取器(如果已定义):
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实例没有初始化的“提取程序”,则当生成的状态机进入其最终状态时,应该由ReaderTaskMethodBuilder调用SetResult 。
Apply方法仅调用SetCtx
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
您可以在github上找到所有代码( 如果仍然没有被阻止 )
现在,我想展示一个更实际的示例,说明如何使用异步阅读器 :
单击以展开示例 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) {
该程序向某些用户显示问候语,但是由于我们只有他们的ID,所以我们不事先知道他们的名字,因此我们需要从“数据库”中读取该信息。 要连接到数据库,我们需要知道一些连接标识符,并创建一个问候,我们需要它的模板,并且...所有信息都通过异步Reader隐式传递。
异步“阅读器”的依赖注入
与经典实现相比,异步阅读器有一个缺陷-我们无法指定传递上下文的类型。 此限制来自以下事实:C#编译器仅期望异步方法构建器类中的单个泛型类型参数(也许将来会修复)。
另一方面,我认为这不是至关重要的,因为在现实生活中,很可能会将某些依赖项注入容器作为上下文传递:
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” (由于finally块存在问题)不同,我会考虑在实际项目中使用异步阅读器来代替(或“除”)传统的依赖项注入方法(当所有依赖项都传递到类构造函数中时),因为Reader具有一些优点:
- 在类属性中不需要存储到注入资源的链接。 实际上,根本不需要类-所有逻辑都可以在静态方法中实现。
- 使用Reader会鼓励创建非阻塞代码,因为所有方法都是异步的,并且没有任何东西会阻止开发人员使用非阻塞版本的输入/输出操作。
- 代码会更具“可读性”-每次我们将Reader作为某种方法的结果类型时,我们都会知道它需要访问某些隐式上下文。
- 异步阅读器不使用反射。
当然,可能会有一些反对使用Reader的争论,但是无论如何,这些文章的主要目的是展示最初为功能语言设计的模式如何可以被用于命令式的编码,该编码被认为更容易实现。被大多数人理解。