83. Interface polymorphism pitfalls

ראינו פעם שעברה מהו explicit implementation (מימוש פרטי) של ממשק.

אחד הדברים הנחמדים שחשבתי שאפשר לעשות עם מימוש פרטי הוא דבר כזה:

נניח שיש לנו יצור שמסוגל לנוע בכל כיוון (צפון, דרום, מזרח ומערב), אז נוכל ליצור ממשק כזה:

1
2
3
4
5
public interface IMoveable
{
void MoveNext();
void MovePrevious();
}

כעת ניצור שני ממשקים שיורשים ממנו:

1
2
3
4
5
6
7
public interface IVerticalMoveable : IMoveable
{
}
public interface IHorizontalMoveable : IMoveable
{
}

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

1
2
3
public class Creature : IVerticalMoveable, IHorizontalMoveable
{
}

ונרצה לממש מימושים פרטיים של כל אחד מהממשקים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Creature : IVerticalMoveable, IHorizontalMoveable
{
void IVerticalMoveable.MoveNext()
{
}
void IVerticalMoveable.MovePrevious()
{
}
void IHorizontalMoveable.MoveNext()
{
}
void IHorizontalMoveable.MovePrevious()
{
}
}

אלא שנקבל למרבה הצער את השגיאות הבאות בזמן קימפול:

‘Creature’ does not implement interface member ‘IMoveable.MovePrevious()’
‘Creature’ does not implement interface member ‘IMoveable.MoveNext()’
‘IVerticalMoveable.MoveNext’ in explicit interface declaration is not a member of interface
‘IVerticalMoveable.MovePrevious’ in explicit interface declaration is not a member of interface
‘IHorizontalMoveable.MoveNext’ in explicit interface declaration is not a member of interface
‘IHorizontalMoveable.MovePrevious’ in explicit interface declaration is not a member of interface

למה זה קורה?

המתודות שיש בממשקים נמצאות בממשק הראשון (IMoveable). לכן הן שייכות לממשק הראשון. לכן כשנממש אותן בתור מימוש פרטי, נצטרך לציין שזהו מימוש של IMoveable.

בקיצור, מבאס.


עוד דוגמה:

הפעם דווקא לא מתחום הExplicit implementation.

נניח שיש לנו מחלקת אב שמממשת ממשק:

1
2
3
4
5
6
7
8
9
10
11
12
public interface IDrawable
{
void Draw();
}
public class Shape : IDrawable
{
public void Draw()
{
Console.WriteLine("A shape was drawn");
}
}

שימו לב שבמחלקת האב, הפונקציה Draw אינה וירטואלית.

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

1
2
3
4
5
6
7
public class Triangle : Shape
{
public void Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

אלא שכעת כשנקרא לDraw עם IDrawable על Triangle, נקבל את התוצאה הלא רצויה הבאה:

1
2
IDrawable shape = new Triangle();
shape.Draw(); // A shape was drawn. :(

מה שקורה זה תהליך מעט מורכב. המתודה Draw היא מתודה של ממשק ולכן מפוענחת בזמן ריצה, ולא בזמן קימפול.

מה שקורה זה בזמן שמקומפלת המחלקה Shape, מאחר והיא מממשת את IDrawable, מתבצע מיפוי של הפונקציה Draw של הממשק, למתודה שנמצאת אצל Shape.

ננסה אולי לעשות משהו כזה:

1
2
3
4
5
6
7
public class Triangle : Shape
{
void IDrawable.Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

אבל נקבל לצערנו את שגיאת הקימפול הבאה:

‘Triangle.IDrawable.Draw()’: containing type does not implement interface ‘IDrawable’

כלומר, מימוש explicit של ממשק, לא עובר בירושה.

אז ננסה עוד משהו:

1
2
3
4
5
6
7
public class Triangle : Shape, IDrawable
{
void IDrawable.Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

וכעת הקוד הבא יעבוד:

1
2
IDrawable shape = new Triangle();
shape.Draw(); // A triangle was drawn. :)

נראה שהבעיה שלנו נפתרה, אבל זה לא כך. אם נעשה משהו כזה:

1
2
Shape shape = new Triangle();
shape.Draw(); // A shape was drawn. :(

שהרי קריאה על instance של Shape, קוראת למתודה Draw של Shape (המתודה לא וירטואלית, ולכן הקריאה מפוענחת בזמן קימפול), ולכן לא פותר לנו את הבעיה, אלא רק מחליף לנו אותה בבעיה אחרת.

המסקנה היא:

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

המשך יום ממומשק טוב

שתף