319. Implementing the virtual method pattern in C# part 1

הכרנו בעבר את המושג של מתודות וירטואליות, ואפילו דיברנו על Multiple dynamic dispatch.

(ראו גם טיפים מספר 221-235)

בפעמים הקרובות אנחנו ננסה להבין איך מימשו (או איך היה אפשר לממש) פונקציות וירטואליות בC#.

כמובן, פונקציות וירטואליות כבר קיימות בC#, ולכן ניתן לעצמנו מספר הגבלות: לא נוכל להשתמש בפונקציות וירטואליות.

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

לכן נניח למעשה שאין לנו פונקציות של Instance, אלא רק פונקציות סטטיות.


אז מה אנחנו רוצים להשיג? נתבסס על הדוגמאות קוד מטיפ מספר 221:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class Shape
{
public virtual double GetPerimeter()
{
return 0;
}
public virtual double GetArea()
{
return 0;
}
}
public class RegularTriangle : Shape
{
public double Edge { get; set; }
public double GetPerimeter()
{
return Edge*3;
}
public override double GetArea()
{
return Edge*Edge*Math.Sqrt(3)/2;
}
}
public class Square : Shape
{
public double Edge { get; set; }
public override double GetArea()
{
return Edge*Edge;
}
public override double GetPerimeter()
{
return 4*Edge;
}
}

בכל מקרה, אנחנו רוצים קוד שיתנהג בצורה כזאת:

1
2
3
4
5
6
7
8
9
10
11
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter(); // 12
RegularTriangle triangle = new RegularTriangle(){Edge = 3};
triangle.GetPerimeter(); // 9
shape = triangle;
shape.GetPerimeter(); // 0 (GetPerimeter doesn't override..)

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


אז איך מממשים? נתחיל בכך שאין לנו פונקציות של Instance בכלל.

בואו ננסה להתגבר על הבעיה הזאת, ולראות איך אפשר לפתור אותה. יש לנו פונקציות סטטיות, ולכן ננסה לממש פונקציות של Instance בעזרת פונקציות סטטיות:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class Shape
{
public static double GetPerimeter(Shape _this)
{
return 0;
}
public static double GetArea(Shape _this)
{
return 0;
}
}
public class RegularTriangle : Shape
{
public double Edge { get; set; }
public static double GetPerimeter(RegularTriangle _this)
{
return _this.Edge*3;
}
public static double GetArea(RegularTriangle _this)
{
return _this.Edge*_this.Edge*Math.Sqrt(3)/2;
}
}
public class Square : Shape
{
public double Edge { get; set; }
public static double GetArea(Square _this)
{
return _this.Edge*_this.Edge;
}
public static double GetPerimeter(Square _this)
{
return 4*_this.Edge;
}
}

כעת כשיקרא הקוד הזה:

1
2
Square square = new Square() { Edge = 4 };
square.GetArea(); // 16

הקומפיילר החכם ידע לתרגם אותו לקריאה הזאת:

1
2
Square square = new Square() { Edge = 4 };
Square.GetArea(square); // 16

ואת הקוד הזה

1
2
Shape shape = new Square() { Edge = 3 };
shape.GetPerimeter();

הוא יתרגם לקוד הבא:

1
2
Shape shape = new Square() { Edge = 3 };
Shape.GetPerimeter(shape); // 0

למי מכם שתכנת בשפות כמו פייתון, בוודאי יצא להתקל בפונקציות של instance שמקבלות את this (או את self) בתור הפרמטר הראשון של הפונקציה.

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

אז איך אנחנו יכולים לפתור את הבעיה? בעזרת delegateים. מה שאנחנו יכולים לעשות זה לדאוג שכשאנחנו נקרא לפונקציה הסטטית, יקרא delegate שמצביע לפונקציה הסטטית שאנחנו באמת רוצים לקרוא לה. את הdelegate הזה אנחנו נחזיק בתור Member של הInstance שלנו.

את הניסיון לפתרון הזה נראה בפעם הבאה.

המשך יום וירטואלי טוב.

שתף