Menu

Func ו-Action ב-C#: delegates גנריים, Predicate ודוגמאות

איך עובדים Func ו-Action ב-C#: Func<T, TResult> לפונקציות שמחזירות ערך, Action<T> לפונקציות שלא, Predicate ו-Comparison, העברה והחזרה של פונקציות, מילון של פקודות, lambdas אסינכרוניות, ומתי עדיף להצהיר על delegate משלכם.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

Func ו-Action הם טיפוסי delegate גנריים ש-.NET מצהירה עליהם בשבילכם, כך שאפשר לשמור ולהעביר פונקציות בלי להצהיר קודם על טיפוס delegate. 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>Tvoid
Action<T1, T2> ... עד 16T1, T2, ...void
Func<TResult>איןTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... עד 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (שלילי, אפס, חיובי)
Converter<TIn, TOut>TInTOut

Predicate, Comparison ו-Converter ותיקים יותר ומופיעים במתודות של List<T> ושל Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ משתמש ב-Func לאורך כל הדרך, ולכן list.Where(...) ו-list.FindAll(...) מקבלות lambdas באותה צורה אבל מטיפוסים שונים:

פלט:

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 לוכדת את המשתנה עצמו ולא עותק של הערך שלו.

Method groups: העברת מתודה לפי שם

לא צריך lambda כדי למלא Func או Action. השם של מתודה קיימת עם חתימה מתאימה (method group) מומר ישירות:

פלט:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) זהה ל-Select(s => int.Parse(s)), רק בלי שכבת קריאה אחת. הקומפיילר בוחר את ההעמסה שמתאימה לטיפוס היעד: ל-int.Parse יש כמה העמסות, ורק Parse(string) מתאימה ל-Func<string, int>. כשהקומפיילר לא מצליח להכריע (כמה העמסות מתאימות באותה מידה, או שאי אפשר להסיק את ארגומנטי הטיפוס של מתודה גנרית מתוך method group), הקריאה לא מתקמפלת, ו-lambda שכותבת את הקריאה במפורש פותרת את זה.

method group שמשמש כ-Func עדיין רץ מאוחר יותר, בכל פעם שמפעילים את ה-delegate, בדיוק כמו 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, טיפוס ה-delegate קובע אם מי שקורא יכול לחכות לעבודה:

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: שום דבר לא יכול לבצע עליה await, וחריגה שבתוכה לעולם לא מגיעה למי שקרא. היא נזרקת מחדש ב-thread pool (או ב-UI thread באפליקציית דסקטופ), ושם היא בדרך כלל מפילה את התהליך. כשכותבים מתודה שמקבלת עבודה אסינכרונית, קבלו Func<Task> (או Func<Task<T>>).

מתי להצהיר על delegate משלכם

Func ו-Action מכסים את רוב הצרכים. הצהירו על טיפוס delegate עם שם כאשר:

  • בחתימה יש פרמטרים 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, שגם הוא לא אפשרי ב-delegates גנריים.

אירועים הם המקום האחר שבו טיפוסי delegate מותאמים עדיין מופיעים, אם כי EventHandler<TEventArgs> המובנה מכסה את רובם. העמוד על delegates מכסה הצהרה ושילוב של טיפוסי delegate.

שאלות נפוצות

מה ההבדל בין 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#?

טיפוס delegate שמקבל 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 מותאם: delegate bool TryParser<T>(string text, out T value);.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל