The ربما Monad عبر المزامنة / في انتظار C # (بدون Task ov!)


تعد أنواع الإرجاع غير المتزامنة العامة ميزة جديدة مقدمة في C # 7 تتيح لك استخدام ليس فقط المهمة كنوع الإرجاع لأساليب غير متزامنة (غير متزامنة / تنتظر ) ، ولكن أيضًا أي أنواع أخرى (فئات أو بنيات) تفي بمتطلبات معينة.


في الوقت نفسه ، يعد التزامن / الانتظار طريقة لاستدعاء مجموعة معينة من الوظائف بالتسلسل في سياق معين ، وهو جوهر نمط تصميم Monad . والسؤال هو ، هل يمكننا استخدام المزامنة / الانتظار لكتابة التعليمات البرمجية التي تتصرف كما لو كنا نستخدم monads؟ اتضح أن نعم (مع بعض التحفظات). على سبيل المثال ، يقوم الكود أدناه بترجمة ويعمل:


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

بعد ذلك ، أشرح كيف يعمل هذا الرمز ...


أنواع الإرجاع غير المتزامن العامة


بادئ ذي بدء ، دعنا نتعرف على ما هو مطلوب لاستخدام نوعنا الخاص (على سبيل المثال ، فئة MyAwaitable <T> ) كنوع النتيجة لبعض الوظائف غير المتزامنة. تقول الوثائق أن هذا النوع يجب أن يكون:


  1. أسلوب GetAwaiter () ، والذي يقوم بإرجاع كائن من النوع يقوم بتنفيذ واجهة INotifyCompletion ، كما يحتوي على خاصية IsCompleted المنطقية وطريقة T GetResult () ؛


  2. [AsyncMethodBuilder (Type)] - سمة تشير إلى النوع الذي سيكون بمثابة " Method Builder " ، على سبيل المثال MyAwaitableTaskMethodBuilder <T> . يجب أن يحتوي هذا النوع في الطرق التالية:


    • إنشاء ثابت ()
    • البداية (ولاية ماشين)
    • SetResult (النتيجة)
    • SetException (استثناء)
    • SetStateMachine (stateMachine)
    • AwaitOnCompleted (awaiter ، stateMachine)
    • AwaitUnsafeOnCompleted (awaiter، 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);//   return await new MyAwaitable<int>(arg); } 

يعمل هذا الرمز ، ولكن لفهم جوهر متطلبات فئة MyAwaitable ، دعونا نرى ما يفعله المعالج المسبق لـ C # مع طريقة MyAwaitableMethod . إذا قمت بتشغيل برنامج التحويل البرمجي لبرنامج التحويل البرمجي .NET (على سبيل المثال ، 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:// 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); } } 

بعد فحص الكود الذي تم إنشاؤه ، نرى أن Method Builder لديه المسؤوليات التالية:


  1. تنظيم استدعاء الأسلوب MoveNext () الذي ينقل جهاز الحالة المولدة إلى الحالة التالية.
  2. إنشاء كائن يمثل سياق عملية غير متزامنة ( public MyAwaitable<T> Task { get; } )
  3. الاستجابة لترجمة جهاز الحالة الذي تم إنشاؤه إلى حالات نهائية: SetResult أو SetException .

بمعنى آخر ، بمساعدة Method Builder ، يمكننا التحكم في كيفية تنفيذ الأساليب غير المتزامنة ، ويبدو أن هذه فرصة ستساعدنا على تحقيق هدفنا - تنفيذ سلوك ربما أحادي.


ولكن ما هو جيد حول هذا monad؟ ... في الواقع ، يمكنك العثور على العديد من المقالات حول هذا monad على شبكة الإنترنت ، لذلك هنا سوف أصف فقط الأساسيات.


ربما مناد


باختصار ، ربما يكون monad عبارة عن نمط تصميم يتيح لك مقاطعة سلسلة استدعاءات الوظائف إذا لم تتمكن بعض الوظائف من السلسلة من إرجاع نتيجة ذات معنى (على سبيل المثال ، معلمات الإدخال غير الصالحة).


حلت لغات البرمجة التاريخية الحتمية هذه المشكلة بطريقتين:


  1. الكثير من المنطق الشرطي
  2. استثناءات

كلتا الطريقتين لها عيوب واضحة ، لذلك تم اقتراح نهج بديل:


  1. قم بإنشاء نوع يمكن أن يكون في حالتين: "بعض القيمة" و "بلا قيمة" (" لا شيء ") - دعنا نسميها ربما
  2. إنشاء وظيفة (دعنا نسميها SelectMany ) التي تأخذ وسيطات 2:
    2.1. ربما يعترض
    2.2. الوظيفة التالية من قائمة المكالمات. يجب أن تُرجع هذه الوظيفة أيضًا كائنًا من النوع ربما ، والذي قد يحتوي على نوع من القيمة الناتجة أو في حالة لا شيء إذا تعذر الحصول على النتيجة (على سبيل المثال ، تم تمرير معلمات غير صحيحة إلى الدالة)
  3. تقوم وظيفة SelectMany بالتحقق من كائن من النوع ربما ، وإذا كانت تحتوي على القيمة الناتجة ، فسيتم استخراج هذه النتيجة وتمريرها كوسيطة إلى الوظيفة التالية من سلسلة الاتصال (يتم تمريرها كوسيطة ثانية). إذا كان الكائن ربما في حالة Nothing ، فسوف يقوم SelectMany بإرجاع Nothing على الفور.


في 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 # يقوم المعالج المسبق بإدراج استدعاء Select Many عند معالجة التعبيرات المكتوبة في Query Notation ، والتي هي في جوهرها "السكر النحوي" للسلاسل المعقدة للمكالمات (يمكنك العثور على مزيد من المعلومات حول هذا الموضوع في مقالي السابق ).


في الواقع ، يمكننا إعادة كتابة الكود السابق على النحو التالي:


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

وبالتالي الوصول إلى الحالة المتوسطة (x2 ، x3) ، والتي في بعض الحالات يمكن أن تكون مريحة للغاية. لسوء الحظ ، فإن قراءة هذه الشفرة أمر صعب للغاية ، لكن لحسن الحظ ، يحتوي C # على ترميز الاستعلام بمساعدة مثل هذا الرمز الذي سيكون أسهل بكثير:


 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)); //       "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; 

لا يبدو هذا الرمز أنيقًا جدًا ، حيث إن C # لم يكن مصممًا في الأصل كلغة وظيفية ، ولكن هذا النهج شائع جدًا في اللغات الوظيفية "الحقيقية".


متزامن ربما


إن جوهر ربما monad هو التحكم في سلسلة استدعاءات الوظائف ، ولكن هذا هو بالضبط ما يفعله المزامنة / الانتظار . لذلك ، دعونا نحاول الجمع بينهما. أولاً ، نحتاج إلى جعل نوع ربما متوافقًا مع الوظائف غير المتزامنة ، ونعرف بالفعل كيفية تحقيق ذلك:


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

الآن ، لنرى كيف يمكن إعادة كتابة "الكلاسيكية" كآلية حالة محدودة حتى نتمكن من العثور على أي أوجه تشابه:


 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 هو كائن من النوع ربما . المشكلة هنا هي أننا لا نستطيع ضبط الآلة على الحالة "النهائية" (-1) ، لكن هل يعني ذلك أننا لا نستطيع التحكم في تدفق التنفيذ؟ هذا في الواقع ليس هو الحال. الحقيقة هي أنه بالنسبة لكل إجراء غير متزامن ، تقوم C # بإعداد وظيفة رد اتصال لمتابعة الإجراء غير المتزامن من خلال واجهة INotifyCompletion ، لذلك إذا أردنا كسر تدفق التنفيذ ، يمكننا ببساطة استدعاء وظيفة رد الاتصال عندما لا يمكننا متابعة سلسلة العمليات غير المتزامنة.
هناك مشكلة أخرى هنا وهي أن جهاز الحالة الذي تم إنشاؤه ينقل الخطوة التالية (كدالة رد اتصال) إلى التسلسل الحالي للعمليات غير المتزامنة ، لكننا نحتاج إلى وظيفة رد اتصال للتسلسل الأصلي الذي سيتيح لنا تجاوز جميع السلاسل المتبقية من العمليات غير المتزامنة (من أي مستوى تداخل) :



لذلك ، نحن بحاجة إلى ربط الإجراء غير المتزامن المتداخل الحالي بطريقة ما مع منشئه. يمكننا القيام بذلك باستخدام Method Builder ، والذي يحتوي على رابط للعملية غير المتزامنة الحالية - المهمة . سيتم تمرير الارتباطات إلى جميع العمليات غير المتزامنة AwaitOnCompleted (ref awaiter) إلى AwaitOnCompleted (ref awaiter) كـ awaiter ، لذلك نحتاج فقط إلى التحقق مما إذا كانت المعلمة هي مثال ربما ، ثم قم بتعيين الحالي ربما بصفته أصل الإجراء التابع الحالي:


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

الآن كل الكائنات من النوع ربما يمكن دمجها في تسلسل هرمي ، ونتيجة لذلك ، سنتمكن من الوصول إلى الاستدعاء النهائي للتسلسل الهرمي بأكمله (طريقة الخروج ) من أي عقدة:


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

في الوقت الذي يستدعي فيه الجهاز غير المتزامن الحالة (عبر Method Builder ) طريقة 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) //  "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(); } } 

الآن يبقى سؤال واحد فقط - كيفية الحصول على نتيجة غير متزامن ربما خارج نطاقه (أي طريقة غير متزامنة نوع الإرجاع الخاص به ربما ربما ). إذا حاولت استخدام الكلمة الرئيسية التي تنتظر الانتظار فقط مع مثيل ربما ، فسيتم طرح استثناء بواسطة هذا الرمز:


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

لحل هذه المشكلة ، يمكننا ببساطة إضافة عامل انتظار جديد سيعيد بنية MaybeResult بأكملها ، ومن ثم يمكننا كتابة هذا الرمز:


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

هذا كل شيء الآن. في أمثلة التعليمات البرمجية ، حذفت بعض التفاصيل للتركيز فقط على الأجزاء الأكثر أهمية. يمكنك العثور على النسخة الكاملة على جيثب .


في الواقع ، لا أوصي باستخدام الطريقة المذكورة أعلاه في أي رمز عمل ، لأنه يحتوي على مشكلة واحدة مهمة - عندما نكسر سلسلة التنفيذ ، مما يؤدي إلى استمرار عملية الجذر غير المتزامنة (مع النوع ربما ) ، فإننا نكسر الكل على الإطلاق! بما في ذلك جميع الكتل الأخيرة (هذا هو الجواب على السؤال "هل تسمى الكتل أخيرًا دائمًا؟") ، لذلك لن تستخدم جميع العبارات المستخدمة بشكل صحيح ، مما قد يؤدي إلى تسرب مورد. يمكن حل هذه المشكلة إذا قمنا بدلاً من الاتصال المباشر بالاستمرار ، فسنثير استثناء خاصًا سيتم معالجته ضمنيًا ( هنا يمكنك العثور على هذا الإصدار ) ، ولكن من الواضح أن هذا الحل له حد أداء (قد يكون مقبولًا في بعض السيناريوهات). في الإصدار الحالي من برنامج التحويل البرمجي C # ، لا أرى حلاً آخر ، لكن ربما يتغير هذا في يوم ما في المستقبل.


ومع ذلك ، لا تعني هذه القيود أن جميع التقنيات الموضحة في هذه المقالة عديمة الفائدة تمامًا ، يمكن استخدامها لتنفيذ monads الأخرى التي لا تتطلب تغييرات في سلاسل الرسائل ، على سبيل المثال ، "Reader". كيفية تنفيذ هذا "قارئ" أحادي من خلال async / انتظار سوف تظهر في المقالة التالية .

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


All Articles