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;

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

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

שתף