
أنواع الإرجاع المتزامن العامة - إنها ميزة C # 7 جديدة تسمح باستخدام ليس فقط Task كنوع إرجاع لأساليب غير متزامنة ولكن أيضًا أنواع أخرى (فئات أو بنيات) تلبي بعض المتطلبات المحددة.
في الوقت نفسه ، يمثل المزامنة / الانتظار طريقة لاستدعاء مجموعة من وظائف "الاستمرارية" داخل بعض السياق الذي يعد جوهرًا لنمط تصميم آخر - 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()); }
علاوة على ذلك ، سأشرح كيف يعمل الرمز ...
أنواع الإرجاع المتزامن المعمم
بادئ ذي بدء ، دعونا نتعرف على ما هو مطلوب لاستخدام نوعنا الخاص (على سبيل المثال ، MyAwaitable <T> ) كنوع نتيجة لبعض وظائف المزامنة. تقول الوثائق أن هذا النوع يجب أن يكون:
GetAwaiter () الأسلوب الذي يقوم بإرجاع كائن من النوع الذي ينفذ واجهة INotifyCompletion ولديه خاصية IsCompleted منطقية وطريقة T GetResult () ؛
سمة [AsyncMethodBuilder (Type)] التي تشير إلى فئة ( أو بنية ) "منشئ الطريقة" (مثال: 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);
يعمل الرمز كما هو متوقع ولكن لفهم الغرض من متطلبات MyAwaitable ، دعونا نلقي نظرة على ما يفعله المعالج المسبق لـ C # مع MyAwaitableMethod . إذا قمت بتشغيل بعض de-compilation (مثل 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:
عند مراجعة الكود الذي تم إنشاؤه ، يمكننا أن نرى أن "منشئ الأسلوب" لديه المسؤوليات التالية:
- جدولة استدعاء الأسلوب MoveNext () الجهاز الحالة عند إجراء عملية غير متزامنة تابعة (في أبسط سيناريو نحن فقط تمرير MoveNext () إلى OnCompleted () من انتظار عملية المزامنة).
- إنشاء كائن سياق عملية غير متزامن (
public MyAwaitable<T> Task { get; }
) - الرد على الحالات النهائية لآلات الحالة التي تم إنشاؤها: SetResult أو SetException .
بمعنى آخر ، من خلال "مُنشئي الأساليب" ، يمكننا التحكم في كيفية تنفيذ الأساليب غير المتزامنة ويبدو أنها ميزة ستساعدنا على تحقيق هدفنا - تنفيذ سلوك ربما أحادي. ولكن ما هو جيد عن هذا monad؟ حسنًا ... يمكنك العثور على الكثير من المقالات حول هذا الموناد في الإنترنت ، لذلك سوف أصف هنا الأساسيات فقط.
ربما أحادي
باختصار ، ربما يكون monad عبارة عن نمط تصميم يتيح مقاطعة سلسلة استدعاء دالة إذا لم تتمكن بعض الوظائف من السلسلة من الحصول على نتيجة قيمة (مثل تحليل الخطأ).
تاريخيا ، كانت لغات البرمجة الملحة تحل المشكلة بطريقتين:
- الكثير من المنطق الشرطي
- استثناءات
لكلتا الطريقتين عيوب واضحة ، لذلك اخترع طريق ثالث:
- قم بإنشاء نوع يمكن أن يكون في حالتين: "بعض القيمة" و "لا شيء" - دعنا نسميها "ربما"
- أنشئ وظيفة (دعنا نسميها "SelectMany") تسترجع وسيطين:
2.1. كائن من نوع "ربما"
2.2. وظيفة تالية من مجموعة الاتصال - يجب أن تُرجع الدالة أيضًا كائنًا "ربما" سيحتوي على نتيجة أو "لا شيء" إذا تعذر تقييم نتيجتها (على سبيل المثال ، معلمات الوظيفة ليست بالتنسيق الصحيح) - تقوم وظيفة "SelectMany" بالتحقق مما إذا كانت "ربما" لها قيمة ثم تستدعي الوظيفة التالية باستخدام القيمة (المستخرجة من "ربما") كوسيطة ثم ترجع النتيجة ، وإلا فإنها تُرجع كائن "ربما" في حالة "لا شيء" .

في 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 # يتم استخدام SelectMany بواسطة المعالج الأولي لدعم تدوين الاستعلامات الذي يبسط العمل مع سلاسل الاتصال (يمكنك العثور على مزيد من التفاصيل في مقالتي السابقة )).
في الواقع ، يمكننا تغيير سلسلة الاتصال على النحو التالي:
var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4)));
حتى نتمكن من الوصول إلى جميع النتائج الوسيطة التي هي مريحة ولكن من الصعب قراءة الكود.
هنا يساعدنا تدوين الاستعلام على:
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));
لا يبدو الرمز رائعًا لأن C # لم يتم تصميمه كلغة وظيفية ، ولكن بلغات وظيفية "حقيقية" مثل Haskell ، يعد هذا النهج شائعًا للغاية
متزامن ربما
إن جوهر ربما monad هو التحكم في سلسلة نداء الوظائف ، ولكن هذا بالضبط هو "async / انتظار". لذلك دعونا نحاول الجمع بينهما. أولاً ، نحتاج إلى جعل ربما تكتب متوافقًا مع وظائف غير متزامنة ونعرف بالفعل كيفية القيام بذلك:
[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 ، لذلك إذا أردنا كسر تدفق التنفيذ ، يمكننا فقط استدعاء إجراء رد الاتصال في حالة عندما يتعذر علينا مواصلة التدفق.
التحدي الآخر هنا هو أن جهاز الحالة المولدة يمرر الإجراء التالي (كرد اتصال مستمر) للتدفق الحالي ، لكننا نحتاج إلى رد اتصال مستمر للتدفق الأولي الذي يسمح بتجاوز باقي عمليات المزامنة:

لذلك ، نحن بحاجة إلى ربط عمل مزامنة الطفل بطريقة ما مع أسلافه. يمكننا القيام بذلك باستخدام "أداة إنشاء" الخاصة بنا والتي تحتوي على رابط لعملية مزامنة حالية - مهمة . سيتم تمرير الروابط إلى جميع عمليات AwaitOnCompleted(ref awaiter
في AwaitOnCompleted(ref awaiter
كمسؤول انتظار ، لذلك نحتاج فقط إلى التحقق مما إذا كانت المعلمة هي مثال ربما ، وإذا كان قد تم تعيينها الحالية ، فربما تكون AwaitOnCompleted(ref 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() { }
عندما تستدعي آلة حالة غير متزامنة (من خلال منشئ الطريقة) طريقة OnCompleted لحل تم حله بالفعل ، ربما في حالة لا شيء ، سنكون قادرين على كسر تدفق كامل:
public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result)
الآن يبقى الشيء الوحيد - كيفية الحصول على نتيجة لمزامنة ربما خارج نطاقه (أي طريقة غير متزامنة لن يكون نوع الإرجاع الخاص بها). إذا حاولت استخدام مجرد انتظار الكلمة الرئيسية مع مثال ربما ، فسيتم طرح استثناء بسبب هذا الرمز:
[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; }
لحل المشكلة ، يمكننا فقط إضافة Awaiter جديد من شأنه أن يعيد بنية MaybeResult بالكامل وسوف نتمكن من كتابة رمز مثل هذا:
var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... };
هذا كل شيء الآن. في نماذج التعليمات البرمجية ، أغفل بعض التفاصيل للتركيز فقط على الأجزاء الأكثر أهمية. يمكنك العثور على نسخة كاملة على جيثب .
ومع ذلك ، لا أوصي باستخدام هذا الإصدار في أي كود إنتاج لأنه يحتوي على مشكلة كبيرة - عندما نقضي على تدفق التنفيذ عن طريق استدعاء استمرار الجذر ربما سنتجاوز كل شيء! بما في ذلك جميع الكتل الأخيرة (إنها إجابة على السؤال "هل تسمى الكتل أخيرًا دائمًا؟") ، وبالتالي فإن جميع المشغلين الذين يستخدمون لن يعمل كما هو متوقع وقد يؤدي ذلك إلى تسرب الموارد. يمكن حل المشكلة إذا بدلاً من استدعاء رد الاتصال الأولي للاستمرار ، فسنطرح استثناءًا خاصًا سيتم معالجته داخليًا ( هنا يمكنك العثور على الإصدار ) ، ولكن يبدو أن هذا الحل يحتوي على تقليد للأداء (قد يكون مقبولًا في بعض السيناريوهات). مع الإصدار الحالي من برنامج التحويل البرمجي C # ، لا أرى أي حل آخر ولكن قد يتم تغيير ذلك في المستقبل.
لا تعني هذه القيود أن جميع الحيل الموضحة في هذه المقالة عديمة الفائدة تمامًا ، يمكن استخدامها لتنفيذ monads الأخرى التي لا تتطلب تغييرات في تدفقات التنفيذ ، على سبيل المثال "Reader". كيفية تنفيذ هذا "القارئ" أحادي الحوض الصغير / تنتظر سأظهر في المقالة التالية .