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.

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

שתף