41. Anonymous delegates

אחת ההמצאות היפות בC# היא הdelegateים.

מדובר במצביע לפונקציה, כלומר טיפוס שמאפשר להכניס פונקציה למשתנה.

אחד הדברים המגניבים שאפשר לעשות עם delegateים הוא ליצור פונקציות שמקבלות delegate ומשתמשות בו.

למשל, אנחנו יכולים לכתוב delegate כזה

1
public delegate bool Predicate<T>(T item);

זהו delegate שמקבל באופן איבר מטיפוס T ובודק אם חל עליו תנאי.

כעת נוכל לכתוב פונקציה כזאת:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static T First<T>(IEnumerable<T> enumerable,
Predicate<T> condition)
{
foreach (T currentItem in enumerable)
{
if (condition(currentItem))
{
return currentItem;
}
}
return default(T);
}

זו פונקציה שמקבלת אוסף של איברים ופרדיקט, ומוצאת את האיבר הראשון באוסף שחל עליו התנאי.

נוכל לקרוא לה לדוגמה כך:

ניצור פונקציה עם חתימה של הפרדיקט

1
2
3
4
public static bool IsOdd(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

יתרונות:

  1. חסכנו הרבה מתודות קטנות ומיותרות
  2. זה כלי מאוד חזק, ואנחנו נראה את זה בהמשך

חסרונות:

  1. זה לא הכי קריא בעולם
  2. ברגע שכתבנו דבר כזה במתודה, לא נוכל לשנות את המתודה שכתבנו את זה בה בזמן דיבוג (Edit and continue)

שבוע אנונימי טוב

שתף

40. Interface covariance and contravariance

כפי שראינו במהלך השבוע האחרון, covaraince וcontravariance הם מושגים שהם די בעיתיים.

ראינו שעוד בC# 1.0 מערכים הם covariant, ושבC# 4.0 אפשר לעשות delegateים גנריים שהם covariant/contravariant בטיפוס הגנרי.

טעות נפוצה היא לחשוב שבC# 4.0 יש תמיכה בcovariance לכל הטיפוסים.

זה בכלל לא נכון.

למען האמת, פרט לdelegateים הגנריים, אין אף מחלקה שתומכת בcovariance וcontravariance מלא, אפילו בC# 4.0.

עם זאת, אכן הוסיפו תמיכה בcovariance וcontravariance בשפה, והיא באמצעות interfaceים.


ראינו ביום ראשון שאם לכל

: B``` יתקיים למשל ```List : List;``` זה יגרום לבעיות בהכרח בזמן ריצה.
1
2
3
4
5
6
הסיבה לכך היא שיש ב ```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>
{
object Clone(T value);
}

ואז השורות הנ"ל מתקמפלות.

אכן, ניתן להכניס לפונקציית ה Clone של הshapeCloner גם Circle שהרי גם הוא Shape.

הin מציין באופן אנלוגי לoutשאנחנו מעוניינים בcontravariance, ולכן יכולים לקבל את הטיפוס הגנרי רק בתור ארגומנט של פונקציות של הממשק.

גם כאן שיפרו כמה ממשקים בframework, למשל IComparer<in T>. נוכל למשל לכתוב משהו כזה:

1
2
IComparer<Shape> shapeComparer;
IComparer<Circle> circleComparer = shapeComparer;

שהרי אובייקט שיודע להשוות בין כל שתי צורות, יודע בפרט גם להשוות שני מעגלים.

סופ"ש אין-וואריאנטי מצוין

שתף

39. Delegate covariance and contravariance

מארועי הפרקים הקודמים:

ראינו כי אם יש לנו את שני הdelegateים הבאים

1
2
public delegate void ShapeDrawer(Shape shape);
public delegate void CircleDrawer(Circle circle);

קיים איזשהו implicit cast בין מתודה של הסוג הראשון לשני. עם זאת, אין ירושה אמיתית, והבאנו דוגמה לקטע קוד שלא מתקמפל:

קיימת מתודה:

1
public static void DrawShape(Shape shape)

וראינו את הדוגמה הזאת:

1
2
3
4
5
6
7
ShapeDrawer shapeDrawer =
new ShapeDrawer(DrawShape); // Compiles
CircleDrawer circleDrawer =
new CircleDrawer(DrawShape); // Compiles!
circleDrawer = shapeDrawer; // Doesn't compile

כלומר ShapeDrawer הוא לא CircleDrawer למרות שפעם שעברה הסברנו למה הגיוני שכן.

באופן דומה הייתה לנו דוגמה של covariance שתפקדה באופן דומה:

1
2
public delegate Shape ShapeBuilder(int x, int y);
public delegate Circle CircleBuilder(int x, int y);

הייתה את המתודה הזאת:

1
public static Circle BuildCircle(int x, int y)

וראינו את קטע הקוד הזה:

1
2
3
4
5
6
7
ShapeBuilder builder =
new ShapeBuilder(BuildCircle); // Compiles
CircleBuilder circleBuilder =
new CircleBuilder(BuildCircle); // Still compiles
builder = circleBuilder; // Doesn't compile

שגם אז סיכמנו שהגיוני שהגיוני שCircleBuilder יהיה ShapeBuilder.

עד כאן מארועי הפרקים הקודמים.


בC# 4.0 החליטו שפותרים את הבעיה הזאת.

לאחר שלא נגעו בCLR מאז Framework 2.0 החליטו לגעת בו ולפתור את הבעיה.

איך זה עובד?

בדוגמה הראשונה במקום שני הdelegateים שהגדרנו

1
2
public delegate void ShapeDrawer(Shape shape);
public delegate void CircleDrawer(Circle circle);

נגדיר delegate כזה:

1
2
public delegate void ShapeDrawer<TShape>(TShape shape)
where TShape : Shape;

את זה יכולנו לעשות בC# 2.0. אז השורות שלנו הופכות להיות

1
2
3
4
5
6
7
ShapeDrawer<Shape> shapeDrawer =
new ShapeDrawer<Shape>(DrawShape); // Compiles
ShapeDrawer<Circle> circleDrawer =
new ShapeDrawer<Circle>(DrawShape); // Compiles!
circleDrawer = shapeDrawer; // Doesn't compile

ועדיין זה לא מתקמפל לנו. בשביל זה המציאו לנו keyword חדש ששמו in. הוא מציין שאנחנו רוצים שיתקיים Contravariance בטיפוס הגנריים:

1
2
public delegate void ShapeDrawer<in TShape>(TShape shape)
where TShape : Shape;

ועכשיו סוף סוף קטע הקוד הזה יתקמפל!

1
2
3
4
5
6
7
ShapeDrawer<Shape> shapeDrawer =
new ShapeDrawer<Shape>(DrawShape); // Compiles
ShapeDrawer<Circle> circleDrawer =
new ShapeDrawer<Circle>(DrawShape); // Compiles!
circleDrawer = shapeDrawer; // finally compiles!

שימו לב שבעצם מתקיים ש

$ T \mapsto \text{ShapeDrawer[T]}$

הוא contravariant כפי שציפינו בפעם הקודמת.

באופן אנלוגי נוכל לגרום לדוגמה השנייה לעבוד:

במקום שני הdelegateים,

1
2
public delegate Shape ShapeBuilder(int x, int y);
public delegate Circle CircleBuilder(int x, int y);

נוכל לכתוב

1
2
public delegate TShape ShapeBuilder<TShape>(int x, int y)
where TShape : Shape;

הקוד נהפך לזה

1
2
3
4
5
6
7
ShapeBuilder<Shape> builder =
new ShapeBuilder<Shape>(BuildCircle); // Compiles
ShapeBuilder<Circle> circleBuilder =
new ShapeBuilder<Circle>(BuildCircle); // Still compiles
builder = circleBuilder; // Doesn't compile

עדיין לא מתקמפל.

אבל נוכל לציין שאנחנו מעוניינים בcovariance ע"י המילה השמורה out:

1
2
public delegate TShape ShapeBuilder<out TShape>(int x, int y)
where TShape : Shape;

אז קטע הקוד כן יתקמפל:

1
2
3
4
5
6
7
ShapeBuilder<Shape> builder =
new ShapeBuilder<Shape>(BuildCircle); // Compiles
ShapeBuilder<Circle> circleBuilder =
new ShapeBuilder<Circle>(BuildCircle); // Still compiles
builder = circleBuilder; // Сompiles

ושוב, בעצם מתקיים ש

$ T \mapsto \text{ShapeBuilder[T]} $

הוא covariant כפי שציפינו בעבר.


למה בחרו דווקא במילים השמורות in וout?

הסיבה היא המילים האלה מציינות שאנחנו רק יכולים לקבל/להחזיר אובייקט מסוג גנרי המצוין.

לדוגמה, קטע הקוד הבא לא יתקמפל:

1
2
public delegate TShape InvariantDelegate<in TShape>(TShape shape)
where TShape : Shape;

Invalid variance: The type parameter ‘TShape’ must be covariantly valid on ‘InvariantDelegate.Invoke(TShape)’. ‘TShape’ is contravariant.

הסיבה היא שאם הוא היה מתקמפל, היינו יכולים לעשות שטויות כאלה:

1
2
3
4
public static Shape ShapeMethod(Shape shape)
InvariantDelegate<Circle> method =
new InvariantDelegate<Shape>(ShapeMethod);

אבל מי אמר שShapeMethod מחזירה בכלל Circle? בעיה.

באופן דומה נוכל לעשות שטויות עם covariance.

לכן כשאנחנו מציינים על הטיפוס הגנרי אם הוא covariance או contravariance אנחנו מגבילים אותו, ולכן כל הדברים האלה אינם אוטומטיים.

יום קו/קונטרה וואריאנטי טוב

שתף

38. Method group contravariance

בהמשך לטיפ היומי של אתמול, נניח שיש לנו delegate כזה

1
public delegate void CircleDrawer(Circle circle);

אבל אנחנו הגדלנו ראש, והצלחנו לממש פונקציית ציור של כל צורה:

1
2
3
4
public static void DrawShape(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
public delegate void ShapeDrawer(Shape shape);
public delegate void CircleDrawer(Circle circle);

יתקיים משהו בסגנון

1
ShapeDrawer : CircleDrawer

כי ראינו שאפשר להכניס לתוך CircleDrawer מתודה עם חתימה של ShapeDrawer.

בC# 2.0 אין תמיכה בזה, ולמשל הקוד הבא מתקמפל:

(הפונקציה היא כמקודם)

1
2
3
4
5
6
7
8
9
public static void DrawShape(Shape shape)
ShapeDrawer shapeDrawer =
new ShapeDrawer(DrawShape); // Compiles
CircleDrawer circleDrawer =
new CircleDrawer(DrawShape); // Compiles!
circleDrawer = shapeDrawer; // Doesn't compile

יום קונטרה וואריאנטי מצוין

שתף

37. Method group covariance

בהמשך לטיפ של אתמול,

נניח שיש לנו מחלקה כזאת שאחראית לבנות צורה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ShapeBuilder
{
private Builder m_Builder;
public Builder Builder
{
get
{
return m_Builder;
}
set
{
m_Builder = value;
}
}
}

כשהdelegate הוא מהסוג הזה ומוזרק מבחוץ (ככה החליט יוצר המחלקה)

1
public delegate Shape Builder(int x, int y);

וכתבנו כבר מתודה כזאת:

1
2
3
4
public static Circle BuildCircle(int x, int y)
{
//
}

כעת אנחנו רוצים להשתמש בה בShapeBuilder.

עם זאת, נראה כי אנחנו לא יכולים להשתמש בה מאחר והdelegate מחזיר Shape, והמתודה שלנו מחזירה Circle.

לכן נראה שיש לנו שתי ברירות:

  1. ליצור עוד מתודה עם חתימה שמחזירה Shape שתקרא למתודה שלנו
  2. להחליף את החתימה של המתודה שלנו.

אם נחשוב על זה עוד קצת, הגיוני שדווקא כן נוכל להכניס את המתודה לShapeBuilder, כי Circle הוא Shape.

מסתבר שבC# 2.0 הכניסו לנו תמיכה בזה, ובאמת השורות הבאה תתקמפלנה לנו!

1
2
ShapeBuilder builder = new ShapeBuilder();
builder.Builder = new Builder(BuildCircle);

אם נחזור למושגים של אתמול, אומרים שהסבה של method group (יענו מתודה) לdelegate היא covariant בערך ההחזר.

כלומר אם יש לנו

1
B : A

וחתימה של delegate שמחזירה A, נוכל להכניס לdelegate כזה מתודה עם אותה חתימה שמחזירה B במקום.

עם זאת, אין covariance ברמת הטיפוסים של הdelegate, כלומר אם יש לנו את השני הdelegateים

1
2
public delegate Shape Builder(int x, int y);
public delegate Circle CircleBuilder(int x, int y);

לא מתקיים

1
CircleBuilder : Builder

נמחיש זאת באמצעות דוגמת קוד:

1
2
3
4
5
6
7
Builder builder =
new Builder(BuildCircle); // Compiles
CircleBuilder circleBuilder =
new CircleBuilder(BuildCircle); // Still compiles
builder = circleBuilder; // Doesn't compile

המשך יום קו-וואריאנטי טוב

שתף

36. Array covariance

היי,

במדעי המחשב קיים מושג שנקרא covariance ומושג מקביל שנקרא contravariance.

הסבר פשוט של המושג הוא הבא:

נניח שיש לנו פונקציה $ f $ שפועלת על Typeים (מקבלת Type ומחזירה Type).

נאמר שהיא covariant אם לכל טיפוס $ B $ שיורש מטיפוס $ A $, מתקיים $ f(B) $ יורש מ$ f(A) $.

כתיב:

$ B : A \implies f(B) : f(A) $

לדוגמה,

נניח שיש לנו את הTypeים הבאים

1
2
3
public class Shape
public class Circle : Shape
public class Triangle : 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: למשל לא מתקיים

1
int[] : object[]

שבוע מעולה

שתף

35. Nullabe 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
public static int IndexOf(string source,
string value)
{
if (source.Length == 0)
{
return -1;
}
// Write other cases here.
}

נוכל להחליף את ערך ההחזר להיות Nullable<int> ולהחזיר null במקרה שקיבלנו ערך לא תקין:

1
2
3
4
5
6
7
8
9
10
public static Nullable<int> IndexOf(string source,
string value)
{
if (source.Length == 0)
{
return null;
}
// Write other cases here.
}

יש syntactic sugar שעשו בשפה, כדי שנוכל לכתוב במקום Nullable<int> פשוט int?:

1
2
3
4
5
6
7
8
9
10
public static int? IndexOf(string source,
string value)
{
if (source.Length == 0)
{
return null;
}
// 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, ולכן נוכל לכתוב בצורה האינטואיטיבית שהזכרתי מעלה.

סופ"ש כבר לא גנרי מצוין

שתף

34. Naked type constraint

זהו כנראה ההסבר האחרון על constraintים בgenerics.

(אנחנו עוד נראה ונשתמש בהם הרבה, אבל יהיה מדובר בטריקים ולא בהסברים על הFeatureים)

נניח שיש לנו מתודה גנרית עם מספר Typeים גנריים.

נוכל לכתוב בconstraint איזושהי יחס בין הטיפוסים הגנריים:

למשל נניח שיש לנו מתודה שמוצאת מקסימום, באמצעות IComparer (זהו ממשק שמאפשר לנו לעשות Compare בלי לממש ICompareable):

נוכל במקום להעביר את הIComparer לפונקציה לקבל את הסוג שלו וליצור אותו במתודה.

דוגמת קוד:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static T Max<T, TComparer>(T[] array)
where TComparer : IComparer<T>, new()
{
if (array.Length == 0)
{
throw new ArgumentException("Array was empty",
"array");
}
else
{
TComparer comparer = new TComparer();
T currentMax = array[0];
foreach (T element in array)
{
if (comparer.Compare(element, currentMax) > 0)
{
currentMax = element;
}
}
return currentMax;
}
}

שימו לב לconsraint where TComparer : IComparer<T>, new(), בעצם יש לנו כאן constraint שמקשר בין שני הTypeים שמגיעים למתודה.

לT קוראים naked type constraint.

קריאה למתודה היא כבר לא implicity אלא explicity:

1
2
3
4
public class MyComparer : IComparer<string>
string[] fruits = { "Apple", "Banana", "Orange", "Mango" };
string bestFruit = Max<string, MyComparer>(fruits);

באופן דומה אפשר לעשות דברים כאלה:

1
public static U Copy<T, U>(T input) where U : T

כלומר להכריח את אחד הTypeים להיות Base class של השני.

יום גנרי טוב

שתף

33. new() constraint

ראינו בפעם הקודמת שיש דרך להחזיר את הערך הדיפולטי של Type גנרי.

לפעמים אנחנו רוצים לעשות יותר מזה – ממש לאתחל משתנה מהType הפנימי.

למשל, נניח שיש לנו ממשק של נקודה במרחב

1
2
3
4
5
6
public interface IPoint
{
double X { get; set; }
double Y { get; set; }
double Z { get; set; }
}

ויש לנו אוסף של נקודות (זה יכול היה להיות למשל קוביה או תיבה).

היינו רוצים שתהיה לנו פונקציית Add כדלהלן

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PointCollection<TPoint> : List<TPoint>
where TPoint : IPoint
{
public void Add(double x, double y, double z)
{
TPoint point = new TPoint();
point.X = x;
point.Y = y;
point.Z = z;
this.Add(point);
}
}

אבל לצערנו זה לא מתקמפל.

הבעיה היא שלא בהכרח קיים Constructor דיפולטי לTPoint, ולכן זה לא מתקמפל.

המציאו בשבילנו משהו שפותר את הבעיה הזאת: נוכל להוסיף לConstraint את התנאי

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PointCollection<TPoint> : List<TPoint>
where TPoint : IPoint, new()
{
public void Add(double x, double y, double z)
{
TPoint point = new TPoint();
point.X = x;
point.Y = y;
point.Z = z;
this.Add(point);
}
}

זה אומר לקומפיילר שTPoint יכול לקבל רק Typeים שיש להם Constructor דיפולטי.

מספר הערות:

  1. הnew() חייב להופיע אחרון ברשימה של הConstraintים
  2. למרבה הצער המנגנון לא מספיק חזק כדי שנוכל לציין את הפרמטרים שאנחנו מעוניינים שיופיעו בConstructor, למשל היה יותר טבעי שהמחלקה והממשק יראו ככה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IPoint
{
double X { get; }
double Y { get; }
double Z { get; }
}
public class PointCollection<TPoint> : List<TPoint>
where TPoint : IPoint, new(double, double, double)
{
public void Add(double x, double y, double z)
{
this.Add(new TPoint(x, y, z));
}
}

הקטע קוד לעיל לא מתקמפל. הדבר מונע מאיתנו לשים readonly properties בinterfaceים.

ראו גם טיפים 322-323 המסבירים מה קורה מאחורי הקלעים כשאנחנו קוראים לnew על פרמטר גנרי.

המשך יום גנרי טוב

שתף

32. default(T) keyword

כאשר אנחנו כותבים מתודה המחזירה Type גנרי, לפעמים קורה שאנחנו רוצים להחזיר null.

הבעיה היא שאז לא נוכל להחזיר null במידה וT הוא value type.

לכן היינו רוצים להחזיר null במידה ומדובר בReference type.

במידה וT הוא value type כנראה נרצה להחזיר משהו כמו 0, false או הערך הדיפולטי של הType – בד”כ מדובר בType כשכל הערכים בו מאותחלים בערך הדיפולטי שלהם, כאשר הוא לtypeים הנומריים הערך הדיפולטי הוא 0.

יש תמיכה בדבר כזה, באמצעות הkeyword שנקרא default:

באופן כללי כל member שלא אתחלנו אותו בעצמנו, יאותחל בערך default() של הType שלו:

1
private int m_Member = default(int);

שקולה לשורה

1
private int m_Member = 0;

או לשורה

1
private int m_Member;

באופן דומה זה נכון גם לכל Type ואין משהו מיוחד בint.

עוד דוגמה:

השורות הבאות שקולות

1
2
3
private string m_Member = default(string);
private string m_Member = null;
private string m_Member;

string הוא לא value type ולכן מאותחל בnull.

גם אם המשתנה גנרי קורה אותו הדבר והשורות הבאות שקולות:

1
2
private T m_Member = default(T);
private T m_Member;

בכל מקרה נוכל גם להשתמש בזה גם במתודות גנריות, למשל המתודה שראינו פעם:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static T Max<T>(T[] array)
where T : IComparable
{
if (array.Length == 0)
{
throw new ArgumentException("Array was empty",
"array");
}
else
{
T currentMax = array[0];
foreach (T element in array)
{
if (element.CompareTo(currentMax) > 0)
{
currentMax = element;
}
}
return currentMax;
}
}

נוכל להחזיר במקרה של 0 פרמטרים את default(T):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static T Max<T>(T[] array)
where T : IComparable
{
if (array.Length == 0)
{
return default(T);
}
else
{
T currentMax = array[0];
foreach (T element in array)
{
if (element.CompareTo(currentMax) > 0)
{
currentMax = element;
}
}
return currentMax;
}
}

יום גנרי טוב

שתף