Die Vielleicht-Monade über Async / Warten in C # (ohne Aufgabe ov!)


Generische asynchrone Rückgabetypen sind eine neue in C # 7 eingeführte Funktion, mit der Sie nicht nur Task als Rückgabetyp für asynchrone (asynchrone / wartende ) Methoden verwenden können, sondern auch alle anderen Typen (Klassen oder Strukturen), die bestimmte Anforderungen erfüllen.


Gleichzeitig ist async / await eine Möglichkeit, einen bestimmten Satz von Funktionen in einem bestimmten Kontext nacheinander aufzurufen, was die Essenz des Monad- Entwurfsmusters darstellt. Die Frage ist, können wir async / await verwenden , um Code zu schreiben, der sich so verhält, als würden wir Monaden verwenden? Es stellt sich heraus, dass ja (mit einigen Vorbehalten). Der folgende Code kompiliert und funktioniert beispielsweise:


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()); } // 3, 11, Nothing, Nothing } async Maybe<int> Sum(string input) { var args = await Split(input);//   var result = 0; foreach (var arg in args) result += await Parse(arg);//   return result; } Maybe<string[]> Split(string str) { var parts = str?.Split(',').Where(s=>!string.IsNullOrWhiteSpace(s)).ToArray(); return parts == null || parts.Length < 2 ? Maybe<string[]>.Nothing() : parts; } Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing(); 

Als nächstes erkläre ich, wie dieser Code funktioniert ...


Generische asynchrone Rückgabetypen


Lassen Sie uns zunächst herausfinden, was erforderlich ist, um unseren eigenen Typ (z. B. die MyAwaitable <T> -Klasse) als Ergebnistyp einer asynchronen Funktion zu verwenden. Die Dokumentation besagt, dass dieser Typ Folgendes haben sollte:


  1. GetAwaiter () -Methode, die ein Objekt vom Typ zurückgibt, das die INotifyCompletion- Schnittstelle implementiert, und außerdem die Eigenschaft bool IsCompleted und die T- Methode GetResult () enthält ;


  2. [AsyncMethodBuilder (Type)] - Ein Attribut, das den Typ angibt, der als " Method Builder " fungiert, z. B. MyAwaitableTaskMethodBuilder <T> . Dieser Typ sollte folgende Methoden enthalten:


    • statisch Create ()
    • Start (stateMachine)
    • SetResult (Ergebnis)
    • SetException (Ausnahme)
    • SetStateMachine (stateMachine)
    • AwaitOnCompleted (Kellner, stateMachine)
    • AwaitUnsafeOnCompleted (Kellner, stateMachine)
    • Aufgabe


Ein Beispiel für eine einfache Implementierung von MyAwaitable und 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; } } 

Jetzt können wir MyAwaitable als Ergebnistyp für asynchrone Methoden verwenden:


 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);//   return await new MyAwaitable<int>(arg); } 

Dieser Code funktioniert, aber um das Wesentliche der Anforderungen für die MyAwaitable- Klasse zu verstehen , sehen wir uns an, was der C # -Vorprozessor mit der MyAwaitableMethod- Methode macht. Wenn Sie einen .NET-Compiler-Dekompiler ausführen (z. B. dotPeek), sehen Sie, dass die ursprüngliche Methode wie folgt geändert wurde:


 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

Dies ist eigentlich vereinfachter Code, bei dem ich viele Optimierungen überspringe, um vom Compiler generierten Code lesbar zu machen


 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:// awaiter1    this._arg1 = this._awaiter1.GetResult(); this._result += this._arg1; this.State = 2; this._awaiter2 = this.Owner.GetMyAwaitable(2).GetAwaiter(); if (!this._awaiter2.IsCompleted) { this.SetAwaitCompletion(this._awaiter2); return; } goto label_begin; case 2:// awaiter2    this._arg2 = this._awaiter2.GetResult(); this._result += this._arg2; this.State = 3; this._awaiter3 = this.Owner.GetMyAwaitable(3).GetAwaiter(); if (!this._awaiter3.IsCompleted) { this.SetAwaitCompletion(this._awaiter3); return; } goto label_begin; case 3:// awaiter3    this._arg3 = this._awaiter3.GetResult(); this._result += this._arg3; finalResult = this._result; break; default: throw new Exception(); } } catch (Exception ex) { this.State = -1; this.Builder.SetException(ex); return; } this.State = -1; this.Builder.SetResult(finalResult); } } 

Nach Prüfung des generierten Codes stellen wir fest, dass der Method Builder die folgenden Verantwortlichkeiten hat:


  1. Organisation eines Aufrufs der MoveNext () -Methode, die die generierte Zustandsmaschine in den nächsten Zustand überträgt.
  2. Erstellen eines Objekts, das den Kontext einer asynchronen Operation darstellt ( public MyAwaitable<T> Task { get; } )
  3. Antworten auf die Übersetzung der generierten Zustandsmaschine in Endzustände: SetResult oder SetException .

Mit anderen Worten, mit Hilfe von Method Builder können wir steuern, wie asynchrone Methoden ausgeführt werden, und dies scheint eine Gelegenheit zu sein, die uns dabei hilft, unser Ziel zu erreichen - die Implementierung von Vielleicht Monadenverhalten.


Aber was ist so gut an dieser Monade? ... Tatsächlich finden Sie im Internet viele Artikel über diese Monade, daher werde ich hier nur die Grundlagen beschreiben.


Vielleicht Monade


Kurz gesagt, Vielleicht ist Monade ein Entwurfsmuster, mit dem Sie die Kette von Funktionsaufrufen unterbrechen können, wenn eine Funktion aus der Kette kein aussagekräftiges Ergebnis zurückgeben kann (z. B. ungültige Eingabeparameter).


Historisch zwingende Programmiersprachen haben dieses Problem auf zwei Arten gelöst:


  1. Viel bedingte Logik
  2. Ausnahmen

Beide Methoden haben offensichtliche Nachteile, daher wurde ein alternativer Ansatz vorgeschlagen:


  1. Erstellen Sie einen Typ, der zwei Zustände haben kann: "Ein Wert" und "Kein Wert" (" Nichts ") - nennen wir ihn Vielleicht
  2. Erstellen Sie eine Funktion (nennen wir sie SelectMany ), die zwei Argumente akzeptiert :
    2.1. Vielleicht Objekt
    2.2. Die nächste Funktion aus der Anrufliste. Diese Funktion sollte auch ein Objekt vom Typ " Vielleicht" zurückgeben , das möglicherweise einen resultierenden Wert enthält oder sich im Status " Nichts" befindet, wenn das Ergebnis nicht abgerufen werden kann (z. B. wurden falsche Parameter an die Funktion übergeben).
  3. Die SelectMany- Funktion überprüft ein Objekt vom Typ Vielleicht. Wenn es den resultierenden Wert enthält, wird dieses Ergebnis extrahiert und als Argument an die nächste Funktion aus der Aufrufkette übergeben (als zweites Argument übergeben). Befindet sich das Vielleicht- Objekt im Status " Nichts" , gibt SelectMany sofort " Nichts" zurück .


In C # kann dies wie folgt implementiert werden:


 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()); } 

und Anwendungsbeispiel:


 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; } 

Warum ist "SelectMany"?

Ich denke, einige von Ihnen fragen sich vielleicht: „Warum hat der Autor diese Funktion„ SelectMany “genannt? Eigentlich gibt es einen Grund dafür - in C # fügt der Präprozessor einen Select Many- Aufruf ein, wenn er in Query Notation geschriebene Ausdrücke verarbeitet, was im Wesentlichen der Fall ist "Syntaktischer Zucker" für komplexe Anrufketten (Weitere Informationen hierzu finden Sie in meinem vorherigen Artikel ).


Tatsächlich können wir den vorherigen Code wie folgt umschreiben:


 var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4))); 

Dadurch erhalten Sie Zugriff auf den Zwischenzustand (x2, x3), was in einigen Fällen sehr praktisch sein kann. Leider ist das Lesen eines solchen Codes sehr schwierig, aber glücklicherweise verfügt C # über eine Abfrage-Notation, mit deren Hilfe ein solcher Code viel einfacher aussehen wird:


 var res = from x2 in Function1(i) from x3 in Function2(x2) from x4 in Function3(x3) select x2 + x3 + x4; 

Um diesen Code kompilieren zu können, müssen wir die Funktion " Viele auswählen" leicht erweitern:


 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()); } 

So sieht der Code aus dem Titel des Artikels aus, wenn Sie ihn mit der "klassischen" Vielleicht-Implementierung neu schreiben
 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)); //       "Maybe" static Maybe<int> Acc(int res, int index, IReadOnlyList<string> array) => index < array.Count ? Add(res, array[index]) .SelectMany(newRes => Acc(newRes, index + 1, array)) : res; static Maybe<int> Add(int acc, string nextStr) => Parse(nextStr).SelectMany<int, int>(nextNum => acc + nextNum); static Maybe<string[]> Split(string str) { var parts = str?.Split(',') .Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); return parts == null || parts.Length < 2 ? Maybe<string[]>.Nothing : parts; } static Maybe<int> Parse(string value) => int.TryParse(value, out var result) ? result : Maybe<int>.Nothing; 

Dieser Code sieht nicht sehr elegant aus, da C # ursprünglich nicht als funktionale Sprache entworfen wurde, aber dieser Ansatz ist in "echten" funktionalen Sprachen weit verbreitet.


Async vielleicht


Die Essenz von Vielleicht Monade besteht darin, die Kette von Funktionsaufrufen zu steuern, aber genau das macht Async / Warten . Versuchen wir also, sie miteinander zu kombinieren. Zunächst müssen wir den Typ Vielleicht mit asynchronen Funktionen kompatibel machen, und wir wissen bereits, wie dies erreicht werden kann:


 [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() =>... } 

Nun wollen wir sehen, wie die „klassische“ Implementierung möglicherweise als endliche Zustandsmaschine umgeschrieben werden kann, damit wir Ähnlichkeiten finden können:


 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; } } } 

Wenn wir diese Zustandsmaschine mit dem generierten C # -Vorprozessor vergleichen (siehe „MyAwaitableMethodStateMachine“ oben), können wir feststellen, dass die Statusprüfung möglicherweise in folgenden Funktionen implementiert werden kann:


 this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine); 

wo ref awaiter ein Objekt vom Typ Vielleicht ist . Das Problem hierbei ist, dass wir die Maschine nicht in den Status „final“ (-1) versetzen können. Bedeutet dies jedoch, dass wir den Ausführungsfluss nicht steuern können? Dies ist eigentlich nicht der Fall. Tatsache ist, dass C # für jede asynchrone Aktion eine Rückruffunktion einrichtet , um die asynchrone Aktion über die INotifyCompletion- Schnittstelle fortzusetzen. Wenn wir also den Ausführungsfluss unterbrechen möchten, können wir die Rückruffunktion einfach aufrufen, wenn wir die Kette der asynchronen Operationen nicht fortsetzen können.
Ein weiteres Problem besteht darin, dass die generierte Zustandsmaschine den nächsten Schritt (als Rückruffunktion) an die aktuelle Folge von asynchronen Operationen übergibt. Für die ursprüngliche Folge benötigen wir jedoch eine Rückruffunktion, mit der wir alle verbleibenden Ketten von asynchronen Operationen (von jeder Verschachtelungsebene aus) umgehen können. ::



Wir müssen also die aktuell verschachtelte asynchrone Aktion irgendwie mit ihrem Ersteller verknüpfen. Wir können dies mit unserem Method Builder tun, der einen Link zur aktuellen asynchronen Operation - Task hat . Links zu allen AwaitOnCompleted (ref awaiter) asynchronen Vorgängen werden als AwaitOnCompleted (ref awaiter) an AwaitOnCompleted (ref awaiter) Daher müssen wir nur prüfen, ob der Parameter eine Instanz von Maybe ist , und dann das aktuelle Maybe als übergeordnetes Element für die aktuelle untergeordnete Aktion festlegen:


 [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); } ... } 

Jetzt können alle Objekte vom Typ Vielleicht zu einer Hierarchie zusammengefasst werden, wodurch wir von jedem Knoten aus Zugriff auf den letzten Aufruf der gesamten Hierarchie ( Exit- Methode) erhalten:


 [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(); } } ... } 

Die Exit- Methode sollte aufgerufen werden, wenn wir beim Navigieren durch die Hierarchie das bereits berechnete Maybe- Objekt im Status Nothing finden . Solche Vielleicht können Objekte mit folgenden Methoden zurückgegeben werden:


 Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing(); 

Erstellen Sie zum Speichern des Status " Vielleicht" eine neue separate Struktur:


 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() { }//Used in async method private Maybe(MaybeResult result) => this._result = result;// ""  ... } 

In dem Moment, in dem die asynchrone Zustandsmaschine (über Method Builder ) die OnCompleted- Methode der bereits berechneten Vielleicht- Instanz aufruft und sich im Status " Nichts" befindet , können wir den gesamten Stream unterbrechen :


 public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result) //  "method builder"     { this._result = MaybeResult.Value(result); this.IsCompleted = true; this.NotifyResult(this._result.Value.IsNothing); } private void NotifyResult(bool isNothing) { this.IsCompleted = true; if (isNothing) { this._parent.Exit();//    } else { this._continuation?.Invoke(); } } 

Jetzt bleibt nur noch eine Frage: Wie kann das Ergebnis des asynchronen Vielleicht außerhalb seines Gültigkeitsbereichs abgerufen werden (jede asynchrone Methode, deren Rückgabetyp nicht Vielleicht ist ) ? Wenn Sie versuchen, nur das Schlüsselwort await mit der Instanz " Vielleicht" zu verwenden , wird durch diesen Code eine Ausnahme ausgelöst:


 [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; } 

Um dieses Problem zu lösen, können wir einfach einen neuen Kellner hinzufügen, der die gesamte MaybeResult- Struktur als Ganzes zurückgibt , und dann können wir diesen Code schreiben:


 var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... }; 

Das ist alles für jetzt. In den Codebeispielen habe ich einige Details weggelassen, um mich nur auf die wichtigsten Teile zu konzentrieren. Die Vollversion finden Sie auf github .


Tatsächlich würde ich die Verwendung des obigen Ansatzes in keinem Arbeitscode empfehlen, da er ein erhebliches Problem aufweist: Wenn wir den Ausführungsthread unterbrechen und die Fortsetzung der asynchronen Root-Operation (mit dem Typ Vielleicht ) verursachen, brechen wir ALLE überhaupt! Einschließlich aller finally- Blöcke (dies ist die Antwort auf die Frage „Werden finally- Blöcke immer aufgerufen?“), sodass alle using- Anweisungen nicht ordnungsgemäß funktionieren, was zu einem Ressourcenleck führen kann. Dieses Problem kann gelöst werden, indem anstelle eines direkten Aufrufs der Fortsetzung eine spezielle Ausnahme ausgelöst wird, die implizit behandelt wird ( hier finden Sie diese Version ). Diese Lösung weist jedoch offensichtlich eine Leistungsbeschränkung auf (die in einigen Szenarien akzeptabel sein kann). In der aktuellen Version des C # -Compilers sehe ich keine andere Lösung, aber vielleicht wird sich dies eines Tages in der Zukunft ändern.


Diese Einschränkungen bedeuten jedoch nicht, dass alle in diesem Artikel beschriebenen Techniken völlig nutzlos sind. Sie können verwendet werden, um andere Monaden zu implementieren, für die keine Änderungen in den Threads erforderlich sind, z. B. "Reader". Wie man diese "Reader" -Monade durch async / await implementiert, werde ich im nächsten Artikel zeigen .

Source: https://habr.com/ru/post/de468017/


All Articles