
In meinem vorherigen Artikel habe ich beschrieben, wie das Maybe Monad-Muster mithilfe von async / await- Anweisungen implementiert wird. Dieses Mal werde ich Ihnen erklären, wie Sie ein anderes beliebtes Entwurfsmuster, "Monad Reader", mit denselben Tricks implementieren können.
Mit dieser Vorlage können Sie einen bestimmten Kontext implizit in die Hierarchie von Funktionsaufrufen übertragen, ohne Parameter oder Klassenfelder zu verwenden, und sie kann als eine andere Möglichkeit zum Implementieren der Abhängigkeitsinjektion betrachtet werden. Zum Beispiel:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
Klassischer "Reader"
Lassen Sie uns zunächst sehen, wie Sie dieses Muster ohne asynchrone / await- Anweisungen implementieren können:
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}"}));
(Leser)
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); }); }
Dieser Code funktioniert, aber ohne die Abfragesyntax zu verwenden (was an sich nicht immer praktisch ist), sinkt seine Lesbarkeit dramatisch und dies ist nicht überraschend, da Monaden aus funktionalen Sprachen stammen, in denen ein solcher Code natürlich aussieht und gut liest (obwohl selbst in Haskelel ein "do" gefunden wurde) Notation zur Verbesserung der Lesbarkeit). Die klassische Implementierung hilft jedoch dabei, die Essenz des Musters zu verstehen. Anstatt Code sofort auszuführen, wird er in eine Funktion eingefügt, die nur aufgerufen wird, wenn er seinen Kontext erhält.
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany kann mehrere dieser Funktionen in einer bündeln, sodass Sie eine gesamte Routine erstellen können, die verzögert wird, bis ihr Kontext angewendet wird. Andererseits ähnelt dieser Ansatz dem Schreiben von asynchronem Code, bei dem die Programmausführung gestoppt wird, wenn das Ergebnis einer asynchronen Operation erforderlich ist. Wenn das Ergebnis der Operation fertig ist, wird das Programm fortgesetzt. Es besteht die Annahme, dass die C # -Infrastruktur, die für asynchrone Operationen ( async / await ) ausgelegt ist, bei der Implementierung der "Reader" -Monade auf irgendeine Weise verwendet werden kann, und ... diese Annahme ist wahr! Wenn Funktionen Zugriff auf den Kontext erfordern, kann die Ausführung angehalten werden, bis dieser Kontext extern angegeben wird.
Asynchroner Leser
In meinem vorherigen Artikel habe ich gezeigt, wie Sie mithilfe generischer asynchroner Rückgabetypen die Kontrolle über asynchrone / warten- Anweisungen erlangen. Diesmal wird der gleiche Ansatz verwendet. Beginnen wir mit der Reader- Klasse, die als Ergebnistyp für asynchrone Operationen verwendet wird:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
Diese Klasse hat zwei Aufgaben (theoretisch könnten wir zwei verschiedene Klassen erstellen):
- Werte aus dem Kontext abrufen.
- Erstellen Sie eine verknüpfte Liste von Instanzen der Reader- Klasse, mit der der Kontext in der Aufrufhierarchie verteilt wird.
Für jede dieser Aufgaben erstellen wir einen separaten Konstruktor:
private readonly Func<object, T> _extractor;
Wenn eine Instanz der Reader- Klasse als Argument für den Operator await verwendet wird, erhält diese Instanz einen Verweis auf den Delegaten, der das Programm fortsetzt. Dieser Delegat sollte aufgerufen werden, nachdem er den Kontext aus der Extraktion der Daten erhalten hat, die für die Fortsetzung des Programms erforderlich sind.

Um Instanzen der Reader- Klasse zu verknüpfen, erstellen wir die SetChild- Methode:
private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } }
welches in ReaderTaskMethodBuilder aufgerufen wird :
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; } }
Innerhalb der SetChild- Methode rufen wir die SetCtx- Funktion auf, um den Kontext über die Aufrufhierarchie zu verteilen. Wenn beim Aufrufen von SetCtx auf dieser Hierarchieebene die Funktion _ extractor (der erste Konstruktor der Reader- Klasse) angegeben wurde, die Daten direkt aus dem Kontext extrahiert, können Sie sie jetzt aufrufen, die erforderlichen Daten abrufen und die aktuelle asynchrone Operation durch einen Aufruf von SetResult abschließen :
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 speichert den aus dem Kontext abgerufenen Wert und ruft den Delegaten auf, um das Programm fortzusetzen:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
Wenn für die Instanz der Reader- Klasse die Extraktorfunktion _ nicht initiiert ist (der zweite Konstruktor der Reader- Klasse), sollte SetResult von ReaderTaskMethodBuilder aufgerufen werden, wenn die generierte Zustandsmaschine in den Endzustand wechselt.
SetCtx wird auch in der Apply- Methode aufgerufen, um den Kontext im Stammknoten der Hierarchie festzulegen :
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
Voller GitHub-Code
Jetzt können Sie sich ein realistischeres Beispiel für die Verwendung des asynchronen Readers -a ansehen:
Klicken Sie hier, um das Beispiel zu erweitern. 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) {
Das Programm zeigt Grüße für einige Benutzer an, aber wir kennen ihre Namen nicht im Voraus, da wir nur ihre Kennungen haben. Daher müssen wir ihre Namen aus der "Datenbank" lesen. Um eine Verbindung zu dieser "Datenbank" herzustellen, müssen Sie die Verbindungsparameter kennen und um Begrüßungen zu generieren, benötigen wir auch Vorlagen für diese Begrüßungen. Alle diese Informationen werden implizit über den asynchronen Reader übertragen .
Abhängigkeitsinjektion über asynchronen "Reader"
Im Vergleich zur klassischen Implementierung hat der asynchrone Reader einen Nachteil: Wir können den Typ des zu übertragenden Kontexts nicht angeben. Diese Einschränkung ergibt sich aus der Tatsache, dass der C # -Compiler nur einen parametrisierten Datentyp (generischen Typ) in der ReaderTaskMethodBuilder- Klasse zulässt (dies kann in zukünftigen Versionen behoben sein).
Andererseits halte ich dies nicht für kritisch, da im wirklichen Leben ein Container mit Abhängigkeitsinjektion höchstwahrscheinlich als Kontext übergeben wird:
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); } ...
( Hier finden Sie die Vollversion ... )
Im Gegensatz zum asynchronen „Vielleicht“ , das ich in keinem Industriecode empfohlen habe, würde ich den asynchronen Reader -a in einigen realen Projekten als Ersatz (oder Ergänzung) für herkömmliche Abhängigkeitsinjektionsmechanismen verwenden (wenn alle Abhängigkeiten als Konstruktorparameter übergeben werden). da Reader -a eine Reihe von Vorteilen hat:
- Es sind keine Felder erforderlich, deren Klassen Links zu eingebetteten Ressourcen speichern. Tatsächlich werden überhaupt keine realen Klassen benötigt, da die gesamte Logik in statischen Methoden implementiert werden kann.
- Die Verwendung von Reader -a führt zum Schreiben von nicht blockierendem Code, da alle Methoden asynchron sind und die Verwendung asynchroner Versionen von Bibliotheksfunktionen durch nichts beeinträchtigt wird.
- Der Code ist etwas lesbarer, da wir jedes Mal, wenn wir Reader als Rückgabetyp einer Methode sehen, wissen, dass er Zugriff auf einen impliziten Kontext erfordert
- Der asynchrone Reader verwendet keine Reflexion.
Natürlich kann es Einwände gegen die Verwendung dieses Readers -a geben, aber in jedem Fall besteht die Hauptaufgabe dieser Artikel darin, zu zeigen, wie Vorlagen, die ursprünglich für funktionale Sprachen erstellt wurden, für einen zwingenden Programmierstil angepasst werden können, der von den meisten Menschen als leichter verständlich angesehen wird .