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 אנחנו מגבילים אותו, ולכן כל הדברים האלה אינם אוטומטיים.

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

שתף