180. Activator CreateInstance alternative

הכרנו בעבר (טיפ מספר 139) את הפונקציה Activator.CreateInstance המאפשרת לנו ליצור אובייקט באופן דינאמי בזמן ריצה עפ”י הType שלו.

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

בהמשך (טיפ מספר 151) הכרנו את הפונקציה Delegate.CreateDelegate המאפשרת לנו למפות MethodInfo לDelegate, דבר המאפשר לנו לבצע קריאה מהירה יחסית לפונקציה בReflection, במחיר חד פעמי של יצירת הDelegate.

היינו רוצים לעשות דבר דומה – ליצור Delegate שמקבל את הפרמטרים של הConstructor ומחזיר לנו instance הנוצר מהפעלת הConstructor עם הפרמטרים הנתונים.

נראה שהפתרון הטבעי הוא להשתמש בDelegate.CreateDelegate ולהעביר לו את הConstructorInfo המתאים כדי ליצור Delegateכזה. למשל:

1
2
3
4
5
6
7
public class Circle : Shape
{
public Circle(double x, double y, double radius)
{
// ...
}
}

אז היינו מצפים ליצור את הDelegate המתאים בצורה הבאה:

1
2
3
4
5
6
7
8
9
Type circleType = typeof (Circle);
// We get this somehow through reflection
ConstructorInfo personConstructorInfo =
circleType.GetConstructor(new[] {typeof (double), typeof (double), typeof (double)});
Func<double, double, double, Shape> circleConstructor =
(Func<double, double, double, Shape>)
Delegate.CreateDelegate(typeof (Func<double, double, double, Shape>), personConstructorInfo);

ואז לקרוא לו ככה:

1
2
Shape circle = circleConstructor(3, 4, 5);
// Shape circle = new Circle(3, 4, 5);

ואז נוכל לבצע קריאה זו כמה פעמים שנרצה בצורה יעילה – כמעט זהה לקריאה שמתחתיה (עם עלות חד פעמית של יצירת הDelegate).

הכול נראה טוב ויפה, אלא שהדבר הזה לא עובד. למה? כפי שראינו בטיפ מספר 178, קריאה לConstructor הוא באמצעות הפעולה הILית newobj לעומת קריאה למתודה שמתבצע באמצעות הפעולה call או callvirt.

לכן Delegate.CreateDelegate מקבל MethodInfo ולא MethodBase, ולכן אי אפשר להעביר אל הoverload שלוConstructorInfo.


מה נוכל לעשות במקום?

אי אפשר ליצור Delegate שמצביע לConstructor, אבל אפשר ליצור מתודה דינאמית בזמן ריצה שקוראת לConstructor ואז ליצור Delegate אל המתודה הזאת.

זה די קל ודומה למה שראינו אתמול (טיפ מספר 179):

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
public static class Constructor
{
public static TDelegate CreateDelegate<TDelegate>(Type givenType)
{
ConstructorInfo requestedConstructor =
GetConstructorInfo(givenType, typeof (TDelegate));
ParameterInfo[] parameterInfos = requestedConstructor.GetParameters();
List<ParameterExpression> parameterExpressions =
new List<ParameterExpression>(parameterInfos.Length);
foreach (ParameterInfo parameterInfo in parameterInfos)
{
parameterExpressions.Add
(Expression.Parameter(parameterInfo.ParameterType,
parameterInfo.Name));
}
NewExpression constructorCall =
Expression.New(requestedConstructor,
parameterExpressions.ToArray());
Expression<TDelegate> constructorLambda =
Expression.Lambda<TDelegate>(constructorCall,
parameterExpressions);
return constructorLambda.Compile();
}
private static ConstructorInfo GetConstructorInfo(Type givenType, Type delegateType)
{
MethodInfo invoke = delegateType.GetMethod("Invoke");
ParameterInfo[] constructorParameters = invoke.GetParameters();
Type[] constructorParameterTypes =
constructorParameters.Select(x => x.ParameterType).ToArray();
ConstructorInfo requestedConstructor =
givenType.GetConstructor(constructorParameterTypes);
return requestedConstructor;
}
}

מה שקורה בהתחלה זה שאנחנו משיגים את הConstructorInfo המתאים עפ"י הType שאנחנו רוצים ליצור והDelegate שקיבלנו. זאת באמצעות קריאה לפונקציה GetConstructorInfo שפשוט מוצאת את הפרמטרים של הDelegate ע"י סריקת הפונקציה Invoke – זו פונקציה שיש לכל Delegate שנקראת כאשר אנחנו מפעילים אותו. היא מקבלת, בין השאר, את הפרמטרים של הDelegate שלנו.

אחר כך אנחנו יוצרים ParameterExpressionים שמייצגים כל אחד מהארגומנטים של הConstructor ע"י בדיקת הפרמטרים שלו. לאחר מכן אנחנו יוצרים NewExpression המייצג את הפעלת האופרטור new על הConstructor המתאים. לבסוף אנחנו יוצרים Lambda Expression מהסוג המתאים שהגוף שלו הוא הNewExpression והפרמטרים שלו הם הפרמטרים שיצרנו קודם. אנחנו מסיימים את הפונקציה ע"י קימפול הExpression לDelegate מתאים 😃

זהו, כעת נוכל לקבל Delegate שמעניין אותו ולהפעיל אותו כמה פעמים שנרצה:

1
2
3
4
5
6
7
8
9
10
11
12
Type circleType = typeof(Circle);
// We get this somehow through reflection
Func<double, double, double, Shape> circleConstructor =
Constructor.CreateDelegate<Func<double, double, double, Shape>>(circleType);
for (int i = 0; i <= 10000; i++)
{
Shape circle = circleConstructor(3, 4, 5);
// Much faster than
//Shape circle = (Shape)Activator.CreateInstance(circleType, 3, 4, 5);
}

הקריאה הזו תהיה הרבה יותר מהירה מהקריאה באמצעות Invoke של ConstructorInfo או Activator.CreateInstance, מאחר ובקריאות לפונקציות האחרונות שצוינו מתבצע Binding. (ראו טיפ 179)

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

שימו לב שיש עלות חד-פעמית של יצירת הDelegate (הפעולה הכבדה היא Compile).


הערה:

יש עוד דרך ליצור אובייקט ע"י שימוש במתודה:

FormatterServices.GetUninitializedObject(), המתודה יוצרת אובייקט "לא מאותחל" ללא קריאה לctor שלו (הדרך הזאת יותר מהירה אפילו מnew). הבעיה בשימוש הזה שהאובייקט שמוחזר לא נוצר ב"state" ולידי ולכן מומלץ להשתמש בה (כמו שכתוב בMSDN) רק אם מאתחלים את כל השדות של האובייקט מיד לאחר השימוש במתודה. לדוגמה הDeserialize של WCF משתמש במתודה הזאת כדי ליצור את האובייקט ואז מאתחל את כל השדות שלו מתוך הstream.

המשך יום מבוטא דינאמית טוב

שתף

179. Dynamic clone method

היום נראה שימוש מגניב בExpression Trees.

מכירים את זה שיש לכם אובייקט ואתם רוצים לשכפל אותו? כלומר ליצור instance חדש שמכיל את אותם Properties?

בשביל זה המציאו את ICloneable. אבל יש עם זה מספר בעיות:

הראשונה היא שלא חובה לממש ICloneable, ולכן אי אפשר להסתמך על זה שזה מממומש.

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

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

דבר כזה די קל לכתוב בReflection:

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 static class Cloner<T>
{
public static T Clone(T source)
{
Type givenType = typeof(T);
ConstructorInfo constructorInfo =
givenType.GetConstructor(new Type[0]);
IEnumerable<PropertyInfo> interestingProperties =
from property in givenType.GetProperties()
where property.CanWrite && property.CanRead
select property;
T result = (T)constructorInfo.Invoke(null);
foreach (PropertyInfo interestingProperty in interestingProperties)
{
interestingProperty.SetValue(result,
interestingProperty.GetValue(source,null),
null);
}
return result;
}
}

מחפשים את כל הProperties שיש להם גם Getter וגם Setter ומעתיקים מהSource לTarget.

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

הסיבה לכך היא הBinding של המתודות. כזכור, בקריאה לInvoke של MethodBase מתבצע פענוח כיצד יש לשלוח את הפרמטרים לפונקציה וכיצד יש לקבל את ערך ההחזר חזרה וכו’. (טיפ מספר 151)

יש דברים שאפשר לשפר. למשל, אפשר לעשות Cache לinterestingProperties, אבל בסה"כ נשאר עם ביצועים גרועים.­

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

למה הכוונה? נניח שיש לנו את המחלקה המפורסמת Person. איך היה נראה Delegate שמשכפל אותה? משהו כזה:

1
2
3
4
5
6
Func<Person, Person> personCloner =
x => new Person()
{
FirstName = x.FirstName,
LastName = x.LastName
};

אם נקמפל את זה לExpression במקום:

1
2
3
4
5
6
Expression<Func<Person, Person>> personCloner =
x => new Person()
{
FirstName = x.FirstName,
LastName = x.LastName
};

זה מתקמפל למשהו כזה (שוב תודה ל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
Type personType = typeof (Person);
ConstructorInfo constructorInfo = personType.GetConstructor(new Type[0]);
PropertyInfo firstNameInfo =
personType.GetProperty("FirstName");
PropertyInfo lastNameInfo =
personType.GetProperty("LastName");
ParameterExpression parameterX = Expression.Parameter(personType, "x");
Expression<Func<Person, Person>> personClone =
Expression.Lambda<Func<Person, Person>>(
Expression.MemberInit(
Expression.New(constructorInfo, new Expression[0]),
new MemberBinding[]
{
Expression.Bind(firstNameInfo.GetSetMethod(),
Expression.Property(parameterX, firstNameInfo.GetGetMethod())),
Expression.Bind(lastNameInfo.GetSetMethod(),
Expression.Property(parameterX, lastNameInfo.GetGetMethod()))
}),
new ParameterExpression[] {parameterX});

מה שנעשה עכשיו זה שניצור Expression דומה בזמן ריצה עפ"י הProperties ששלפנו קודם (מה שהכנסנו למשתנה interestingProperties):

זה נראה משהו כזה:

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
public static class Cloner<T>
{
private static Func<T, T> mCloner;
public static T Clone(T source)
{
if (mCloner == null)
{
mCloner = CreateCloneDelegate();
}
return mCloner(source);
}
private static Func<T, T> CreateCloneDelegate()
{
Type givenType = typeof(T);
ConstructorInfo constructorInfo =
givenType.GetConstructor(new Type[0]);
IEnumerable<PropertyInfo> interestingProperties =
from property in givenType.GetProperties()
where property.CanWrite && property.CanRead
select property;
ParameterExpression parameterX =
Expression.Parameter(givenType, "x");
MemberAssignment[] memberBindings =
(from property in interestingProperties
let getter = Expression.Property(parameterX, property.GetGetMethod())
let setter = Expression.Bind(property.GetSetMethod(), getter)
select setter).ToArray();
Expression<Func<T, T>> cloner =
Expression.Lambda<Func<T, T>>(
Expression.MemberInit(
Expression.New(constructorInfo, new Expression[0]),
memberBindings),
new ParameterExpression[] { parameterX });
return cloner.Compile();
}
}

המתודה שיוצרת את הDelegate היא CreateCloneDelegate.

מה שהיא עושה זה פשוט מחקה את הבנייה של הExpression שראינו קודם בעזרת הReflector.

שימו לב שהשורה האחרונה (זו לפני הCompile) אחראית ליצור את הExpression וכל השאר פשוט יוצרות את ההשמות של הProperties, בצורה המחקה את מה שראינו בReflector, בשיטה הדומה לזו שהשתמשנו בה קודם לכן.

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

(ראו גם טיפים 177,174)

השימוש במחלקה הוא משהו בסגנון:

1
2
3
4
5
6
7
8
9
Person person =
new Person
{
FirstName = "Maor",
LastName = "C",
Age = 37
};
Person clonedPerson = Cloner<Person>.Clone(person);

נוכל לבצע קריאה זו מספר רב של פעמים בצורה מהירה.

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

1
2
3
4
5
6
7
public static class Cloner
{
public static T Clone<T>(T source)
{
return Cloner<T>.Clone(source);
}
}

וקריאה:

1
2
3
4
5
6
7
8
9
Person person =
new Person
{
FirstName = "Maor",
LastName = "C",
Age = 37
};
Person clonedPerson = Cloner.Clone(person);

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

בנוסף, אין התמודדות עם אוספים (רשימה/Dictionary).

המטרה כאן היא להראות דוגמא של יצירת מתודה בזמן ריצה. כמובן אפשר לשפר אותה.

בכל מקרה, זה די מגניב.

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

המשך יום דינמי משוכפל טוב

שתף

178. ConstructorInfo

ראינו בעבר איך אפשר לקבל Properties ומתודות של טיפוס מסוים בReflection.

ראינו גם שאפשר ליצור instance של טיפוס מסוים בעזרת Activator.CreateInstance.

(טיפים 139,146,158)

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

למשל, נניח שיש לנו שני Constructorים:

1
2
3
4
5
6
7
8
9
10
public class Person
{
public Person(Person source)
{
}
public Person(string name)
{
}
}

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

1
2
object person =
Activator.CreateInstance(typeof (Person), new object[] {null});

אז אנחנו נחטוף Exception:

Ambiguous match found.


מה הסיפור? null יכול להתאים לשני הConstructorים ולכן Activator.CreateInstance לא יודע לאיזה Constructor לנתב את הקריאה.

המקרה עם null הוא לא כזה מונפץ, מאחר וייתכן ואחד האובייקטים שאנחנו רוצים להעביר לConstructor הוא Instance ששווה לnull.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Circle : Shape, IColored
{
public Circle()
{
}
public Circle(IColored source)
{
}
public Circle(Shape source)
{
}
}
Circle myCircle =
(Circle)Activator.CreateInstance(typeof (Circle), new Circle());

נחטוף Exception כיוון שהוא לא יודע לאן לנתב את הCircle.

איך אפשר לפתור את זה? כמו שיש MethodInfo שמתאר מתודות, יש גם ConstructorInfo המייצג metadata של Constructorים.

נוכל להשתמש בזה בצורה הבאה:

1
2
3
4
5
ConstructorInfo stringConstructorInfo =
typeof (Person).GetConstructor(new Type[] {typeof (string)});
Person myPerson =
(Person)stringConstructorInfo.Invoke(new object[] {null});

ככה לא נעוף.


יש דמיון בין ConstructorInfo לMethodInfo ולמעשה שניהם יורשים מהמחלקה MethodBase.

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

במקרה של MethodInfo צריכה להתבצע קריאה הדומה לפקודות הIL ששמן Call וCallvirt. במקרה של ConstructorInfo צריכה להתבצע קריאה הדומה לפקודת הIL ששמה newobj.

זו אחת הסיבות שאי אפשר להתייחס לConstructorים כמו מתודות רגילות, ולמשל אי אפשר ליצור delegate שמצביע לConstructor.

שיהיה המשך יום מידע בנוי טוב

שתף

177. Static generic classes

הכרנו בעבר (טיפים מספר 26-34) מחלקות ומתודות גנריות.

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

מה שלא כל כך מובן מאליו הוא היכולת הבאה:

נניח שיש לנו מחלקה גנרית סטטית:

1
2
3
4
5
6
7
8
9
10
11
12
public static class MyStaticClass<T>
{
private static object mMyObject = new object();
public static object MyObject
{
get
{
return mMyObject;
}
}
}

מה תחזיר השורה הבאה?

1
2
3
4
if (MyStaticClass<string>.MyObject == MyStaticClass<int>.MyObject)
{
// false
}

התנאי יחזיר שקר. הסיבה היא שעבור כל T נוצרת מחלקה (סטטית) אחרת בזמן ריצה ולכן יש לה Member משלה.

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

למשל, נוכל לכתוב מחלקה כזו:

1
2
3
4
5
6
7
8
9
10
11
12
public static class EmptyArray<T>
{
private static readonly T[] mInstance = new T[] {};
public static T[] Instance
{
get
{
return mInstance;
}
}
}

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

1
2
3
4
5
6
7
8
int[] emptyIntArray = EmptyArray<int>.Instance;
string[] emptyStringArray = EmptyArray<string>.Instance;
string[] anotherEmptyStringArray = EmptyArray<string>.Instance;
if (emptyStringArray == anotherEmptyStringArray)
{
// true
}

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

ראינו מספר דרכים לעשות זאת, ביניהם טיפים מספר 50, 139, 170.

נוכל לנצל שיטה זו כדי ליצור Factory פר טיפוס שמזכיר את טיפ מספר 50:

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
public static class Factory<T>
{
private static Func<T> mBuilder;
public static void RegisterBuilder(Func<T> builder)
{
if (mBuilder != null)
{
throw new ArgumentException("Builder was already initialized.",
"builder");
}
mBuilder = builder;
}
public static T Build()
{
if (mBuilder == null)
{
throw new ArgumentException("Builder wasn't initialized.");
}
return mBuilder();
}
}

ונוכל להשתמש בו כך:

1
2
Factory<Shape>.RegisterBuilder(() => new Triangle());
Shape shape = Factory<Shape>.Build(); // Triangle

זה מאוד מזכיר את טיפ מספר 50, אבל יש הבדל קטן. שם אנחנו שומרים את הDelegateים (הBuilderים) בDictionary ולכן בכל גישה אליו מתבצע חיפוש.

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

שימו לב שהFactory הזה ממפה טיפוס לDelegate אחד, בניגוד לFactoryים שראינו בעבר שממפים מחרוזות לDelegateים מתאימים. (כמובן, אפשר לטפל בזה עם לא הרבה עבודה)


הטריק הזה שימושי עבור חישובים שהם כבדים, אבל הם סטטיים פר טיפוס. למשל, התעסקות בReflection – נניח ואנחנו רוצים למצוא את כל הטיפוסים שמממשים ממשק מסוים או את כל המתודות שיש מעליהן Attribute מסוג מסוים. חישובים כאלה יכולים להתבצע פעם אחת (בעליית האפליקציה, או בפעם הראשונה שהחישוב מתבצע), ובד"כ אין טעם שיתבצעו יותר מפעם אחת.

המשך ערב סטטי גנרי טוב

שתף

176. Typed safe method & property names

ראינו בעבר כיצד אפשר להשתמש בReflection כדי להשיג MethodInfo/PropertyInfo של מתודה או Property נתון. (ראו גם טיפים 146,147,158)

הבעיה בעסק הזה היא שאנחנו מקבלים הרבה stringים Hard-Coded שמייצגים את השם של האלמנט שאנחנו מעוניינים להשיג בReflection.

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

בניגוד לTypeים שיש בשבילם את הKeyword ששמו typeof, אין מקבילה למתודות/Properties המאפשרת לנו להשיג אותם בצורה שהיא Typed safe.

אפשר להשתמש בExpression Trees כדי להשיג את השמות האלה בצורה אחרת:

1
2
3
4
5
6
7
8
Expression<Func<Person, int>> myProperty =
(Person x) => x.Age;
MemberExpression myPropertyBody =
(MemberExpression) myProperty.Body;
PropertyInfo ageInfo = (PropertyInfo) myPropertyBody.Member;
// Same result as PropertyInfo ageInfo = typeof(Person).GetProperty("Age");

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

1
2
3
4
5
6
7
8
9
10
11
12
public static PropertyInfo GetProperty<TSource, TResult>(Expression<Func<TSource, TResult>> expression)
{
MemberExpression expressionBody =
expression.Body as MemberExpression;
if (expressionBody == null)
{
return null;
}
return expressionBody.Member as PropertyInfo;
}

ולקרוא לה כך:

1
2
PropertyInfo ageInfo = GetProperty((Person x) => x.Age);
// Same result as PropertyInfo ageInfo = typeof(Person).GetProperty("Age");

בקשר למתודות, נוכל לעשות משהו דומה:

1
2
3
4
5
6
7
8
9
10
11
12
public static MethodInfo GetMethod<TSource, TResult>(Expression<Func<TSource, TResult>> expression)
{
MethodCallExpression expressionBody =
expression.Body as MethodCallExpression;
if (expressionBody == null)
{
return null;
}
return expressionBody.Method;
}

ולהשתמש בה כך:

נניח שיש לנו את המתודה הזאת:

1
2
3
4
5
6
7
public class Circle : Shape
{
public Square GetBoundedSquare(double angle, Color color)
{
// ...
}
}

אזי נשתמש במתודה GetMethod כך:

1
2
3
MethodInfo getBoundedSquareInfo =
GetMethod((Circle x) => x.GetBoundedSquare(default(int), default(Color)));
// Same result as MethodInfo getBoundedSquareInfo = typeof(Circle).GetMethod("GetBoundedSquare");

שימו לב שאין פה stringים שהם Hard Coded. במידה והפונקציה תחליף שם או חתימה, הקוד לא יתקמפל.

במידה ונרצה לבצע אותו הדבר למתודות שהן סטטיות/שאינן מחזירות ערך, נצטרך להוסיף Overloadים מתאימים.

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

1
2
3
MethodInfo getBoundedSquareInfo =
GetMethod((Circle x) => x.GetBoundedSquare(3, Color.Red));
// Same result as MethodInfo getBoundedSquareInfo = typeof(Circle).GetMethod("GetBoundedSquare");

מה שדי דוחה, אבל אפשר להפוך את החיסרון ליתרון! מאחר ואנחנו חייבים לציין ערכים – נציין את הדיפולטים של הערכים שאנחנו מחפשים (ראו למעלה), וככה נקבל את החתימה המדויקת שאנחנו מחפשים! 😃

שימו לב שבשיטה הזאת אנחנו לא מסוגלים לגשת לMemberים שהם לא חשופים לנו (למשל, private מחוץ למחלקה שלנו). יש לזה גם יתרונות, כמובן.

סופ"ש מצוין

שתף

175. Solving 1,5,6,7

פעם שעברה ראינו שאפשר לקמפל Expression Tree לAnonymous Delegate.

היום נראה משהו יפה שאפשר לעשות עם זה.

יש חידה מפורסמת שהולכת ככה: נתונים המספרים 1,5,6,7 (כל מספר בדיוק פעם אחת). איך אפשר להגיע למספר 21 ע”י הוספת פעולות חשבון וסוגריים בין המספרים?

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

נראה איך אפשר לפתור אותה באמצעות Expression Trees.

ראשית נכתוב משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static IEnumerable<Expression> GetAllExpressions(IEnumerable<double> numbers)
{
if (numbers.Count() == 1)
{
yield return Expression.Constant(numbers.First());
}
foreach (double number in numbers)
{
IEnumerable<Expression> expressionsWithoutNumber =
GetAllExpressions(numbers.Except(new[] {number}));
foreach (Expression expression in expressionsWithoutNumber)
{
yield return Expression.Add(Expression.Constant(number), expression);
yield return Expression.Subtract(Expression.Constant(number), expression);
yield return Expression.Multiply(Expression.Constant(number), expression);
yield return Expression.Divide(Expression.Constant(number), expression);
}
}
}

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

איך זה עובד?

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

אחר כך, אנחנו מחליטים מי יהיה האיבר השמאלי ביותר. אחרי זה אנחנו מחשבים את כל הביטויים האפשריים של אוסף המספרים שאינו כולל את האיבר השמאלי ביותר.

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

(Expression.Constant מייצג איבר שהוא קבוע בExpression Tree, בניגוד לParameter שמייצג פרמטר של הExpression)

זה מאוד מזכיר את הקוד שראינו שמחשב את כל התמורות של אוסף נתון.

הקוד נראה בסדר, אלא שיש בעיה קטנה: אסור לחלק באפס. נוכל לפתור זאת ע"י בדיקה לפני הוספת חלוקה האם הביטוי בו אנחנו מחלקים הוא 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
private static IEnumerable<Expression> GetAllExpressions(IEnumerable<double> numbers)
{
if (numbers.Count() == 1)
{
yield return Expression.Constant(numbers.FirstOrDefault());
}
foreach (double number in numbers)
{
var expressionsWithoutNumber =
GetAllExpressions(numbers.Except(new[] {number}));
foreach (Expression expression in expressionsWithoutNumber)
{
yield return Expression.Add(Expression.Constant(number), expression);
yield return Expression.Subtract(Expression.Constant(number), expression);
yield return Expression.Multiply(Expression.Constant(number), expression);
Func<double> denominator =
Expression.Lambda<Func<double>>(expression).Compile();
if (denominator() != 0)
{
yield return Expression.Divide(Expression.Constant(number), expression);
}
}
}
}

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

כעת נוכל לפתור את החידה:

נקרא לפונקציה GetAllExpressions עם האוסף 1,5,6,7 ונבדוק מתי יוצא 21:

שוב, אנחנו עובדים עם Expressionים. נצטרך לקמפל אותם ולהבין מתי זה יוצא 21:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IEnumerable<Expression> expressions =
GetAllExpressions(new double[] {1, 5, 6, 7});
foreach (Expression currentExpression in expressions)
{
Expression<Func<double>> currentLambda =
Expression.Lambda<Func<double>>(currentExpression);
Func<double> currentDelegate = currentLambda.Compile();
// We check when the expression represents 21
if (currentDelegate() == 21)
{
Console.WriteLine("21 = {0}", currentExpression);
}
}
// 21 = (6 / (1 - (5 / 7)))

יאללה, $ \displaystyle{21 = \frac{6}{1 - \frac{5}{7}} = 6\div\left(1-5\div7\right) } $. מי היה מאמין?

הערה: הריצה על הערכים כאן עובדת במקרה: היא מחפשת רק ביטויים מצורה מאוד ספציפית: $ a\#\left(b\#\left(c\#d\right)\right) $. במקום זאת, הדבר הנכון לעשות זה לרוץ על כל התת-קבוצות של האיברים שקיבלנו: על כל תת-קבוצה למצוא את כל הערכים שהיא מניבה, לעשות כנ"ל על הקבוצה המשלימה שלה, ולבצע על כל האפשרויות של שתיהן את 4 פעולות החשבון.

המשך ערב טוב

שתף

174. Expression{TDelegate}.Compile

הכרנו בפעם שעברה את המושג של Expression Trees בC#.

אחד הדברים השימושיים שאפשר לעשות עם Expressionים מסוג Lambda Expression זה לקמפל אותם למתודה חיות.

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

1
2
3
4
5
6
7
Expression<Func<int, int, int>> sumOfSquaresExpression =
(x, y) => x * x + y * y;
Func<int, int, int> sumOfSquaresFunction =
sumOfSquaresExpression.Compile();
int fiveSquared = sumOfSquaresFunction(3, 4); // 25

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

1
2
Func<int, int, int> sumOfSquaresFunction =
(x, y) => x * x + y * y;

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

1
2
3
4
5
6
7
8
9
10
11
Expression<Func<int, int, int>> sumOfSquaresExpression =
Expression.Lambda<Func<int, int, int>>(
Expression.Add(
Expression.Multiply(parameterX, parameterX),
Expression.Multiply(parameterY, parameterY)),
new ParameterExpression[] {parameterX, parameterY});
Func<int, int, int> sumOfSquaresFunction =
sumOfSquaresExpression.Compile();
int fiveSquared = sumOfSquaresFunction(3, 4); // 25

מה שקורה כאן זה שאנחנו מקמפלים Expression Tree לAnonymous Delegate.

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

נראה דוגמאות לזה בהמשך.

יום טוב

שתף

173. Expression Trees

אחד הFeatureים היותר חזקים בC# 3.0 שלמרבה הצער, גם פחות מוכרים הוא Expression Trees.

מה זה בעצם?

הכרנו בעבר את הSyntax של Lambda Expressions – ראו גם טיפ מספר 45.

בזכות Lambda Expressions, אנחנו מסוגלים לכתוב Anonymous Delegates בצורה פשוטה, למשל:

1
2
Func<int, int> squareFunction = x => x*x;
int nine = squareFunction(3); // 9

או למשל

1
2
Func<int, int, int> sumOfSquares = (x, y) => x*x + y*y;
int fiveSquared = sumOfSquares(3, 4); // 25

ראינו כל מיני דוגמאות שבהן Lambda Expressions הם שימושיים, למשל בכל Linq.

מסתבר שהקומפיילר מאפשר לנו לקמפל Lambda Expression בשתי דרכים.

הדרך הראשונה, שבה נתקלנו כבר היא לAnonymous Delegate. במקרה זה, מה שקורה זה שנוצרת לנו מחלקה/מתודה מאחורי הקלעים שהDelegate מפנה אליה. (ראו גם טיפים 41, 44)

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

למשל,

1
Expression<Func<int, int, int>> sumOfSquares = (x, y) => x*x + y*y;

הקוד יתקמפל למשהו כזה: (תודה לReflector)

1
2
3
4
5
6
7
8
9
ParameterExpression parameter = Expression.Parameter(typeof(int), "x");
ParameterExpression parameter = Expression.Parameter(typeof(int), "y");
Expression<Func<int, int, int>> sumOfSquares =
Expression.Lambda<Func<int, int, int>>(
Expression.Add(
Expression.Multiply(parameterX, parameterX),
Expression.Multiply(parameterY, parameterY)),
new ParameterExpression[] { parameterX, parameterY });

מה כתוב כאן? ובכן, הקוד הזה בעצם מתאר את הביטוי. אם נסתכל מבפנים, נראה קודם כל את הExpression.Multiplyים שהם בסה"כ מייצגים את הביטויים x*x וy*y בהתאמה.

לאחר מכן, מופיע פה Expression.Add בין הExpression.Multiplyים שמייצג את ה+ בביטוי המקורי.

בסוף הExpression.Lambda מצוין שמדובר בגוף פונקציה עם חתימה מסוימת, ומצוינים הפרמטרים שלה.

זה נחמד.


בשביל מה צריך את הביטויים האלה?

באופן כללי הביטויים האלה הם חלק מהNamespace של System.Linq.Expressions. הם הומצאו בשביל מטרה מאוד פשוטה – הכרנו את Linq עלEnumerableים.

מה אם אנחנו רוצים לעשות שאילתות LINQ על מקור מרוחק, למשל Database?

למשל, יש לנו טבלה מאוד גדולה של עובדים ואנחנו רוצים לשלוף את כל העובדים שיש להם משכורת גדולה מ4000 שקלים.

דרך אחת לעשות זאת היא לשלוף את כל הטבלה לאפליקציה ולהריץ עליה את הLINQ:

1
2
3
4
5
6
IEnumerable<Worker> workers = GetAllWorkersFromDb(); // This is heavy!
IEnumerable<Worker> interestingWorkers =
from worker in workers
where worker.Salary > 4000
select worker;

אלא שזה כבד! אנחנו שולפים את כל העובדים בעולם לאפליקציה – מה שגם מכביד על הDB, גם על התקשורת וגם מפוצץ לאפליקציה את הזכרון.

במקום זאת, החליטו לאפשר דרך שנייה:

מהי הדרך? כידוע (טיפים 95-99), שאילתות LINQ מתקמפלות לשרשור של הרכבות של Extension Methods עם Lambda Expressions מתאימים. במקום זאת, ידאגו לקמפל את הLambda Expressions לExpression Tress ותתבצע בצד הלקוח המרה של הExpression Tree לשאילתא שתרוץ מול הDB וכך תחזיר את התוצאות הנכונות במהירות הטובה ביותר.

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

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

המשך יום טוב

שתף

172. Permutations code

לחלק מאיתנו עלה הצורך בעבר למצוא את כל התמורות של אוסף מסוים.

כלומר, למצוא את כל הסידורים האפשריים של אוסף מסוים.

לכתוב קוד כזה בד”כ דורש רקורסיה ויוצא משהו די מכוער.

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

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
public static IEnumerable<IEnumerable<T>> GetAllPermutations<T>(IEnumerable<T> collection)
{
if (!collection.Any())
{
// There is exactly one permutation of length 0
// the empty permutation.
yield return Enumerable.Empty<T>();
}
else
{
foreach (T firstElement in collection)
{
// firstElement will be the first element
// of our permutation.
// We now find all permutations of all
// elements except him.
IEnumerable<T> withoutCurrent =
collection.Except(new[] {firstElement});
// Get the permutations (using recursive call)
IEnumerable<IEnumerable<T>> permutationsWithoutCurrent =
GetAllPermutations(withoutCurrent);
// Iterate over the permutations of the rest elements.
foreach (IEnumerable<T> currentPermutation in permutationsWithoutCurrent)
{
// Put firstElement as the first element :)
IEnumerable<T> currentFullPermutation =
new[] {firstElement}.Concat(currentPermutation);
yield return currentFullPermutation;
}
}
}

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

איך זה עובד? כתבתי תיעוד, אבל מה שקורה זה ככה:

אם קיבלנו אוסף ריק, אנחנו מחזירים אוסף שמכיל את האוסף הריק.

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

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

מה שעוד יפה בפונקציה זו, זה שבגלל שהיא משתמשת בyield return , אזי היא Lazy ולכן אינה מפוצצת את הזכרון בכל התמורות בקריאה אליה.

ראו גם טיפים מספר 54,55,105

שבוע טוב

שתף

171. Attribute GetCustomAttributes

כתבתי בטיפ על AttributeUsage שאפשר לשים Inherited = true, ואז אם נקרא לGetCustomAttributes עם הפרמטר true, נקבל את הAttribute, גם אם הMember נדרס במחלקות הבנות.

אכן, אם נעשה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ParentClass
{
[Cool]
public virtual void NiceMethod()
{
}
}
public class ChildClass : ParentClass
{
public override void NiceMethod()
{
Console.WriteLine("This method is nice!");
}
}

ונקרא לשורות הבאות:

1
2
3
4
5
6
7
8
MethodInfo niceMethodInfo =
typeof (ChildClass).GetMethod("NiceMethod");
object[] niceMethodInfoAttributes =
niceMethodInfo.GetCustomAttributes(true);
if (niceMethodInfoAttributes.Length > 0)
{
// true
}

נכנס לתנאי.

זה יפה, אבל תכף יבוא מישהו ויגיד שהטיפ היומי הוליך אותו שולל. למה?

כי אם נעשה אותו הדבר עם Property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ParentClass
{
[Cool]
public virtual string NiceProperty
{
get
{
return string.Empty;
}
}
}
public class ChildClass : ParentClass
{
public override string NiceProperty
{
get
{
return "This property is nice!";
}
}
}

ונקרא לאותן שורות:

1
2
3
4
5
6
7
8
9
10
PropertyInfo nicePropertyInfo =
typeof (ChildClass).GetProperty("NiceProperty");
object[] nicePropertyInfoAttributes =
nicePropertyInfo.GetCustomAttributes(true);
if (nicePropertyInfoAttributes.Length > 0)
{
// false
}

לא נכנס לתנאי!


מה לעזאזל קורה כאן?

לדעתי מדובר בבאג, ואם נסתכל בReflector על RuntimePropertyInfo (זהו המימוש של המחלקה האבסטרקטית PropertyInfo) על המתודה

1
2
3
4
5
6
public override object[] GetCustomAttributes(bool inherit)
{
return CustomAttribute.GetCustomAttributes
(this,
typeof (object) as RuntimeType);
}

לא כל כך מעניין אותנו מה עושה הפונקציה CustomAttribute.GetCustomAttributes, אבל שימו לב להתעלמות המדהימה מהפרמטר inherit.

לא כל כך ברור…


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

אחד התותחים האמיתיים נתקל בבעיה זו ומצא לה פתרון. מסתבר שקיימת פונקציה בשם Attribute.GetCustomAttributes שיודעת לטפל בProperties:

1
2
3
4
5
6
7
8
9
10
PropertyInfo nicePropertyInfo =
typeof (ChildClass).GetProperty("NiceProperty");
object[] nicePropertyInfoAttributes =
Attribute.GetCustomAttributes(nicePropertyInfo, true);
if (nicePropertyInfoAttributes.Length > 0)
{
// true
}

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

סופ"ש בר סגולה

שתף