Func وAction أنواع مفوّضات عامة تصرّح بها .NET لك، فتستطيع تخزين الدوال وتمريرها دون التصريح بنوع مفوّض أولًا. تعيد Func قيمة؛ ولا تعيد Action شيئًا.
المخرجات:
4
46.00
False
Hello!
Mia scored 87
القاعدة التي تتذكرها: في Func يكون وسيط النوع الأخير نوع الإرجاع. تأخذ Func<int, int, bool> قيمتي int وتعيد bool. وتأخذ Func<int> لا شيء وتعيد int. أما وسائط نوع Action فكلها معاملات.
العائلة
| النوع | المعاملات | ما تعيده |
|---|---|---|
Action | لا شيء | void |
Action<T> | T | void |
Action<T1, T2> ... حتى 16 | T1, T2, ... | void |
Func<TResult> | لا شيء | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... حتى 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (سالب، صفر، موجب) |
Converter<TIn, TOut> | TIn | TOut |
Predicate وComparison وConverter أقدم وتظهر في دوال List<T> وArray: FindAll(Predicate<T>) وSort(Comparison<T>) وConvertAll(Converter<T, TOut>). يستخدم LINQ النوع Func في كل مكان، ولهذا تأخذ list.Where(...) وlist.FindAll(...) تعابير lambda بالشكل نفسه لكن بنوعين مختلفين:
المخرجات:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
تمرير الدوال إلى الدوال
يتيح معامل Func أو Action للمستدعي أن يقدّم الجزء المتغيّر من خوارزمية. شكلان شائعان: "نفّذ هذا وقِسه" و"جرّب هذا حتى ينجح":
المخرجات:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry عامة، فيمكن لـ Func<int, T> فيها أن تعيد أي شيء؛ واستنتج المترجم T على أنه string من تعبير lambda. ويغيّر تعبير lambda الممرّر إلى Twice المتغيّر المحلي clicks، وهذا يعمل لأن تعبير lambda يلتقط المتغيّر نفسه لا نسخة من قيمته.
مجموعات الدوال: تمرير دالة باسمها
لا تحتاج إلى تعبير lambda لملء Func أو Action. اسم دالة موجودة بتوقيع مطابق ("مجموعة دوال" أو method group) يتحوّل مباشرة:
المخرجات:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) مثل Select(s => int.Parse(s))، مع طبقة استدعاء أقل. يختار المترجم الشكل الذي يناسب النوع المستهدف: لـ int.Parse عدة أشكال، ولا يطابق Func<string, int> إلا Parse(string). وحين لا يستطيع المترجم الاستقرار على واحد (عدة أشكال تناسب بالقدر نفسه، أو لا يمكن استنتاج وسائط نوع دالة عامة من مجموعة دوال) لا يُترجم الاستدعاء، ويصلحه تعبير lambda يجعل الاستدعاء صريحًا.
مجموعة الدوال المستخدمة كـ Func ما زالت تُنفَّذ لاحقًا، كلما استُدعي المفوّض، تمامًا مثل تعبير lambda. الفرق فقط في طريقة الكتابة.
إعادة الدوال
يمكن للدالة أن تبني دالة وتعيدها. وتحمل Func المعادة القيم التي بُنيت منها:
المخرجات:
140
180
162
تبني Twice(staff) دالة جديدة تطبّق خصم الموظفين مرتين: 200، ثم 180، ثم 162.
قاموس من الدوال
تخزين الدوال في قاموس يحوّل switch طويلة إلى جدول بحث. وهو الشكل المعتاد لمعالجات الأوامر والآلات الحاسبة وأفعال القوائم، ويمكن إضافة مدخلات جديدة وقت التشغيل:
المخرجات:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
إضافة معامل جديد مدخل واحد إضافي في القاموس، دون تغيير في الحلقة. ويجعل StringComparer.OrdinalIgnoreCase الممرّر إلى القاموس الثاني أسماء الأوامر غير حساسة لحالة الأحرف.
الدوال غير المتزامنة: Func<Task> لا Action
مع الشيفرة async يحدد نوع المفوّض هل يستطيع المستدعي انتظار العمل:
Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save(); // the caller can await it and see its exceptions
Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);
Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad(); // async void: fire and forget, exceptions escape
تعبير lambda غير المتزامن المُسند إلى Action يصبح دالة async void: لا شيء يستطيع انتظارها، والاستثناء داخلها لا يصل أبدًا إلى المستدعي. يُعاد رميه على مجمّع الخيوط (أو خيط الواجهة في تطبيق سطح المكتب)، حيث يُسقط العملية عادة. حين تكتب دالة تقبل عملًا غير متزامن فخذ Func<Task> (أو Func<Task<T>>).
متى تصرّح بمفوّضك الخاص
تغطي Func وAction معظم الحاجات. صرّح بنوع مفوّض مسمّى حين:
- يحتوي التوقيع على معاملات
refأوoutأوin. لا تستطيعFuncالتعبير عنها، فتحتاج دالة بأسلوبTryParseإلىdelegate bool TryParser<T>(string text, out T value);. - يوثّق الاسم دورًا في واجهة API عامة. تقول
delegate bool Validator(string input)في توقيع دالة أكثر مما تقولهFunc<string, bool>، وتظهر أسماء معاملاتها في IntelliSense. - تحتاج إلى معامل
params، الذي لا تستطيعه المفوّضات العامة أيضًا.
الأحداث هي المكان الآخر الذي ما زالت تظهر فيه أنواع المفوّضات المخصصة، مع أن EventHandler<TEventArgs> المدمج يغطي معظمها. تغطي صفحة المفوّضات التصريح بأنواع المفوّضات ودمجها.
الأسئلة الشائعة
ما الفرق بين Func وAction في C#؟
تعيد Func قيمة ولا تعيد Action. في Func<int, string, bool> يكون وسيط النوع الأخير (bool) نوع الإرجاع والبقية معاملات. وتأخذ Action<int, string> قيمة int وقيمة string وتعيد void. كلتاهما تأتي بنسخ من 0 إلى 16 معاملًا.
كيف أمرّر دالة كمعامل في C#؟
صرّح بالمعامل كـ Func أو Action بالشكل المناسب واستدعه داخل الدالة: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. يمرّر المستدعون تعبير lambda (Apply(80m, p => p * 0.9m)) أو اسم دالة (Apply(80m, HalfPrice)).
ما هو Predicate<T> في C#؟
نوع مفوّض يأخذ T ويعيد bool، بشكل Func<T, bool> نفسه. أقدم من Func وتستخدمه List<T>.Find وFindAll وRemoveAll وExists وArray.Find. لا يتحوّل أحد النوعين إلى الآخر، لكن تعبير lambda يتحوّل إلى أي منهما.
كيف أستخدم Func مع الشيفرة غير المتزامنة؟
استخدم Func<Task> لدالة غير متزامنة بلا نتيجة وFunc<T, Task<TResult>> لدالة بنتيجة، كي يستطيع المستدعي انتظارها بـ await. تجنّب إسناد تعبير lambda غير متزامن إلى Action: هذا يجعله async void، الذي لا يمكن انتظاره ولا يستطيع المستدعي التقاط استثناءاته.
هل يمكن أن تملك Func معاملات out أو ref؟
لا. معاملات الأنواع العامة في Func وAction معاملات قيم عادية، فلا سبيل لكتابة Func<string, out int, bool>. لتوقيع مثل TryParse صرّح بمفوّض مخصص: delegate bool TryParser<T>(string text, out T value);.