370. Single Abstract Methods

למי שלא יודע, עד היום אין בJava תמיכה בDelegateים.

התמיכה תכנס בJava 8, הגרסה הבאה שתצא. (אם אני לא טועה, היא מתוכננת לקיץ הבא)

אני רוצה לדבר על איך זה הולך לעבוד.


איך זה עובד בC#? למי שזוכר ומי שלא, בC# אנחנו מגדירים אובייקטים מיוחדים שנקראים Delegateים, ע”י המילה השמורה delegate:

1
public delegate int NumericFunction(int value);

ואז אנחנו יכולים להשתמש בטיפוס בזה – לקבל אותו כפרמטר בפונקציה, לאתחל אותו במשתנה, ולבסוף גם להריץ את הDelegate.

מה שקורה מאחורי הקלעים זה שכל Delegate בעצם מתקמפל למחלקה.

כאשר אנחנו כותבים קוד כזה:

1
2
3
4
public int MyFunctionAt3(NumericFunction myFunction)
{
return myFunction(3);
}

הוא מתקמפל לקוד הזה:

1
2
3
4
public int MyFunctionAt3(NumericFunction myFunction)
{
return myFunction.Invoke(3);
}

אנחנו נהנים מהיכולת של הקומפיילר להסוות לנו את העובדה שנוצרת מחלקה מאחורי הקלעים, ושאנחנו מפעילים את הפונקציה Invoke, וכך אנחנו כותבים פחות קוד. (למעשה, ע"י שימוש בFunc/Action, כמעט ואין לנו צורך לכתוב Delegateים משלנו, ראו גם טיפ מספר 46-47)


בJava 8 לא יוסיפו מילה שמורה חדשה בשם Delegate. אז מה יהיה במקום?

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

מה שנאלץ לעשות זה ליצור משהו שנקרא SAM – אלה ראשי תיבות של Single abstract method.

זה ממשק שיש לו מתודה בודדת, למשל:

1
2
3
4
interface NumericFunction
{
public int calculate(int value);
}

את הפונקציה הקודמת נכתוב ככה:

1
2
3
4
public int myFunctionAt3(NumericFunction myFunction)
{
return myFunction.calculate(3);
}

והקומפיילר של Java ידע לתרגם Referenceים למתודות לממשק שלנו.

היתרון הוא שיש פה יותר הבנה של מה שקורה.

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


המשך שבוע עם קפה רותח טוב.

שתף

369. The difference between generics and templates

נדבר כעת על ההבדל הכללי בין 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
interface IHasName{ string Name(); };
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.

סופ"ש גנרי טוב.

שתף

368. The difference between generic parameter types

בהמשך לטיפים על טיפוסיים גנריים:

קיים הבדל ב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];
public void Add(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
public class DogList
{
private Dog[] mValues = new Dog[4];
public void Add(Dog value)
{
mValues[0] = value;
}
public Dog GetIndex(int index)
{
return mValues[0];
}
}
public class CatList
{
private Cat[] mValues = new Cat[4];
public void Add(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 void Add(class Dog 'value') cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld class mValues
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
public class ColorList
{
private Color[] mValues = new Color[4];
public void Add(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 void Add(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 יאלץ ליצור קוד מתאים חדש, מאחר ולא יוכל למחזר את הקוד.


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

שתף

367. The difference between Java generics and C# generics

הכרנו בעבר את המושג של 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.


שבוע גנרי וקפה טוב.

שתף

366. Implementing an interception framework – Summary

בהמשך לטיפים הקודמים,

נראה כעת השוואה בין כל השיטות שראינו למימוש Interception:

להלן טבלה שגנובה מהאתר של SharpCrafters (היוצרים של PostSharp)

האלמנט שעושים לו Interception Static weaving (PostSharp) Runtime subclassing ContextBoundObject
מתודות של ממשק V V V
מתודות וירטואליות שהן public V V V
מתודות לא וירטואליות שהן public V V
מתודות וירטואליות שהן protected V V
מתודות סטטיות או private או protected V
מתודות חיצוניות (מAssembly אחר) V
Constructorים V
Fieldים V
Properties V
Events V

מה שאנחנו רואים זה ששימוש בIL weaving נותן לנו את הכוח החזק ביותר, אבל זו גם הדרך הקשה ביותר לממש Interception.

סופ”ש מיורט לטובה.

שתף

365. Implementing an interception framework - Profiler API

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

עוד דרך לממש 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 נראה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void Main(string[] args)
{
_CAP_Enter_Function_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0);
_CAP_StartProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0xa000011);
_CAP_StopProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0);
MyClass class2 = new MyClass();
_CAP_StartProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0xa000012);
class2.Test("Foo");
_CAP_StopProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0);
_CAP_StartProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0xa000013);
_CAP_StopProfiling_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0);
Console.ReadLine();
_CAP_Exit_Function_Managed((int) Microsoft.VisualStudio.Instrumentation.g_fldMMID_2D71B909-C28E-4fd9-A0E7-ED05264B707A, 0x6000005, 0);
}

כאשר הקוד המקורי היה נראה כך:

1
2
3
4
5
6
private static void Main(string[] args)
{
MyClass instance = new MyClass();
instance.Test("Foo");
Console.ReadLine();
}

יש פה כמה קריאות כאלה לProfiling שלא ברור למה הוא עושה (הוא עושה Start ומיד לאחר מכן Stop), אבל אפשר לראות שלפונקציה Test הוא כן עושה Profiling (בודק כמה זמן לוקח לה לרוץ)

ככה הוא עושה על כל הTarget Projects/Binaries שאנחנו נותנים לו, וכך בעצם הוא יודע כמה זמן בערך לוקח לכל קריאה לרוץ. (שים לב שזה זמן יחסי, שהרי הקריאות לProfiling מאטות את הפונקציות האמיתיות. בין השאר, הדבר הזה לא משקף באופן אמין ביצועים של דברים שקשורים לIO כגון עבודה עם Streamים וכו’, להם עדיף להשתמש בMode של Sampling)

המשך יום עם ממשק מדידה טוב.

שתף

364. Implementing an interception framework - Compile-time IL weaving

בהמשך לטיפים הקודמים שהיו לפני כמה זמן, נראה עכשיו עוד שיטה ליצור 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ים של התשתית, למשל

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
[Serializable]
public class LogAttribute : OnMethodBoundaryAspect
{
private static readonly ILog mLog = LogManager.GetLogger(typeof (LogAttribute));
public override void OnEntry(MethodExecutionArgs args)
{
mLog.DebugFormat("Entering {0}, with parameters {1}",
args.Method,
GetParameters(args));
}
public override void OnExit(MethodExecutionArgs args)
{
mLog.DebugFormat("Exiting {0} with return value: {1}",
args.Method,
args.ReturnValue);
}
public override void OnException(MethodExecutionArgs args)
{
mLog.Error("An error occured on " + args.Method, args.Exception);
}
private static string GetParameters(MethodExecutionArgs args)
{
return string.Join
(",",
args.Method.GetParameters().Select((x, i) =>
new {x.Name, Index = i}).
Select(x => x.Name + ":" + args.Arguments[x.Index]).ToArray());
}
}

ואז לשים אותם מעל הפונקציות שלנו, למשל:

1
2
3
4
5
6
7
8
9
10
11
12
[Log]
public static int Factorial(int n)
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
return result;
}

בפועל יקרא הקוד שלנו. בואו נסתכל על הקוד שיוצא ב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
public static int Factorial(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 בכניסה למתודה, למרות שלא ראינו בקוד קריאה אליו.

למרות זאת, מדובר בכלי מאוד חזק שיכול להיות מאוד שימושי לכתיבת תשתיות.

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

שתף

363. DebuggerTypeProxy

[נכתב ע”י גיא דוברובסקי]

בוקר טוב,

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

החלטתי לעזור קצת לזלינגר ולתרום את חלקי לפורום בדמות הופעת אורח.

אתם מוזמנים לשלוח גם טיפים יומיים כדי שנעשה את הפורום הזה פעיל ופורה יותר – for us by us.

ובכן, אחרי ההקדמה הקצרה,

הטיפ היומי הוא בנושא DebuggerTypeProxy.

מה זה ולמה זה טוב?

קרה לכם פעם שדיבגתם אוביקטים של טיפוסים מורכבים?

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

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

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

קרה לכם שבעת דיבוג הופיע לכם מלא זבל שלא באמת מעניין אותכם?

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

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

DebuggerTypeProxy הינו attribute אשר מגדיר איך יראה האוביקט בזמן דיבאג.

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

  • פריטי המידע הרלוונטיים לא נוחים לגישה
  • רוצים להחביא פריטי מידע לא רלונטיים

טוב מראה עיניים ממשמע אוזניים:

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

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
class Student
{
public Student(string name)
{
_name = name;
_grades = new Dictionary<string, int>();
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private Dictionary<string, int> _grades;
public Dictionary<string, int> Grades
{
get { return _grades; }
set { _grades = value; }
}
private string _unimportantDetail;
public string UnimportantDetail
{
get { return _unimportantDetail; }
set { _unimportantDetail = value; }
}
private string _anotherBoringFact;
public string AnotherBoringFact
{
get { return _anotherBoringFact; }
set { _anotherBoringFact = value; }
}
}

ניצור instance של Student ונראה ונסתכל עליו בדיבאג:

1
2
3
4
5
6
7
8
Student dubrovski = new Student("Dubrovski");
dubrovski.Grades.Add("Infi", 80);
dubrovski.Grades.Add("Combi", 90);
dubrovski.Grades.Add("Tennis", 100);
dubrovski.UnimportantDetail = "some unimportant garbage that is not relevant for debugging";
dubrovski.AnotherBoringFact = "some boring info that is not relevant for debugging";
363_1.jpg

בעיות:

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

נפתור זאת ע"י יצירית DebuggerTypeProxy:

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
class StudentDebuggerTypeProxy
{
private Student _student;
public StudentDebuggerTypeProxy(Student student)
{
_student = student;
}
public string Name
{
get
{
return _student.Name;
}
}
public string Grades
{
get
{
IEnumerable<string> flatGrades =
_student.Grades.Select(item => item.Key + ":" + item.Value);
return String.Join(", ", flatGrades);
}
}
}

במחלקה זאת אנחנו פותרים את הבעיות לעיל ע"י:

  • הגדרת אופן תצוגת הציונים - בחרתי לשרשר אותם
  • חשיפת רק פריטי המידע הרלונטיים: בחרתי לא להציג את:
    • UnImportantDetail
    • AnotherBoringFact

הפעלת ה attribute שיצרנו תהיה באופן הבא:

1
2
3
4
5
[DebuggerTypeProxy(typeof(StudentDebuggerTypeProxy))]
class Student
{
// ...
}

וכעת בעת דיבוג של אוביקט מטיפוס Student, הוא יראה לנו בצורה הבאה:
363_2.jpg

כמובן שנוכל תמיד לראות את האוביקט המקורי ע"י בחירת Raw View:
363_3.jpg

דיבוג יעיל.

שתף

362. Using interception for transactions

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

נראה עוד שימוש בInterception והוא שימוש בשביל טרנזקציות:

נניח שיש לנו פונקציה שעובדת בטרנזקציה: כלומר בתחילה אליה אנחנו יוצרים טרנזקציה, ובסוף אנחנו מבצעים Commit במידה והצלחנו, ואחרת אנחנו עושים Rollback.

נוכל לכתוב Interceptor שמשתמש בטרנקציות. זה יראה משהו בסגנון הזה.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TransactionInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Transaction transaction = new Transaction();
try
{
invocation.Proceed();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback(ex);
throw;
}
}
}

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

למשל בהכנסה לDatabase לא נרצה לעשות Commit לפעולה לפני שכתבנו את כל הFKים.

המשך יום טרנזקציוני טוב.

שתף

361. Using interception to implicitly implement INotifyPropertyChanged

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

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

שימוש נוסף שיש לInterception הוא מימוש של הממשק INotifyProperyChanged בחינם.

למי שלא מכיר – הממשק INotifyPropertyChanged הוא ממשק שקיים בFramework ונראה כך:

1
2
3
4
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}

כשתכלס 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
public class Person : INotifyPropertyChanged
{
#region Data Members
private int mAge;
private string mName;
#endregion
#region Properties
public string Name
{
get
{
return mName;
}
set
{
string originalValue = mName;
mName = value;
if (originalValue != value)
{
RaisePropertyChanged("Name");
}
}
}
public int Age
{
get
{
return mAge;
}
set
{
int originalValue = mAge;
mAge = value;
if (originalValue != value)
{
RaisePropertyChanged("Age");
}
}
}
#endregion
#region Private Methods
private void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}

למי שאין כוח לקרוא: פשוט בSetterים של Properties בודקים אם באמת השתנה הערך של הProperty, ובמידה וכן – מקפיצים את הEvent.


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

פה נכנס העניין של Interception – נוכל לכתוב Interceptor שיודע להזריק את הקוד הזה לProperties שלנו.

איך זה יראה? בערך ככה:

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
public class NotifyPropertyChangedInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
PropertyInfo propertyInfo =
invocation.Method.GetProperty();
// Imaginary extension method
if (propertyInfo == null)
{
invocation.Proceed();
}
else
{
object originalValue =
propertyInfo.GetValue(invocation.InvocationTarget, null);
invocation.Proceed();
if (!Equals(originalValue, invocation.Arguments[0]))
{
RaisePropertyChanged(propertyInfo.Name);
}
}
}
}

זה Interceptor שעושה בערך את הקוד שראינו למעלה בProperties.

השאלה המתבקשת היא – מה עושה הפונקציה RaisePropertyChanged?

היינו רוצים שהיא תקפיץ Event, אבל זה קצת מסובך, כי מי אמר שהטיפוס שלנו בכלל מממש את INotifyPropertyChanged?

אז מה שאפשר לעשות זה את הדבר הבא:

ליצור Proxy לטיפוס שלנו שגם יממש את INotifyPropertyChanged, ולאחר מכן להתקין עליו את הInterception. זה קצת מסובך, אבל זה פתרון אפשרי.

אפשר גם ליצור Interface שיש לו פונקציה שמקפיצה את הEvent ולהכריח את מי שאנחנו עושים לו Proxy לממש אותו.


בהמשך נראה עוד Interceptorים ואחר כך נחזור למימושים אפשריים.

המשך יום שמודיע שהמאפיין השתנה טוב.

שתף