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.

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

שתף