Covariance and Contravariance in C# 4.0

| More
נוסף ב-16/04/2010 13:33 על ידי דניאל כץ

גם אני כשנתקלתי לראשונה במושגים 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()));
     }
 }

Tags:

.NET 4.0

הוסף תגובה




biuquote
  • הערה
  • תצוגה מקדימה
Loading