דיברנו כבר בעבר על האופרטורים ששמם 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 הוא חסכוני יותר. זוהי כמובן מיקרו-אופטימיזציה שכנראה לא תהיה צוואר בקבוק בתכנית, אבל מעניין לדעת שפעולה זו מהירה יותר מהפעולה השקולה לה.
אז פעם שעברה גילינו ש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 הדיפולטי.
שימו לב שזה גם דורש מספר גדול של אובייקטים כדי לשים לב לזה, כך שזה לא נורא, אם אתם לא צריכים להתמודד עם קצבים/כמויות גדולות של יצירות כאלה.
מה שאנחנו רואים כאן זה בעצם קריאה מורכבת לפונקציה Activator.CreateInstance<T>(). (עם התחשבות בboxים וכו’)
אם נקרא אודות התיעוד של הפונקציה הזו בMSDN, נראה שבעצם הפונקציה הזאת קיימת בשביל שימוש ב-new T().
לא ברור האם מבחינת ביצועים היא גרועה כמו Activator.CreateInstance (ראו גם טיפ מספר 139, 180), אבל אם נסתכל במימוש של שתי הפונקציות האלה בReflector, נראה שמדובר בשני מימושים שונים לחלוטין! (היינו מצפים שאחד יקרא לשני, או ששניהם יקראו לאותה פונקציה)
מה שנותן תקווה כי אולי המימוש הוא לא כל כך נורא.
אולי בהמשך נבצע השוואה בין 3 שיטות: Activator.CreateInstance(typeof(T)), new T() וקריאה רגילה לConstructor.
אז ראינו בפעם הקודמת איך אפשר לממש פונקציות וירטואליות בשפה שתומכת בפונקציות סטטיות וDelegateים לפונקציות סטטיות.
מה הבעיה במימוש הזה? הבעיה היא שיקולי זכרון – במימוש הנוכחי, כל Instance מחזיק Delegate לכל מתודה וירטואלית שלו. בגלל שאלה פונקציות סטטיות, בעצם כל הInstanceים מחזיקים אוטומטית אותם Delegateים!
אם במחלקה יש הרבה מתודות וירטואליות, למשל 100 מתודות וירטואליות, באופן אוטומטי במימוש זה, כל Instance יתחיל עם 100 Delegateים!
אז מה אפשר לעשות?
מה שהCLR בעצם עושה זה מקבץ את כל המתודות הוירטואליות לאיזה מבנה שנקרא Virtual Table, בצורה סטטית, כך שהDelegateים נוצרים פעם אחת עבור כל הInstanceים.
כי אתחלנו באופן סטטי ב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);
לא דיברנו מה קורה כשמחלקת בת מוסיפה פונקציה וירטואלית משלה.