אנחנו נהנים מהיכולת של הקומפיילר להסוות לנו את העובדה שנוצרת מחלקה מאחורי הקלעים, ושאנחנו מפעילים את הפונקציה Invoke, וכך אנחנו כותבים פחות קוד. (למעשה, ע"י שימוש בFunc/Action, כמעט ואין לנו צורך לכתוב Delegateים משלנו, ראו גם טיפ מספר 46-47)
בJava 8 לא יוסיפו מילה שמורה חדשה בשם Delegate. אז מה יהיה במקום?
הרעיון הוא אותו רעיון, אבל קצת שונה. נניח שאנחנו רוצים לכתוב פונקציה שמקבלת הצבעה לפונקציה אחרת, למשל עם החתימה שהשתמשנו בה כאן.
מה שנאלץ לעשות זה ליצור משהו שנקרא SAM – אלה ראשי תיבות של Single abstract method.
נדבר כעת על ההבדל הכללי בין Templateים בC++ לGenerics בC# וJava.
מה זה Template בC++?
Templateים בC++ מאפשרים לנו לכתוב קוד כזה:
1
std::list<Person>* foo = newstd::list<Person>();
זה נראה כמו Generics של Java וC#, אבל מאחורי הקלעים קורים דברים אחרים לגמרי.
זה יותר מזכיר את איך שC# יוצר מחלקות בזמן ריצה מאשר שפיכת הקוד שעושה Java, אבל עדיין זה עוף שונה.
הקומפיילרים של C# וJava מייצרים קוד שמתאים לVirtual machines. אם נכתוב למשל קוד שיש בתוכו את המחלקה Person, בJava או בC# זה יתקמפל לקוד שמכיל מידע אודות המחלקה Person שתפנה לdll המיוצר, והCLR/JVM ידעו לעשות משהו עם זה.
C++ מייצר קוד בשפת מכונה. בניגוד לC# וJava, אין אובייקט שהוא object שכל האובייקטים יורשים ממנו, ואין Virtual Machine שצריכה לדעת על המחלקה Person. גם אין boxing או unboxing ופונקציות לא חייבות להיות שייכות למחלקות.
בגלל זה, הקומפיילר של C++ לא מאפשר להניח הנחות על מה שאפשר לעשות עם templateים –כל קוד שנוכל לכתוב באופן ידני, אפשר לכתוב template שיכתוב אותו בשבילנו.
הדוגמה הברורה ביותר היא הוספת דברים:
בC# וJava, מערכת הטיפוסים הגנרית צריכה לדעת איזה מתודות קיימות עבור מחלקה, וצריכה להעביר את זה לvirtual machine.
הדרך היחידה לעשות היא ע"י כתיבת Hard coded מהי המחלקה הקונקרטית, או ע"י ממשקים.
למשל הקוד הבא לא יתקמפל בC# או Java:
1
int addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
מאחר ולא ידוע למשל אם לטיפוס T אכן יש מתודה בשם Name. אנחנו צריכים לציין לו את זה. בC# אפשר לציין ככה:
1
2
interfaceIHasName{ stringName(); };
int addNames<T>( T first, T second ) where T : IHasName { .... }
ואז אנחנו צריכים לוודא שכל הדברים שעוברים לaddNames מממשים את הממשק IHasName. בJava הSyntax קצת שונה, אבל סובל מאותם בעיות.
המקרה הקלאסי של הבעיה הזאת היא לנסות לכתוב מתודה כזאת:
1
int addNames<T>( T first, T second ) { return first + second; }
בC# אי אפשר לכתוב קוד כזה כי אי אפשר להגדיר ממשק שיש לו אופרטור של +.
C++ לא סובל מהבעיות האלה – הקומפיילר לא צריך לדאוג להעביר טיפוסים לVirtual Machine. אם לטיפוסים שלנו יש מתודת .Name(), הקוד יתקמפל. אחרת לא.
הטיפים על ההבדלים בין Templateים וGenerics בC#/Java מבוססים על הפוסט הבא בStackOverflow.
קיים הבדל בCLR בין התייחסות לפרמטר גנרי שהוא Reference type לפרמטר גנרי שהוא value type.
כאשר הCLR נתקל בטיפוס (או מתודה) גנרי הוא יוצר קוד מתאים של הטיפוס (או המתודה) לפרמטר הגנרי.
בפעם הבאה כשיתקל הCLR באותו טיפוס/מתודה עם אותו פרמטר גנרי, הוא ישתמש באותו קוד שיצר בפעם הקודמת.
מה שכתוב כאן נכון, אבל קיים הבדל דק בין פרמטר גנרי שהוא value type לפרמטר גנרי שהוא reference type. (ראו גם טיפ מספר 131)
נתחיל בreference type: בפעם הראשונה שהCLR נתקל בטיפוס גנרי עם פרמטר גנרי מסוג reference type הוא יוצר קוד מתאים. בפעם הבאה שהCLR יתקל באותו טיפוס גנרי עם פרמטר גנרי אחר שהוא reference type, הוא ישתמש באותו קוד שיצר עבור הreference type הקודם.
הדבר הזה אפשרי מאחר וכל הreference types הם באותו גודל. למשל, נניח שיש לנו את הטיפוס הזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyList<T>
{
private T[] mValues = new T[4];
publicvoidAdd(T value)
{
mValues[0] = value;
}
public T GetIndex(int index)
{
return mValues[0];
}
}
אז כאשר נציב ערכים שונים של T שהם Reference types נצפה לראות את המחלקות האלה:
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
publicclassDogList
{
private Dog[] mValues = new Dog[4];
publicvoidAdd(Dog value)
{
mValues[0] = value;
}
public Dog GetIndex(int index)
{
return mValues[0];
}
}
publicclassCatList
{
private Cat[] mValues = new Cat[4];
publicvoidAdd(Cat value)
{
mValues[0] = value;
}
public Cat GetIndex(int index)
{
return mValues[0];
}
}
(כאשר Cat וDog הם Reference types)
אם נסתכל בIL שנוצר למתודות הנ"ל נראה IL דומה:
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
.method public hidebysig instance voidAdd(class Dog 'value') cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld classmValues
L_0007: ldc.i4.0
L_0008: ldarg.1
L_0009: stelem.ref
L_000a: ret
}
.method public hidebysig instance class Dog GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] class Dog CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
L_0007: ldc.i4.0
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: br.s L_000c
L_000c: ldloc.0
L_000d: ret
}
מה שאנחנו רואים זה שההתייחסויות למחלקה נמצאים במספר מקומות (בBold), אבל מאחר וכל הקצאות הזכרון הן באותו גודל (כלומר ביחידות מידה של Reference), אפשר לעשות Reuse לIL כפי שהוא.
למען האמת אפשר לשנות את הקוד ל:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.method public hidebysig instance class Dog GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] class object CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
L_0007: ldc.i4.0
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: br.s L_000c
L_000c: ldloc.0
L_000d: ret
}
לקמפל מחדש ולראות שזה עדיין עובד!
לעומת זאת אם המחלקה שלנו היא Value Type, אז נראה משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassColorList
{
private Color[] mValues = new Color[4];
publicvoidAdd(Color value)
{
mValues[0] = value;
}
public Color GetIndex(int index)
{
return mValues[0];
}
}
מתקמפל ל:
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
.method public hidebysig instance voidAdd(valuetype Color 'value') cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld valuetype Color [] ColorList::mValues
L_0007: ldc.i4.0
L_0008: ldelema Color
L_000d: ldarg.1
L_000e: stobj Color
L_0013: ret
}
.method public hidebysig instance valuetype Color GetIndex(int32 index) cil managed
{
.maxstack 2
.locals init (
[0] valuetype Color CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld valuetype Color [] ColorList::mValues
L_0007: ldc.i4.0
L_0008: ldelema Color
L_000d: ldobj Color
L_0012: stloc.0
L_0013: br.s L_0015
L_0015: ldloc.0
L_0016: ret
}
אנחנו רואים פה הבדל מאחר ויש צורך להעתיק את הערך של הValue type למשתנה על Stack.
מאחר ולכל Value type מוקצה גודל שונה על הStack, לא ניתן לעשות כאן Reuse.
לכן כל פעם שנתקל בValue type שטרם נראה, הCLR יאלץ ליצור קוד מתאים חדש, מאחר ולא יוכל למחזר את הקוד.
הכרנו בעבר את המושג של Generics בC# (ראו גם טיפים 26-34).
קיים מושג מקביל בJava. עם זאת, Generics בJava אינם נחשבים ל”Real Generics”.
הפעם נדבר על כך ונסביר מה ההבדל בין Generics בין שתי השפות.
Generics בC# מאפשרים לנו לכתוב קוד כזה:
1
List<Person> peopleList = new List<Person>();
מה שהדבר הזה עושה מאחורי הקלעים זה יוצר טיפוס בזמן ריצה בו מוחלף בכל מקום הטיפוס הגנרי בטיפוס שציינו (Person).
אנחנו נהנים משני יתרונות עיקריים:
Type Safety – הקומפיילר לא יאפשר לנו לטעות בטיפוסים שפונקציות שמקבלות או מחזירות, בכל פונקציה שאמורה לקבל Person, לא נוכל להכניס טיפוס אחר. זה יתרון שפותר לנו בעיות שהיו לנו בעבודה עםArrayList וחבריו
ביצועים – מאחר ונוצר טיפוס אמיתי בזמן ריצה, אין צורך בהסבות, ולכן אנחנו חוסכים את ההסבות, וכך נהנים מביצועים טובים יותר.
החסרון העיקרי במימוש הזה (יצירת טיפוס בזמן ריצה), הוא שהוא הצריך שינויים בCLR ולכן אם נרצה לעבוד עם Frameworkים נמוכים במיוחד (כגון Framework 1.0), לא נוכל.
מצד שני, צריכה להיות סיבה ממש טובה שנרצה לעבוד עם גרסה ישנה מאוד של הFramework.
Generics בJava מאפשרים לכתוב קוד כזה:
1
ArrayList<Person> peopleList = new ArrayList<Person>();
על פניו, זה נראה דומה.
מה שקורה כאן זה משהו שונה לגמרי – הקומפיילר מאחורי הקלעים יצור ArrayList רגיל. כאשר נבצע פעולה כגון זו:
1
peopleList.add(new Person());
הקומפיילר אכן יבדוק אם הטיפוס של החתימה מתאים לטיפוס שאנחנו מנסים להכניס.
כאשר נבצע פעולה כזאת:
1
Person person = peopleList.get(0);
מאחר וPeopleList הוא בסה"כ ArrayList פשוט, הקומפיילר יתרגם אותה לפעולה הזאת:
1
Person person = (Person) peopleList.get(0);
אנחנו רואים שאנחנו לא נהנים מהביצועים הטובים יותר שיכלו לתת לנו Generics ע"י מניעת ההסבה הנ"ל.
בנוסף, הטיפוס הגנרי קיים רק בזמן קימפול. בזמן ריצה, מאחר והקומפיילר מתרגם הכל למחלקות לא גנריות, לא קיים הטיפוס ArrayList<Person>. הדבר הזה לא מאפשר לכל מיני מנגנונים בזמן ריצה לנתח את הטיפוס בReflection, ולכן לא ניתן לממש כל מיני מנגנונים דומים לאלו של C#.
בנוסף, בעקבות מגבלות מימוש ובעקבות העובדה שבJava טיפוסים פרימיטיביים הם אינם object, לא נוכל ליצור למשל ArrayList של int.
עוד דרך לממש Framework לInterception היא דרך מטורפת המשתמשת בAPI של הProfiler.
קצת רקע למי שלא מכיר:
בVisual Studio קיימת אפשרות לבצע Profiling לתוכנות שלנו בMode שנקרא Instrumentation.
מה שהדבר הזה עושה זה משנה את הDLLים שלנו כך שמתווסף לפני ואחרי קריאה לכל פונקציה מדידה של הזמן שלקח לפונקציה לרוץ.
מסתבר שלProfiler הזה יש API המאפשר לו לבצע את המניפולציות האלה על הDLLים שלנו, ואפשר להשתמש בו כדי לעשות מניפולציות משלנו.
יש הרבה Frameworkים שיודעים לנצל את הAPI הזה ולעשות איתו ניסים ונפלאות, למשל קיים הFramework לבדיקות בשם TypeMock המאפשר להתערב בכל היצירות של האובייקטים מבלי לשנות את הקוד המורץ, על מנת לאפשר ביצוע Mockים (ראו גם טיפ מספר 350) לאובייקטים מבלי לשנות את הקוד. (כלומר, קריאה, למשל, לnew לא תקרא לConstructor, אלא לפונקציה של הUnit Test שתחזיר את הMock הרצוי)
אז מה הקשר לInterception? ניתן להשתמש בAPI של הProfiler על מנת לבצע “קסמים” כאלה שישנו את הקוד המקומפל שלנו כך שיבצע את מה שאנחנו מעוניינים שיקרה.
היתרון של הסיפור הזה זה שזה נותן לנו הרבה מאוד כוח לעשות מה שאנחנו רוצים.
יש גם כמה חסרונות:
החסרון הראשון הוא שהAPI הזה לא כל כך מתועד וקשה להשתמש בו (הוא גם כתוב בCOM)
החסרון השני הוא שהדבר הזה לא כל כך נתמך, ולכן כל פעם שמתחלפת גרסה של Visual Studio, החבר’ה בTypeMock עובדים קשה על להדביק את הקצב ולהתאים את עצמם לגרסה החדשה.
לא מצאתי באינטרנט Framework של Interception שמשתמש בProfiler API. אני מניח שאין מאחר ומדובר בהרבה עבודה קשה בליצור כלי כזה, וכבר יש אלטרנטיביות מעולות, כגון PostSharp.
אם מישהו מוצא משהו, אני אשמח לשמוע.
כאשר יוצא לך לעשות Profiling בMode הזה, נכתב לך בOutput Window טקסט מהסוג הזה:
Profiling started. Instrumenting C:\Projects\MyProfilingApplication\MyProfilingApplication\obj\x86\Debug\MyProfilingApplication.exe in place Info VSP3049: Small functions will be excluded from instrumentation. Microsoft (R) VSInstr Post-Link Instrumentation 10.0.40219 x86 Copyright (C) Microsoft Corp. All rights reserved. File to Process: C:\Projects\MyProfilingApplication\MyProfilingApplication\obj\x86\Debug\MyProfilingApplication.exe –> C:\Projects\MyProfilingApplication\MyProfilingApplication\obj\x86\Debug\MyProfilingApplication.exe Original file backed up to C:\Projects\MyProfilingApplication\MyProfilingApplication\obj\x86\Debug\MyProfilingApplication.exe.orig Successfully instrumented file C:\Projects\MyProfilingApplication\MyProfilingApplication\obj\x86\Debug\MyProfilingApplication.exe. Instrumenting C:\Projects\MyProfilingApplication\MyProfilingApplication.Dependency\obj\Debug\MyProfilingApplication.Dependency.dll in place Info VSP3049: Small functions will be excluded from instrumentation. Microsoft (R) VSInstr Post-Link Instrumentation 10.0.40219 x86 Copyright (C) Microsoft Corp. All rights reserved. File to Process: C:\Projects\MyProfilingApplication\MyProfilingApplication.Dependency\obj\Debug\MyProfilingApplication.Dependency.dll –> C:\Projects\MyProfilingApplication\MyProfilingApplication.Dependency\obj\Debug\MyProfilingApplication.Dependency.dll Original file backed up to C:\Projects\MyProfilingApplication\MyProfilingApplication.Dependency\obj\Debug\MyProfilingApplication.Dependency.dll.orig Successfully instrumented file C:\Projects\MyProfilingApplication\MyProfilingApplication.Dependency\obj\Debug\MyProfilingApplication.Dependency.dll. Warning VSP2013: Instrumenting this image requires it to run as a 32-bit process. The CLR header flags have been updated to reflect this. Profiling process ID 6568 (MyProfilingApplication).
מה שאנחנו רואים בעצם זה שהDLL שונה וגובה על ידי הProfiler. אם נסתכל על הDLL ששונה בReflector נראה משהו כזה:
יש פה כמה קריאות כאלה לProfiling שלא ברור למה הוא עושה (הוא עושה Start ומיד לאחר מכן Stop), אבל אפשר לראות שלפונקציה Test הוא כן עושה Profiling (בודק כמה זמן לוקח לה לרוץ)
ככה הוא עושה על כל הTarget Projects/Binaries שאנחנו נותנים לו, וכך בעצם הוא יודע כמה זמן בערך לוקח לכל קריאה לרוץ. (שים לב שזה זמן יחסי, שהרי הקריאות לProfiling מאטות את הפונקציות האמיתיות. בין השאר, הדבר הזה לא משקף באופן אמין ביצועים של דברים שקשורים לIO כגון עבודה עם Streamים וכו’, להם עדיף להשתמש בMode של Sampling)
בהמשך לטיפים הקודמים שהיו לפני כמה זמן, נראה עכשיו עוד שיטה ליצור Interception לאובייקטים.
ממליץ לקרוא את הטיפים הקודמים כדי להזכר (350-362)
השיטה הפעם נקראת Compile-time IL weaving.
מה זה?
הרעיון הוא הרעיון המטורף הבא:
אחרי שאנחנו מקמפלים אפליקציה, נוצרים לנו DLLים המכילים את הקוד של האפליקציה שמקומפל לשפת ביניים (מה שנקרא CIL או MSIL, תלוי מתי שאלתם).
Compile-time IL weaving מציע לקחת את הDLL שנוצר, ולשנות את הIL שנוצר, כך שיבצע התנהגות שונה, ועל ידי כך להשיג את הInterception.
הIL לאחר השינוי נשמר בDLL, כך שהשינוי הוא שינוי סטטי. (לא מתבצע בזמן ריצה)
אז איך עושים את זה? יש מספר APIים בעולם המאפשרים לפרסר IL ולבצע עליו מניפולציות, ביניהם: Mono.Cecil, Microsoft.CCI, PostSharp API.
בד”כ לא נרצה להשתמש בהם ישירות מאחר וזו משימה יחסית מסובכת לערוך את הIL בלי לעשות נזק, וגם לדאוג שמה שרצינו לעשות יעבוד.
למרות שלפעמים יחסית קל להשתמש בהם כדי לעשות דברים פשוטים, למשל לשנות את הProperties של המחלקות שלנו שיקראו לPropertyChanged של INotifyPropertyChanged (יש דוגמאות באינטרנט לדבר הזה)
במידה ונעשה משהו כזה, האידיאל יהיה שיהיה איזשהו Post build event בSolution שלנו שמריץ את השינוי של הIL על הDLLים המקומפלים.
קיימים Frameworkים יחסית מפותחים, ביניהם AfterThought וPostSharp המאפשרים לנו לבצע את הפעילות הזו בזמן קימפול בצורה נוחה.
PostSharp הוא הFramework המוביל בשוק בתחום הזה ומה שהוא נותן זה מימוש לAOP (ראו גם טיפ מספר 358) באמצעות Compile-time IL weaving:
איך זה נראה? אנחנו צריכים לרשת מAttributeים של התשתית, למשל
בפועל יקרא הקוד שלנו. בואו נסתכל על הקוד שיוצא בReflector:
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
43
publicstaticintFactorial(int n)
{
int returnValue;
Arguments<int> argsCollection = new Arguments<int>();
argsCollection.Arg0 = n;
MethodExecutionArgs args = new MethodExecutionArgs(null, argsCollection);
args.Method = mAspects.m1;
mAspects.a0.OnEntry(args);
if (args.FlowBehavior == FlowBehavior.Return)
{
return (int)args.ReturnValue;
}
try
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
int temp = result;
returnValue = temp;
}
catch (Exception exception)
{
args.Exception = exception;
mAspects.a0.OnException(args);
throw;
}
finally
{
args.ReturnValue = returnValue;
mAspects.a0.OnExit(args);
}
return returnValue;
}
מה שאנחנו רואים פה זה שנקרא שלפני ביצוע הקוד שלנו, מתבצעת קריאה לOnEntry. כאשר נתפס Exception, מתבצעת קריאה לOnException, וביציאה למתודה מתבצעת קריאה לOnExit.
מה שמגניב זה שיש Member בשם mAspects (זה לא באמת השם שלו, אבל לצורך העניין) שמכיל את האובייקטים של הAttributeים והמתודות, והוא נוצר בגישה הראשונה של המחלקה שלנו, כך שבעצם מתבצעת קריאה ישירות למתודה (ולא ע"י עטיפה כמו שראינו בדוגמאות של הRuntime Subclassing), כך שהקוד שנוצר הוא ממש מהיר.
מה שכן, צריך לשים לב לדברים הבאים:
PostSharp, הכלי המוביל בשוק, הוא לא חינמי ועולה קצת כסף. שאר הכלים פחות טובים
הדבר הזה קצת מוזר בדיבוג – אנחנו נראה שאנחנו נכנסים לקוד שבAttribute בכניסה למתודה, למרות שלא ראינו בקוד קריאה אליו.
למרות זאת, מדובר בכלי מאוד חזק שיכול להיות מאוד שימושי לכתיבת תשתיות.
כשתכלס PropertyChanged הוא Event שקופץ כאשר אחד הProperties של האובייקט משתנה, עם השם של הProperty שהשתנה.
הממשק הזה קיים בעיקר למטרות UI:
במידה ויש לנו UI שמציג אובייקט כלשהו, ואנחנו רוצים שהUI יתעדכן אוטומטית כשהאובייקט משתנה (ע"י Data Binding), הדרך הנכונה לעשות זאת היא לממש את הממשק INotifyPropertyChanged. הFrameworkים המוכרים לנו (Windows Form וWPF) ידעו כבר להרשם לאירוע ולעדכן את הUI בהתאם, במידה ונגדיר את הBinding נכון.
איך מממשים את הממשק הזה?
בסה"כ המימוש הוא יחסית פשוט:
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
publicclassPerson : INotifyPropertyChanged
{
#region Data Members
privateint mAge;
privatestring mName;
#endregion
#region Properties
publicstring Name
{
get
{
return mName;
}
set
{
string originalValue = mName;
mName = value;
if (originalValue != value)
{
RaisePropertyChanged("Name");
}
}
}
publicint Age
{
get
{
return mAge;
}
set
{
int originalValue = mAge;
mAge = value;
if (originalValue != value)
{
RaisePropertyChanged("Age");
}
}
}
#endregion
#region Private Methods
privatevoidRaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));