מדובר במצביע לפונקציה, כלומר טיפוס שמאפשר להכניס פונקציה למשתנה.
אחד הדברים המגניבים שאפשר לעשות עם delegateים הוא ליצור פונקציות שמקבלות delegate ומשתמשות בו.
למשל, אנחנו יכולים לכתוב delegate כזה
1
publicdelegatebool Predicate<T>(T item);
זהו delegate שמקבל באופן איבר מטיפוס T ובודק אם חל עליו תנאי.
כעת נוכל לכתוב פונקציה כזאת:
1
2
3
4
5
6
7
8
9
10
11
12
13
publicstatic T First<T>(IEnumerable<T> enumerable,
Predicate<T> condition)
{
foreach (T currentItem in enumerable)
{
if (condition(currentItem))
{
return currentItem;
}
}
returndefault(T);
}
זו פונקציה שמקבלת אוסף של איברים ופרדיקט, ומוצאת את האיבר הראשון באוסף שחל עליו התנאי.
נוכל לקרוא לה לדוגמה כך:
ניצור פונקציה עם חתימה של הפרדיקט
1
2
3
4
publicstaticboolIsOdd(int number)
{
return (number%2 != 0);
}
ניצור אוסף:
1
int[] numbers = {4,8,15,16,23,42};
ונקרא לה:
1
Console.WriteLine(First(numbers, IsOdd)); // Prints 15
יפה, לא?
מה הבעיה?
הבעיה היא שכעת בשביל כל קריאה לפונקציה, נצטרך לכתוב פונקציה טיפשית כזאת (לפעמים לאו דווקא טיפשית, וכדאי לכתוב פונקציה כזאת אם היא מורכבת ובעלת לוגיקה מסובכת)
אז בC# 2.0 באו והחליטו שיאפשרו לנו לעשות משהו כזה בלי לכתוב פונקציה:
לזה קוראים Anonymous delegates.
נוכל לבצע בדיוק מה שעשינו קודם, רק כך:
1
2
3
Console.WriteLine(First(numbers,
delegate(int number)
{ return (number % 2 == 0); })); // Prints 15
למי שמעדיף, אפשר גם להכניס למשתנה קודם:
1
2
3
4
5
Predicate<int> isOdd =
delegate(int number)
{ return (number%2 == 0); };
Console.WriteLine(First(numbers, isOdd)); // Prints 15
יתרונות:
חסכנו הרבה מתודות קטנות ומיותרות
זה כלי מאוד חזק, ואנחנו נראה את זה בהמשך
חסרונות:
זה לא הכי קריא בעולם
ברגע שכתבנו דבר כזה במתודה, לא נוכל לשנות את המתודה שכתבנו את זה בה בזמן דיבוג (Edit and continue)
הסיבה לכך היא שיש ב ```List``` פונקציות שמקבלות ארגומנט מסוג ```T```, ולכן אם הירושה תתקיים נוכל לעשות משהו כזה:
```csharp
List<Shape> shapeList = new List<Circle>();
shapeList.Add(new Triangle());
אם נגביל את עצמנו ונאפשר רק פונקציות שמחזירות output שתלוי איכשהו בT, ולא פונקציות שמקבלות T, לא נתקל בבעיה הזאת ודומות.
עם זאת, לא רצו שההגבלות של הטיפוסים הגנריים יהיו ברמת הType, שהרי מאחורי הקלעים חייבים להיות setterים ופונקציות אחרות שמקבלות ארגומנט T ועושות עליו מניפולציות.
החליטו לאפשר לנו לקבל covariance, אבל רק ברמת interfaceים.
נוכל למשל להגדיר ממשק כזה:
1
2
3
4
5
6
7
public interface IContainer<T>
{
T Value
{
get;
}
}
אם ננסה לעשות הצבה כזאת נקבל שגיאה בזמן קימפול:
1
2
3
4
IContainer<Shape> shapeContainer;
IContainer<Circle> circleContainer;
shapeContainer = circleContainer;
Cannot implicitly convert type ‘IContainer’ to ‘IContainer’. An explicit conversion exists (are you missing a cast?)
נוכל לתקן את זה עם ציון המילה out
1
2
3
4
5
6
7
public interface IContainer<out T>
{
T Value
{
get;
}
}
כעת קטע הקוד יתקמפל.
בout נתקלנו אתמול. היא ציינה covariance, והכריחה שהטיפוס הגנרי יכול להופיע רק בערך החזר של פונקציה.
באופן דומה, גם בממשקים ברגע שנציין את out, לא נוכל להשתמש בטיפוס הגנרי בתור ארגומנט של פונקציה (או לחלופין setter):
1
2
3
4
5
6
7
8
public interface IContainer<out T>
{
T Value
{
get;
set;
}
}
Invalid variance: The type parameter ‘T’ must be invariantly valid on ‘IContainer.Value’. ‘T’ is covariant
בעקבות השינוי הזה, בframework 4.0 שינו מספר ממשקים כך שיתמכו בcovariance, ביניהם IEnumerable<out T> האהוב עלינו!
1
2
3
4
IEnumerable<Shape> shapes;
IEnumerable<Circle> circles = new List<Circle>();
shapes = circles;
מתקמפל.
באופן דומה, בכדי לתמוך בcotravariance של טיפוס גנרי, נתקל בבעיה אם לא נגביל את הטיפוס הגנרי רק לארגומנט. אחרת קטע הקוד הבא, למשל, יפעל:
1
2
3
4
5
6
7
8
9
public interface ICloner<T>
{
T Clone(T value);
}
ICloner<Shape> shapeCloner;
ICloner<Circle> circleCloner;
circleCloner = shapeCloner;
אבל אף אחד לא יכול להבטיח לנו שפונקציית הclone של ICloner<Shape> תחזיר לנו Circle כאשר נכניס לה Circle.
נוכל, מצד שני לעשות משהו כזה:
1
2
3
4
public interface ICloner<in T>
{
objectClone(T value);
}
ואז השורות הנ"ל מתקמפלות.
אכן, ניתן להכניס לפונקציית ה Clone של הshapeCloner גם Circle שהרי גם הוא Shape.
הin מציין באופן אנלוגי לoutשאנחנו מעוניינים בcontravariance, ולכן יכולים לקבל את הטיפוס הגנרי רק בתור ארגומנט של פונקציות של הממשק.
גם כאן שיפרו כמה ממשקים בframework, למשל IComparer<in T>. נוכל למשל לכתוב משהו כזה:
1
2
IComparer<Shape> shapeComparer;
IComparer<Circle> circleComparer = shapeComparer;
שהרי אובייקט שיודע להשוות בין כל שתי צורות, יודע בפרט גם להשוות שני מעגלים.
בהמשך לטיפ היומי של אתמול, נניח שיש לנו delegate כזה
1
publicdelegatevoidCircleDrawer(Circle circle);
אבל אנחנו הגדלנו ראש, והצלחנו לממש פונקציית ציור של כל צורה:
1
2
3
4
publicstaticvoidDrawShape(Shape shape)
{
// ...
}
היינו רוצים שנוכל להשתמש בפונקציה של ציור צורה כללית DrawShape, כדי לצייר בפרט מעגל.
שוב, מצד אחד זה נראה כאילו אנחנו צריכים להחליף את החתימה של הפונקציה, כדי שתקבל Shape, או לחלופין ליצור פונקציה שקוראת לה עם החתימה של CircleDrawer, שכל ההבדל היא שהפונקציה מקבלת Circle במקום Shape.
גם כאן, בC# 2.0 חשבו על זה, ונתנו לנו תמיכה בהסבות כאלה ברמת הקוד:
1
2
CircleDrawer circleDrawer =
new CircleDrawer(DrawShape); // Compiles!
קצת על contravariance – אם אמרנו שהמשמעות של covariance היא כזאת:
פונקציה $ f $ של Typeים היא covariant אם לכל טיפוס $ B $ שיורש מטיפוס $ A $, מתקיים $ f(B) $ יורש מ$ f(A) $.
כתיב:
$ B : A \implies f(B) : f(A)$ אז המשמעות של Contravariance היא אנלוגית:
פונקציה f של Typeים היא contravariant היא אם לכל טיפוס $ B $ שיורש מטיפוס $ A $ , מתקיים $ f(A) $ יורש מ$ f(B) $:
$ B : A \implies f(A) : f(B)$
שימו לב להבדל הדק, שהיחס בין הירושה לא נשמר, אלא מתהפך.
היינו מצפים, למשל, בעקבות מה שרשמתי למעלה, שאם יש לנו את שני הdelegateים הבאים:
1
2
publicdelegatevoidShapeDrawer(Shape shape);
publicdelegatevoidCircleDrawer(Circle circle);
יתקיים משהו בסגנון
1
ShapeDrawer : CircleDrawer
כי ראינו שאפשר להכניס לתוך CircleDrawer מתודה עם חתימה של ShapeDrawer.
במדעי המחשב קיים מושג שנקרא covariance ומושג מקביל שנקרא contravariance.
הסבר פשוט של המושג הוא הבא:
נניח שיש לנו פונקציה $ f $ שפועלת על Typeים (מקבלת Type ומחזירה Type).
נאמר שהיא covariant אם לכל טיפוס $ B $ שיורש מטיפוס $ A $, מתקיים $ f(B) $ יורש מ$ f(A) $.
כתיב:
$ B : A \implies f(B) : f(A) $
לדוגמה,
נניח שיש לנו את הTypeים הבאים
1
2
3
publicclassShape
publicclassCircle : Shape
publicclassTriangle : Shape
טבעי לצפות שהפונקציה שמעבירה כל טיפוס $ T \mapsto \text{List}[T] $ תהיה covariant, שהרי היינו מצפים ש List<Circle> ירש מList<Shape>שהרי Circle יורש מShape.
אבל דבר זה גורר סתירה, כי אז נוכל לכתוב את השורות הבאות:
1
2
List<Shape> shapes = new List<Circle>();
shapes.Add(new Triangle());
מה הבעיה בשורות אלו? הכנסנו לרשימה של מעגלים, משולש!
מאחר ולא רצו שנחטוף exception בזמן ריצה, החליטו שList<T> לא יהיה covariant (ולא רק הוא), לכן השורה הראשונה של ההשמה, בכלל לא מתקמפלת.
הסיבה לכך היא שאחד הדברים שgenerics מעניקים לנו זה type safety, ומונעים מאיתנו לעשות שטויות כאלה כבר בזמן קימפול.
עם זאת, covariance כן ממומש בC#, ואפילו בC# 1.0:
אם ניקח את הדוגמה הקודמת ונשנה אותה קצת:
1
Shape[] shapes = new Circle[1];
השורה הזאת מתקמפלת. כלומר מתקיים ש $ T \mapsto T[] $ הוא covariant.
במילים אחרות
1
Circle[] : Shape[]
מה יקרה אם ננסה לעשות משהו כמו קודם?
1
2
Shape[] shapes = new Circle[1];
shapes[0] = new Triangle();
נקבל exception בזמן ריצה
"Attempted to access an element as a type incompatible with the array."
המסקנה היא שצריך מאוד להיזהר עם covariance של מערכים בC#.
הערה: Covariance של מערכים בC# עובד רק על Reference Types: למשל לא מתקיים
מדי פעם כשאנחנו כותבים קוד, אנחנו נתקלים בבעיה הנפוצה הבאה:
מדי פעם אנחנו כותבים פונקציה שמחזירה Value type, כגון int, long, bool וכו’.
לפעמים אנחנו רוצים להחזיר ערך המעיד על כך שהתבצעה שגיאה (קלט לא תקין למשל).
אפשרות אחת היא לזרוק exception, אבל זו אפשרות פחות מוצלחת, מאחר וexceptionים אינם אמורים להיות חלק מהflow של האפליקציה.
אפשרות שנייה היא להחזיר ערך “לא תקין” המעיד על כך שקיבלנו ערך לא תקין.
למשל, הפונקציה IndexOf של string מחזירה ערך -1 כאשר לא נמצא אינדקס מתאים לערך המבוקש.
הבעיה עם הפתרון הזה היא שאין מוסכמה של מהו ערך תקין ומהו ערך לא תקין, כלומר מישהו יכול להחליט שהערך -1 מעיד על ערך החזר לא תקין, ומישהו אחר יכול להחליט שדווקא 0 מעיד על שגיאה.
היינו רוצים להחזיר ערך קבוע שמעיד על שגיאה. כאשר אנחנו עובדים עם Reference types אנחנו יכולים תמיד להחזיר null.
לצערנו (ולפעמים אנחנו גם מודים על כך), אנחנו לא יכולים להחזיר null כאשר ערך ההחזר הוא value type.
בframework 2.0 המציאו פתרון. שמו הוא Nullable types.
במקום לכתוב פונקציה כזאת:
1
2
3
4
5
6
7
8
9
10
publicstaticintIndexOf(string source,
stringvalue)
{
if (source.Length == 0)
{
return-1;
}
// Write other cases here.
}
נוכל להחליף את ערך ההחזר להיות Nullable<int> ולהחזיר null במקרה שקיבלנו ערך לא תקין:
1
2
3
4
5
6
7
8
9
10
publicstatic Nullable<int> IndexOf(string source,
stringvalue)
{
if (source.Length == 0)
{
returnnull;
}
// Write other cases here.
}
יש syntactic sugar שעשו בשפה, כדי שנוכל לכתוב במקום Nullable<int> פשוט int?:
1
2
3
4
5
6
7
8
9
10
publicstaticint? IndexOf(string source,
stringvalue)
{
if (source.Length == 0)
{
returnnull;
}
// Write other cases here.
}
קריאה לפונקציה תראה כעת כך:
1
2
3
4
5
6
int? index = IndexOf("Banana", "nana");
if (index != null)
{
// ...
}
או כך:
1
2
3
4
5
6
int? index = IndexOf("Banana", "nana");
if (index.HasValue)
{
// ...
}
במקום כך:
1
2
3
4
5
6
int index = IndexOf("Banana", "nana");
if (index != -1)
{
// ...
}
נסביר קצת על Nullable<T>:
זהו value type (struct) שעוטף value typeים אחרים. יש implicit cast מT הנעטף לNullable<T>.
כלומר נוכל לכתוב קוד מהסגנון
1
2
bool? found = true;
int? number = 4;
בנוסף הוא "יכול לקבל" ערך null.
למה "יכול לקבל" בגרשיים? באופן כללי value typeים לא יכולים לקבל null.
כשאנחנו כותבים שורה כזאת:
1
bool? found = null;
הקומפיילר מקמפל אותה למשהו כזה:
1
2
3
4
L_0001: ldloca.s found
L_0003: initobj [mscorlib]System.Nullable`1
L_0009: ldloc.0
L_000a: stloc.1
שזה שקול לכתיבה:
1
Nullable<bool> found = new Nullable<bool>();
לכן nullable types כמו כל value type לא באמת מקבלים את הערך null.
לכן נוכל לגשת ללא חשש לProperty שנקרא HasValue.
עם זאת, השוואה לnull והצבת null מתרגמים מאחורי הקלעים לפעולות על הProperty HasValue, ולכן נוכל לכתוב בצורה האינטואיטיבית שהזכרתי מעלה.
כאשר אנחנו כותבים מתודה המחזירה Type גנרי, לפעמים קורה שאנחנו רוצים להחזיר null.
הבעיה היא שאז לא נוכל להחזיר null במידה וT הוא value type.
לכן היינו רוצים להחזיר null במידה ומדובר בReference type.
במידה וT הוא value type כנראה נרצה להחזיר משהו כמו 0, false או הערך הדיפולטי של הType – בד”כ מדובר בType כשכל הערכים בו מאותחלים בערך הדיפולטי שלהם, כאשר הוא לtypeים הנומריים הערך הדיפולטי הוא 0.
יש תמיכה בדבר כזה, באמצעות הkeyword שנקרא default:
באופן כללי כל member שלא אתחלנו אותו בעצמנו, יאותחל בערך default() של הType שלו:
1
privateint m_Member = default(int);
שקולה לשורה
1
privateint m_Member = 0;
או לשורה
1
privateint m_Member;
באופן דומה זה נכון גם לכל Type ואין משהו מיוחד בint.
עוד דוגמה:
השורות הבאות שקולות
1
2
3
privatestring m_Member = default(string);
privatestring m_Member = null;
privatestring m_Member;
string הוא לא value type ולכן מאותחל בnull.
גם אם המשתנה גנרי קורה אותו הדבר והשורות הבאות שקולות:
1
2
private T m_Member = default(T);
private T m_Member;
בכל מקרה נוכל גם להשתמש בזה גם במתודות גנריות, למשל המתודה שראינו פעם: