delegate הוא טיפוס שמייצג חתימה של מתודה. משתנה מטיפוס delegate מחזיק הפניה למתודה עם החתימה הזו, וקריאה למשתנה קוראת למתודה. כך C# מתייחסת לפונקציות כערכים: אפשר לשמור אותן, להעביר אותן למתודות אחרות ולקרוא להן מאוחר יותר.
פלט:
40
70
68.00
delegate decimal PriceRule(decimal price); מצהירה על טיפוס חדש, באותה דרך ש-class עושה. כל מתודה שמקבלת decimal אחד ומחזירה decimal מתאימה לו. המשתנה rule הצביע על שלוש מתודות שונות בזו אחר זו, וכל קריאה הריצה את המתודה שהוא החזיק באותו רגע.
הצהרה, יצירה והפעלה
הצהרה על טיפוס delegate נראית כמו חתימה של מתודה עם מילת המפתח delegate לפניה. היא יושבת בדרך כלל ברמת ה-namespace, לצד מחלקות, או מקוננת בתוך מחלקה.
יש ארבע דרכים ליצור מופע של 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). הקומפיילר בודק את החתימה בזמן ההצבה, כך שמתודה לא תואמת היא שגיאת קומפילציה ולא הפתעה בזמן ריצה.
delegate יכול להצביע גם על מתודת מופע. אז הוא זוכר גם את המתודה וגם את האובייקט שעליו צריך לקרוא לה:
פלט:
Ana got: Your order shipped
target: Mailbox, method: Receive
Target הוא האובייקט שעליו ה-delegate יקרא למתודה (null למתודה סטטית), ו-Method מתאר את המתודה. החזקת ה-target פירושה גם ש-delegate שומר את האובייקט הזה בחיים, וזה חשוב ב-events.
Delegates כ-callbacks
התפקיד העיקרי של delegate הוא לאפשר למתודה לקרוא לקוד שהקוד הקורא שלה בחר. המתודה מגדירה מתי משהו קורה; הקוד הקורא מגדיר מה:
פלט:
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
ImportRows לא יודעת כלום על קונסולות או על פסים. אותה מתודה יכולה להניע פס התקדמות, שורת לוג או עדכון ממשק, לפי מה שהקוד הקורא מעביר. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) וכל אופרטור של LINQ עוקבים אחרי התבנית הזו.
Multicast delegates: += ו--=
delegate יכול להחזיק יותר ממתודה אחת. += מוסיף מתודה לסוף רשימת ההפעלה שלו, -= מסיר אותה, והפעלת ה-delegate קוראת לכל המתודות לפי הסדר:
פלט:
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
שלושה פרטים של multicast delegates חשובים בפועל:
- ערכי החזרה: אם טיפוס ה-delegate מחזיר ערך, הפעלה של multicast delegate מחזירה רק את התוצאה של המתודה האחרונה. כדי לאסוף את כל התוצאות, עברו בלולאה על
GetInvocationList()והפעילו כל אחת בעצמכם. - חריגות: אם מתודה אחת זורקת, המתודות הנותרות לא רצות והחריגה מגיעה לקוד הקורא.
- אי-שינוי: delegates הם בלתי ניתנים לשינוי.
+=יוצר delegate חדש עם רשימה ארוכה יותר ומציב אותו בחזרה למשתנה, ולכן הסרה עם-=על עותק לא משפיעה על המקור.
הסרה של lambda עם -= עובדת רק אם מעבירים את אותו מופע delegate. כתיבה של אותה lambda פעמיים יוצרת שני delegates שונים, כך ש-handlers -= id => Log(id); לא מסיר כלום. שמרו את ה-lambda במשתנה כשאתם מתכננים לבטל את הרישום שלה.
Delegates שהם null ו-?.Invoke
משתנה delegate שלא הוצב לו כלום הוא null, וגם הסרת המתודה האחרונה עם -= משאירה null. הפעלה של null זורקת NullReferenceException:
פלט:
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke היא הדרך המקובלת לקרוא ל-callback אופציונלי. אי אפשר לשים את האופרטור ?. ישירות לפני הסוגריים (onSaved?("x") אינו תחביר תקין), ולכן התבנית קוראת למתודה Invoke של ה-delegate בשמה, וזה עושה את אותו הדבר כמו הקריאה המקוצרת.
Delegates, Func, Action ו-events
הצהרה על טיפוס delegate לכל חתימה הייתה מייגעת, ולכן .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 מומרת לכל טיפוס delegate תואם, ולכן APIs בדרך כלל מקבלים lambdas והשאלה אף פעם לא עולה. הדף על Func ו-Action מכסה את המשפחה הגנרית ומתי הצהרה מותאמת עדיין שווה את זה.
Events בנויים ישירות על delegates. שדה event הוא multicast delegate שקוד חיצוני רשאי רק לעשות לו += ו--=, אף פעם לא להפעיל אותו או לדרוס אותו. רשימת המנויים בדוגמת ה-multicast שלמעלה היא בדיוק מה ש-event מנהל בשבילכם, עם ההגנה הנוספת הזו. ל-events יש דף משלהם.
שאלות נפוצות
מהו delegate ב-C#?
delegate הוא הפניה בטוחה-טיפוסית למתודה. מצהירים על טיפוס delegate עם חתימה, למשל delegate decimal PriceRule(decimal price);, וכל מתודה עם החתימה הזו (סטטית, של מופע, או lambda) יכולה להישמר במשתנה מהטיפוס הזה ולהיקרא דרכו מאוחר יותר. delegates הם הדרך של C# להעביר פונקציות כערכים.
מה ההבדל בין delegate ל-Func ב-C#?
Func<T, TResult> ו-Action<T> הם טיפוסי delegate ש-.NET כבר מצהירה עליהם בשבילכם, גנריים לפי טיפוסי הפרמטרים וההחזרה. הצהרת delegate מותאמת נותנת לטיפוס שם ושמות פרמטרים שמתעדים את התפקיד שלו, ותומכת בפרמטרים מסוג ref, out ו-in. לרוב הקוד, Func ו-Action מספיקים.
מהו multicast delegate ב-C#?
delegate שמחזיק כמה מתודות. += מוסיף מתודה לרשימת ההפעלה שלו ו--= מסיר אחת; הפעלת ה-delegate קוראת לכולן לפי הסדר. אם ה-delegate מחזיר ערך, הקוד הקורא מקבל רק את התוצאה של המתודה האחרונה, וחריגה במתודה אחת עוצרת את ההרצה של השאר.
איך מפעילים בבטחה delegate שעלול להיות null?
השתמשו באופרטור ה-null-conditional: onProgress?.Invoke(50);. משתנה delegate שלא הוצבו לו מתודות הוא null, וקריאה ישירה אליו זורקת NullReferenceException. ?.Invoke קורא את המשתנה פעם אחת, ולכן הוא בטוח גם אם thread אחר מסיר את ה-handler האחרון באותו רגע.
מתי כדאי להצהיר על טיפוס delegate משלי במקום להשתמש ב-Func או ב-Action?
כשהחתימה צריכה פרמטרים מסוג ref, out או in (Func ו-Action לא יכולים לבטא אותם), כשטיפוס עם שם הופך API ציבורי לברור יותר (delegate bool Validator(string input)), או כשאתם רוצים ששמות הפרמטרים יופיעו ב-IntelliSense. אחרת העדיפו את Func ו-Action, שכל מפתח .NET מזהה.