330. Why encapsulation matters - part 1

אני רוצה לדבר על משהו ששמתי אליו שוב בפעם האחרונה.

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

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

בית ספר מורכב לצורך העניין משכבות, שכבות מורכבות מכיתות, וכיתות מורכבות ממחנכת ותלמידים.

פעולות שאפשר לעשות על בית ספר הם כאלה:

  • לגלות איזה שכבות יש בבית ספר
    • כיתות יש בשכבה מסוימת
      • איזה תלמידים יש בכיתה מסוימת
      • מי המחנכת של כיתה מסוימת
    • להוסיף כיתה לשכבה
      • להוסיף תלמיד לכיתה

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class School
{
public Dictionary<string, Dictionary<string, List<string>>> Students
{
get;
private set;
}
public Dictionary<string, Dictionary<string, string>> Teachers
{
get;
private set;
}
}

עכשיו, האם ענינו על כל הצרכים?

אולי, כי הרי אפשר לגלות מי התלמידים בכיתה מסוימת ע"י גישה לשכבה ואז לכיתה בDictionary של Students. באופן דומה, אפשר להוסיף או להסיר תלמיד מסוים לכיתה.

באופן שקול, אפשר גם לגלות גם מי המחנכת של כיתה מסוימת.

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

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

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

שבוע מוכמס לטובה.

שתף

329. Environment.GetCommandLineArgs

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

1
2
3
public static void Main(string[] args)
{
}

החתימה מקבלת את הארגומנטים שאיתם הפעילו את התכנית.

לפעמים אנחנו רוצים לגשת לארגומנטים אלה ממקום אחר. אופציה אחת היא להיסחב עם הארגומנטים ולהעבירם ממקום למקום ע"י שליחתם לפונקציות מתאימות.

מסתבר שיש דרך יותר פשוטה:

קיימת הפונקציה

1
Environment.GetCommandLineArgs()

המאפשרת לנו לגשת אליהם מכל מקום בתכנית! 😃

שימו לב שאלה לא אותם args שאנו רגילים אליהם מהMain. כאן נקבל בתור פרמטר ראשון את שם הExe שהפעילו, ובשאר הפרמטרים את הargs שאנחנו רגילים אליהם.

סוף שבוע עם ארגומנט מצוין!

שתף

328. About as and is+cast - part 3

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

1
Shape shape = obj as Shape;

על הקוד הזה:

1
2
3
4
5
6
Shape shape = null;
if (obj is Shape)
{
shape = (Shape) obj;
}

למרבה הצער, הדבר הזה לא כל כך עובד אם במקום Shape נשים Value Type, מאחר וValue Type אינו יכול לקבל ערך null.

עם זאת, היינו רוצים להנות מהיתרונות של הקוד הנ"ל גם עבור Value Types.

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

1
2
3
4
5
long? number = obj as long?;
if (number != null)
{
// ...
}

אנחנו משתמשים בNullable שנוכל להשתמש באופרטור as, ובמידה ואנחנו מצליחים, אנחנו נקבל גם את הערך האמיתי של האובייקט מוסב לטיפוס המתאים.

אם נסתכל בIL שנוצר, נראה את הקוד שראינו כבר:

1
2
3
4
L_0003: ldloc.0
L_0004: isinst [mscorlib]System.Nullable`1
L_0009: unbox.any [mscorlib]System.Nullable`1
L_000e: stloc.1

אנחנו רואים שבעצם מדובר בקוד שכבר ראינו, רק שיש פה גם Unboxing.

מסתבר אגב, שהקוד הזה פחות יעיל מהקוד השקול:

1
2
3
4
if (obj is long)
{
number = (long) obj;
}

הערה: הסדרה מבוססת על השאלה הבאה בStackOverflow.

המשך יום מוסב לטובה!

שתף

327. About as and is+cast - part 2

בהמשך לפעם הקודמת –

גם אם לא מעניין אתכם שהקוד הבא:

1
Shape shape = obj as Shape;

יותר מהיר מהקוד הבא:

1
2
3
4
5
6
Shape shape = null;
if (obj is Shape)
{
shape = (Shape) obj;
}

(ובצדק)

כדאי לדעת למה עדיף להעדיף את הקוד הראשון על פני השני.

הסיבה הראשונה היא קריאות: יותר קל לקרוא את האופרטור as, מאשר את הקוד מטה.

הסיבה השנייה, שהיא טיפה מפתיעה אפילו היא Thread safety:

אם obj הוא משתנה לוקאלי של פונקציה, אין הבדל בThread safety בין שתי הגישות. לעומת זאת, אם הוא Member של המחלקה, אז יש הבדל:

1
Shape shape = mObj as Shape;

לעומת

1
2
3
4
5
6
Shape shape = null;
if (mObj is Shape)
{
shape = (Shape) mObj;
}

מה ההבדל?

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

במימוש השני, לעומת זאת, יכול להיות שאנחנו נכנס לif ובינתיים בThread אחר ישנו לנו את הערך של המשתנה, ואז ההסבה תכשל!

זה אמנם מקרה נדיר (שמבצעים Cast לMember של המחלקה), אבל כדאי להכירו.

המשך יום מוסב לטובה טוב!

שתף

326. About as and is+cast

דיברנו כבר בעבר על האופרטורים ששמם as וis. (טיפ מספר 12)

אמרתי שכתיבה של as היא שקולה לבדיקה של הType המתאים באמצעות is ואז קריאה להסבה. עם זאת, אם נסתכל על הקוד IL המקומפל של שתי הקריאות:

1
2
3
4
5
6
Shape shape = null;
if (obj is Shape)
{
shape = (Shape) obj;
}

מתקמפל ל

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
L_0003: ldnull
L_0004: stloc.1
L_0005: ldloc.0
L_0006: isinst Shape
L_000b: ldnull
L_000c: cgt.un
L_000e: ldc.i4.0
L_000f: ceq
L_0011: stloc.2
L_0012: ldloc.2
L_0013: brtrue.s L_001e
L_0015: nop
L_0016: ldloc.0
L_0017: castclass Shape
L_001c: stloc.1
L_001d: nop
L_001e: ret

ואילו השורה הבאה:

1
Shape shape = obj as Shape;

מתקמפלת ל

1
2
3
4
L_0003: ldloc.0
L_0004: isinst Shape
L_0009: stloc.1
L_000a: ret

מה שאנחנו רואים כאן זה משהו מעניין: הפעולה isinst היא פעולת IL הבודקת אם המשתנה שלנו הוא מסוג מסוים.

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

במקרה שני, אנחנו רואים שפשוט מוכנס ערך המשתנה למשתנה השני!

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

המשך יום מוסב לטובה!

שתף

325. Covariance workaround

הכרנו בעבר (טיפים מספר 36-40) את המושגים של covariance וcontravariance.

ראינו שהFeatureים האלה קיימים רק מC# 4.0.

אפשר לחקות את ההתנהגות הזאת גם בC# 2.0 ומעלה בצורה הבאה:

אם אנחנו רוצים שפונקציה שלנו תוכל להתמודד למשל עם IEnumerable ועם IEnumerable<> של כל מה שיורש מShape, אז נוכל לעשות משהו כזה:

1
2
3
4
5
public void Draw<TShape>(IEnumerable<TShape> shapes)
where TShape : Shape
{
// ...
}

כעת נוכל לקרוא לה כך:

1
2
3
4
5
List<Shape> shapes = new List<Shape>();
List<Square> squares = new List<Square>();
Draw(shapes);
Draw(squares);

זה עובד בזכות הFeature האהוב עלי של הקומפיילר שיודע לזהות implicitly את הפרמטר הגנרי שאנחנו מעוניינים לשלוח לפונקציה. (ראו גם טיפ מספר 28)

כמובן, זה לא מאפשר לנו כל מה שמאפשר covariance, אבל זה פתרון אפשרי.


כמובן, אם אנחנו מתכנתים בC# 4.0, ניתן להשתמש בCovariance של IEnumerable ופשוט לכתוב כך:

1
2
3
4
public static void Draw(IEnumerable<Shape> shapes)
{
// ...
}

ואז זה יעבוד.

סוף שבוע קו-ואריאנטי טוב!

שתף

324. A if rewrite

לפעמים יוצא לנו לכתוב קוד כזה:

1
2
3
4
5
6
7
8
9
10
11
12
if ((value == "Mango") ||
(value == "Banana") ||
(value == "Melon") ||
(value == "Honey") ||
(value == "Cinnamon"))
{
Console.WriteLine("I know a song with this word!");
}
else
{
Console.WriteLine("Dude?");
}

(ראו למשל טיפ מספר 315)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ICollection<string> words =
new List<string>
{
"Mango",
"Banana",
"Melon",
"Honey",
"Cinnamon"
};
if (words.Contains(value))
{
Console.WriteLine("I know a song with this word!");
}
else
{
Console.WriteLine("Dude?");
}

זה גם מאפשר לנו להוסיף עוד תנאים לא קשורים לif.

כמובן אפשר להפוך גם את הCollection להיות Member של המחלקה וכו’.

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

שתף

323. Comparing new T() with other methods

אז פעם שעברה גילינו שnew T() משתמש מאחורי הקלעים בפונקציה Activator.CreateInstance(). ראיתי שיש לה מימוש שונה לחלוטין מהמימוש של Activator.CreateInstance, ולכן זה דורש בדיקה האם זו פעולה יותר יעילה או לא.

השוואתי שלוש צורות של הקצאת אובייקט פשוט: הראשונה היא דרך פונקציה שקוראת לConstructor הדיפולטי שלו, השנייה היא ע”י קריאה לפונקציה גנרית עם Constraint של where T : new() וקריאה לnew T(). השלישית היא ע”י Activator.CreateInstance.

התוצאות לפניכם: (הזמן נמדד במילישניות)

Number of objects new Constructor new T() Activator.CreateInstance
10000 0 1 1
100000 1 14 16
1000000 17 142 166
10000000 191 1435 1655
100000000 1773 14379 16757

המסקנה היא שnew T() שמשתמש בActivator.CreateInstance() הוא יותר טוב מActivator.CreateInstance, אבל פחות טוב (בהרבה) מקריאה לConstructor הדיפולטי.

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

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

שתף

322. new T() and Activator CreateInstance

דיברנו כבר בעבר על הConstraint המיוחד where T : new() (טיפים מספר 33, 317).

מעניין להבין איך הוא עובד. ובכן, אם נכתוב קוד כזה:

1
2
3
4
public static T Create<T>() where T:new()
{
return new T();
}

נקבל את תוצאת הקימפול הזאת:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.method public hidebysig static !!T Create<.ctor T>() cil managed
{
.maxstack 2
.locals init (
[0] !!T CS$1$0000,
[1] !!T CS$0$0001)
L_0000: nop
L_0001: ldloca.s CS$0$0001
L_0003: initobj !!T
L_0009: ldloc.1
L_000a: box !!T
L_000f: brfalse.s L_001c
L_0011: ldloca.s CS$0$0001
L_0013: initobj !!T
L_0019: ldloc.1
L_001a: br.s L_0021
L_001c: call !!0 [mscorlib]System.Activator::CreateInstance<!!T>()
L_0021: stloc.0
L_0022: br.s L_0024
L_0024: ldloc.0
L_0025: ret
}

מה שאנחנו רואים כאן זה בעצם קריאה מורכבת לפונקציה Activator.CreateInstance<T>(). (עם התחשבות בboxים וכו’)

אם נקרא אודות התיעוד של הפונקציה הזו בMSDN, נראה שבעצם הפונקציה הזאת קיימת בשביל שימוש ב-new T().

לא ברור האם מבחינת ביצועים היא גרועה כמו Activator.CreateInstance (ראו גם טיפ מספר 139, 180), אבל אם נסתכל במימוש של שתי הפונקציות האלה בReflector, נראה שמדובר בשני מימושים שונים לחלוטין! (היינו מצפים שאחד יקרא לשני, או ששניהם יקראו לאותה פונקציה)

מה שנותן תקווה כי אולי המימוש הוא לא כל כך נורא.

אולי בהמשך נבצע השוואה בין 3 שיטות: Activator.CreateInstance(typeof(T)), new T() וקריאה רגילה לConstructor.

שבוע גנרי טוב!

שתף

321. Implementing the virtual method pattern in C# part 3

אז ראינו בפעם הקודמת איך אפשר לממש פונקציות וירטואליות בשפה שתומכת בפונקציות סטטיות וDelegateים לפונקציות סטטיות.

מה הבעיה במימוש הזה? הבעיה היא שיקולי זכרון – במימוש הנוכחי, כל Instance מחזיק Delegate לכל מתודה וירטואלית שלו. בגלל שאלה פונקציות סטטיות, בעצם כל הInstanceים מחזיקים אוטומטית אותם Delegateים!

אם במחלקה יש הרבה מתודות וירטואליות, למשל 100 מתודות וירטואליות, באופן אוטומטי במימוש זה, כל Instance יתחיל עם 100 Delegateים!

אז מה אפשר לעשות?

מה שהCLR בעצם עושה זה מקבץ את כל המתודות הוירטואליות לאיזה מבנה שנקרא Virtual Table, בצורה סטטית, כך שהDelegateים נוצרים פעם אחת עבור כל הInstanceים.

לדוגמה, הקוד הזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Square : Shape
{
public double Edge { get; set; }
public override double GetArea()
{
return this.Edge * this.Edge;
}
public override double GetPerimeter()
{
return 4 * this.Edge;
}
}

יתרגם למשהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
internal sealed class VTable
{
public delegate double GetPerimeterSignature(Shape _this);
public delegate double GetAreaSignature(Shape _this);
private readonly GetPerimeterSignature mGetPerimeter;
private readonly GetAreaSignature mGetArea;
public VTable(GetPerimeterSignature getPerimeter,
GetAreaSignature getArea)
{
mGetPerimeter = getPerimeter;
mGetArea = getArea;
}
public GetPerimeterSignature GetPerimeter
{
get { return mGetPerimeter; }
}
public GetAreaSignature GetArea
{
get { return mGetArea; }
}
}

כך שיאותחל Instance שלה באופן סטטי למחלקה Square:

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
public abstract class Shape
{
internal VTable mTable;
}
public class Square : Shape
{
private static VTable sTable =
new VTable(InnerGetArea, InnerGetPerimeter);
public Square()
{
mTable = sTable;
}
public double Edge { get; set; }
public static double InnerGetArea(Shape _this)
{
return GetArea((Square) _this);
}
public static double InnerGetPerimeter(Shape _this)
{
return GetPerimeter((Square) _this);
}
public static double GetArea(Square _this)
{
return _this.Edge * _this.Edge;
}
public static double GetPerimeter(Square _this)
{
return 4 * _this.Edge;
}
}

כעת כשנכתוב קוד כזה:

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

הקומפיילר יתרגם אותו למשהו כזה:

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

כך שבעצם נכנס לפונקציה שרצינו! 😃

כי אתחלנו באופן סטטי בSquare את הMember ששמו sTable להיות VTable שממפה את המתודות למתודות הנכונות, ובכל יצירה של Square, אנחנו מאתחלים אותו עם הVTable המתאים.


זה בגדול איך ממומשות פונקציות וירטואליות בCLR. כמובן, זו לא התמונה המלאה – לא דיברנו בכלל על מה קורה עם Interfaceים, מה קורה עם שליחת null לפונקציה, הרי מותר להפעיל פונקציות סטטיות עם null, אבל אסור להפעיל פונקציות סטטיות כך. לא דיברנו על מה מונע מאיתנו לשלוח לפונקציה הסטטית פרמטר לא מתאים, למשל

1
2
3
RegularTriangle triangle = new RegularTriangle();
Square square = new Square();
square.mTable.GetPerimeter(triangle);

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


הסדרה הזאת מבוססת על הסדרה Implementing the virtual method pattern in C# של Eric Lippert. בסה"כ אני חושב שזה מסביר בצורה טובה איך עובדות בגדול פונקציות וירטואליות. אם אהבתם, אתם מוזמנים לקרוא את הפוסטים המקוריים בבלוג של Eric Lippert.

סופ"ש וירטואלי טוב!

שתף