גם אני כשנתקלתי לראשונה במושגים covariance וcontravariance הייתי מבולבל למדי בעצמי. אבל, מסתבר שלאחר הסבר קצר אלו בסך הכל מונחים למושגים שכולנו מכירים טוב מאוד. מושגי הvariance שאולים במקור מהתפלגות סטטיסטית במתמטיקה, שם השונות המשותפת (variance) היא מדד לקשר בין שני משתנים מקריים. השונות המשותפת חיובית כאשר המשתנים נוטים לסטות באותו כיוון, מעל או מתחת לממוצע, ושלילית כאשר הם משתנים בכיוונים מנוגדים זה לזה. משם הושאל המינוח למדעי המחשב, וליתר דיוק למינוח האפיון של שפות תכנות.
נניח מערכת טיפוסים בה קיים הטיפוס Animal וטיפוס Elephant היורש ממנו:
class Program
{
public class Animal { }
public class Elephant : Animal { }
}
במערכת טיפוסים זו המרה מטיפוס אחד למשנהו נחשבת:
- covariant – כשממירים טיפוס כללי יותר לטיפוס מצומצם יותר לדוגמה: מAnimal לElephant.
- contravariant – כשממירים טיפוס מצומצם יותר לטיפוס כללי יותר לדוגמה: מElephant לAnimal.
- invariant – כשממירים בין שני טיפוסים שאינם קשורים לדוגמה: מProgram לAnimal.
למה זה טוב, ולמה לא קיבלנו את זה עד היום?
הסיפור התחיל בהחלטה של הצוות שבנה את הגרסה הראשונה של net. לעשות את המערכים covariant בשביל למשוך מתכנתי Java לעבור לnet. נו, אז למה בעצם זה כל כך רע?
כדי להדגים למה מערכים covariant זה דבר רע נכתוב את הדוגמה הבאה:
class Program
{
public class Animal { }
public class Elephant : Animal { }
static void Main(string[] args)
{
Animal[] animals = new Elephant[10];
animals[0] = new Animal();
}
}
הקוד מתקמפל נהדר והכל אמור להיות טוב. אבל, כשמריצים את התכנה נזרקת השגיאה:
Attempted to access an element as a type incompatible with the array
אז מה בעצם עשינו רע כאן?
-
הצבנו מערך של Elephant במערך של Animal וכאן הכל טוב, כי הרי לElephant יש את כל המאפיינים של Animal.
-
מצד שני, להגיד את ההפך לא יהיה נכון, כי לAnimal לא בהכרח יש את כל המאפיינים של Elephant.
-
השגיאה הזו נזרקת בגלל שהמערך הזה מורכב מאלמנטים מטיפוס Elephant אש בעצם זה מערך Elephant[].
קוד שמתקמפל אבל זורק שגיאה בזמן ריצה זה דבר רע מאוד. לכן, כשבMicrosoft תכננו את הטיפוסים הג'נריים הם לא רצו לחזור על הטעות שנית, ולכן עשו אותם invariant. ומהסיבה הזו, הקוד הבא לא יתקמפל:
class Program
{
public class Animal { }
public class Elephant : Animal { }
static void Main(string[] args)
{
List<Animal> Animals = new List<Animal>();
//זה יעבוד כי מובטח שלפיל יש את כל המאפיינים של בעל חיים
Animals.Add(new Elephant());
List<Elephant> Elephants = new List<Elephant>();
//זה לא יתקמפל
Elephants.Add(new Animal());
}
}
מותר לי להציב מרשימה של Animal מופע של Elephant כי מובטח שיש לו את כל המאפיינים שיש לAnimal אבל לא ניתן להציב מופע של Animal לרשימה של Elephant משום שלAnimal לא בהכרח יש את כל המאפיינים של Elephant. עכשיו הכל בטוח לשימוש!
אז פתרנו את בעיה, מה עוד רוצים?
מסתבר, שיצירת הטיפוסים הג'נריים כinveriant הפכה כמה מצבים שאמורים להיות חוקיים ללא חוקיים. לדוגמה הקוד הבא לא יעבוד בגרסאות הקודמות לnet 4.0.:
IList <Elephant> elephants = new List <Elephant>();
IEnumerable <Animal> animals = elephants;
זאת השגיאה שתקבלו:
Cannot implicitly convert type
'System.Collections.Generic.IList<BlogCodes.Elephant>' to
'System.Collections.Generic.IEnumerable<BlogCodes.Animal>'. An explicit conversion exists (are you missing a cast?)
בעקרון אין שום סיבה שהקוד הזה לא יעבוד, על ידי שימוש בIEnumerable<Animal> לא ניתן יותר להציב ערכים לרשימה ולכן הקוד בטוח לשימוש. בשביל לאפשר תסריט כזה ודומיו, Microsoft שינו את שיטת הvariance.
הפתרון – Covariance
הדוגמה הקודמת תעבוד בNET 4.0. בגלל שעכשיו הטיפוס IEnumerable מוגדר כך: IEnumerable<out T> מה שמאפשר לו לקבל טיפוסים יותר ספציפיים מאלו של הפרמטר הג'נרי T. הפרמטר out אומר למהדר שAnimal (בדוגמה שלנו) יכול להיות רק מוחזר על ידי המחלקה, מה שמרגיע את המהדר שאין שום דרך להוסיף ערכים לIEnumrable ובכך נמנעת הבעיה שתיארנו.
המונח לשימוש כזה הוא covariance, והוא מאפשר להתייחס לאובייקט כאל אובייקט שהוא יורש ממנו. לדוגמה IEnumerable<string> יכול להיות גם IEnumerable<object>. (יש לשים לב שמונחי variance מתייחסים רק לreference types ולא ישפיעו לדוגמה על int).
המונח Contravariance, אומר בדיוק את ההפך (בהרחבה בהמשך): זה מאפשר להתייחס לטיפוסים כמו Action<object> כאל Action<string>.
ניתן להוסיף את מילת המפתח out גם לממשקים (interfaces) ולשגרירים (delegates) ג'נריים שלך. בנוסף בNET 4.0. מילת מפתח זו נוספה לטיפוסים הבאים:
- IEnumerable<out T>
- IEnumerator<out T>
- IQueryable<out T>
Contravariance
Contravariance הוא ההפך מCovariance, הוא מאפשר להשתמש בטיפוס כללי יותר במקום טיפוס ספיציפי. כדי לקבוע Contravariance יש להשתמש במילת המפתח in. ניתן להוסיף את מילת המפתח int גם לממשקים ולשגרירים הגנריים שלך. בנוסף בNET 4.0. מילת מפתח זו נוספה לטיפוסים הבאים:
- IComparer<in T>
- IEqualityComparer<in T>
- Func<in T, … , out R>
- Action<in T, …>
- Predicate<in T>
- Comparison<in T>
- EventHandler<in T>
הדגמה לשימוש בContravariance
נאמר שאנו רוצים לכתוב מחלקה שתמיין בעלי חיים לפי משקלם, זה בוודאי יהיה מעלה אם נוכל לכתוב אותה כך שהיא תוכל לשקול לכל דבר שיורש מAnimal. כמובן שהדוגמה שלפנינו לא תפעל בVS2008:
class Program
{
public class Animal
{
public int Weight { get ; set ; }
public string Name { get ; set ; }
public Animal() { }
public Animal(string InputName, int InputWeight)
{
Name = InputName;
Weight = InputWeight;
}
}
public class Elephant : Animal
{
public Elephant(string InputName, int InputWeight)
: base (InputName, InputWeight)
{
}
}
public class WeightComparer : IComparer <Animal>
{
public int Compare(Animal x, Animal y)
{
if (x.Weight > y.Weight) return 1;
if (x.Weight == y.Weight) return 0;
return -1;
}
}
static void Main(string[] args)
{
WeightComparer objAnimalComparer = new WeightComparer ();
List <Animal> Animals = new List <Animal>();
Animals.Add(new Animal ("elephant" , 500));
Animals.Add(new Animal ("tiger" , 100));
Animals.Add(new Animal ("rat" , 5));
//יעבוד גם בגרסאות הקודמות
Animals.Sort(objAnimalComparer);
List <Elephant> Elephants = new List <Elephant>();
Elephants.Add(new Elephant ("Nellie" , 100));
Elephants.Add(new Elephant ("Dumbo" , 200));
Elephants.Add(new Elephant ("Baba" , 50));
//לא עובד בגרסות קודמות
Elephants.Sort(objAnimalComparer);
Elephants.ForEach(e => Console .WriteLine(e.Name + " " + e.Weight.ToString()));
}
}