المفوّض (delegate) نوع يمثّل توقيع دالة. يحمل متغيّر من نوع مفوّض مرجعًا إلى دالة بذلك التوقيع، واستدعاء المتغيّر يستدعي الدالة. هكذا تعامل C# الدوال كقيم: يمكنك تخزينها وتمريرها إلى دوال أخرى واستدعاؤها لاحقًا.
المخرجات:
40
70
68.00
تصرّح delegate decimal PriceRule(decimal price); بنوع جديد، تمامًا كما تفعل class. أي دالة تأخذ decimal واحدًا وتعيد decimal تناسبه. أشار المتغيّر rule إلى ثلاث دوال مختلفة بالتتابع، وكل استدعاء نفّذ الدالة التي كان يحملها.
التصريح والإنشاء والاستدعاء
يبدو تصريح نوع المفوّض كتوقيع دالة تسبقه الكلمة delegate. ويوضع عادة على مستوى فضاء الأسماء، بجانب الأصناف، أو متداخلًا داخل صنف.
هناك أربع طرق لإنشاء مثيل مفوّض:
PriceRule a = HalfPrice; // method group (most common)
PriceRule b = new PriceRule(HalfPrice); // explicit constructor, same thing
PriceRule c = p => p - 5m; // lambda expression
PriceRule d = delegate (decimal p) { return p; }; // anonymous method (C# 2 syntax)
وطريقتان لاستدعائه، متطابقتان: rule(80m) وrule.Invoke(80m). يفحص المترجم التوقيع وقت الإسناد، فتكون الدالة غير المطابقة خطأ ترجمة لا مفاجأة وقت التشغيل.
يمكن للمفوّض أن يشير إلى دالة مثيل أيضًا. عندها يتذكر الدالة والكائن الذي يستدعيها عليه:
المخرجات:
Ana got: Your order shipped
target: Mailbox, method: Receive
Target هو الكائن الذي سيستدعي المفوّض الدالة عليه (null للدالة الساكنة)، وMethod تصف الدالة. وحمل الهدف يعني أيضًا أن المفوّض يبقي ذلك الكائن حيًا، وهذا مهم للأحداث.
المفوّضات كدوال استدعاء راجع
المهمة الرئيسية للمفوّض أن يتيح لدالة استدعاء شيفرة اختارها مستدعيها. تحدد الدالة متى يحدث شيء؛ ويحدد المستدعي ماذا:
المخرجات:
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
لا تعرف ImportRows شيئًا عن الكونسول أو الأشرطة. يمكن للدالة نفسها أن تقود شريط تقدم أو سطر سجل أو تحديث واجهة، بحسب ما يمرّره المستدعي. وتتبع List.Sort(Comparison<T>) وArray.Find(Predicate<T>) وTask.Run(Action) وكل معامل في LINQ هذا النمط.
المفوّضات المتعددة: += و-=
يمكن للمفوّض أن يحمل أكثر من دالة. تُلحق += دالة بقائمة استدعائه، وتحذفها -=، واستدعاء المفوّض يستدعي كل دالة بالترتيب:
المخرجات:
email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002
ثلاثة تفاصيل في المفوّضات المتعددة تهمّ عمليًا:
- القيم المعادة: إن كان نوع المفوّض يعيد قيمة، فاستدعاء مفوّض متعدد لا يعيد إلا نتيجة الدالة الأخيرة. لجمع كل النتائج مرّ على
GetInvocationList()واستدعِ كلًّا منها بنفسك. - الاستثناءات: إن رمت دالة واحدة استثناءً فلا تُنفَّذ الدوال الباقية ويصل الاستثناء إلى المستدعي.
- عدم القابلية للتغيير: المفوّضات غير قابلة للتغيير. تنشئ
+=مفوّضًا جديدًا بقائمة أطول وتسنده إلى المتغيّر، ولهذا لا يؤثر الحذف بـ-=على نسخة في الأصل.
حذف تعبير lambda بـ -= لا يعمل إلا إن مرّرت مثيل المفوّض نفسه. كتابة تعبير lambda نفسه مرتين تنشئ مفوّضين مختلفين، فلا تحذف handlers -= id => Log(id); شيئًا. خزّن تعبير lambda في متغيّر حين تنوي إلغاء اشتراكه.
المفوّضات null و?.Invoke
متغيّر المفوّض الذي لم يُسند إليه شيء يكون null، وحذف الدالة الأخيرة بـ -= يتركه null أيضًا. استدعاء null يرمي NullReferenceException:
المخرجات:
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke هي الطريقة القياسية لاستدعاء دالة استدعاء راجع اختيارية. لا يمكن وضع المعامل ?. مباشرة قبل الأقواس (onSaved?("x") ليست صياغة صحيحة)، فيستدعي النمط دالة Invoke في المفوّض باسمها، وهي تفعل ما يفعله الاستدعاء المختصر.
المفوّضات وFunc وAction والأحداث
التصريح بنوع مفوّض لكل توقيع سيكون مملًا، لذا توفّر .NET أنواعًا عامة:
| النوع المدمج | التصريح المخصص المكافئ |
|---|---|
Action | delegate void Action(); |
Action<T> | delegate void Action<T>(T arg); |
Func<TResult> | delegate TResult Func<TResult>(); |
Func<T, TResult> | delegate TResult Func<T, TResult>(T arg); |
Predicate<T> | delegate bool Predicate<T>(T obj); |
PriceRule من المثال الأول بشكل Func<decimal, decimal> نفسه، ومع ذلك هما نوعان مختلفان ولا يتحوّل أحدهما إلى الآخر. وحتى نوعان مدمجان بتوقيعين متطابقين لا يتحوّلان:
Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'
Predicate<int> ok = new Predicate<int>(isPositive); // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x); // or with a lambda
يتحوّل تعبير lambda إلى أي نوع مفوّض متوافق، لذا تأخذ واجهات API عادة تعابير lambda ولا يُطرح السؤال أبدًا. تغطي صفحة Func وAction العائلة العامة ومتى يظل التصريح المخصص مجديًا.
الأحداث مبنية مباشرة على المفوّضات. حقل event مفوّض متعدد لا يستطيع الشيفرة الخارجية سوى += و-= عليه، ولا تستطيع أبدًا استدعاءه أو استبداله. قائمة المشتركين في مثال التعدد أعلاه هي بالضبط ما يديره الحدث عنك، مع تلك الحماية الإضافية. الأحداث مشروحة في صفحتها الخاصة.
الأسئلة الشائعة
ما هو المفوّض (delegate) في C#؟
المفوّض مرجع آمن الأنواع إلى دالة. تصرّح بنوع مفوّض بتوقيع، مثل delegate decimal PriceRule(decimal price);، ويمكن تخزين أي دالة بهذا التوقيع (ساكنة، أو دالة مثيل، أو تعبير lambda) في متغيّر من ذلك النوع واستدعاؤها لاحقًا عبره. المفوّضات هي طريقة C# في تمرير الدوال كقيم.
ما الفرق بين delegate وFunc في C#؟
Func<T, TResult> وAction<T> أنواع مفوّضات صرّحت بها .NET لك مسبقًا، وهي عامة بحسب أنواع المعاملات والإرجاع. أما تصريح delegate المخصص فيمنح النوع اسمًا وأسماء معاملات توثّق دوره، ويدعم معاملات ref وout وin. لمعظم الشيفرات تكفي Func وAction.
ما هو المفوّض المتعدد (multicast) في C#؟
مفوّض يحمل عدة دوال. تضيف += دالة إلى قائمة استدعائه وتحذف -= واحدة؛ واستدعاء المفوّض يستدعيها جميعًا بالترتيب. إن كان المفوّض يعيد قيمة فلا يتلقى المستدعي إلا نتيجة الدالة الأخيرة، والاستثناء في دالة واحدة يمنع تنفيذ البقية.
كيف أستدعي بأمان مفوّضًا قد يكون null؟
استخدم المعامل الشرطي لـ null: onProgress?.Invoke(50);. متغيّر المفوّض الذي لم تُسند إليه دوال يكون null، واستدعاؤه مباشرة يرمي NullReferenceException. تقرأ ?.Invoke المتغيّر مرة واحدة، فتكون آمنة أيضًا إن حذف خيط آخر آخر معالج في اللحظة نفسها.
متى أصرّح بنوع مفوّض خاص بدل استخدام Func أو Action؟
حين يحتاج التوقيع إلى معاملات ref أو out أو in (لا تستطيع Func وAction التعبير عنها)، وحين يجعل نوع مسمّى واجهة API عامة أوضح (delegate bool Validator(string input))، أو حين تريد أن تظهر أسماء المعاملات في IntelliSense. وإلا ففضّل Func وAction، اللتين يعرفهما كل مطوّر .NET.