150. Calling generic methods with unknown generic type

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

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

1
2
3
4
public interface ICloneable<T>
{
T Clone();
}

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

היינו מנסים לכתוב קוד כזה:

1
2
3
4
5
6
7
8
9
10
ICollection<object> clonedObjects = new List<object>();
foreach (object current in enumerable)
{
if (current is ICloneable<>) // Compile error
{
ICloneable<> currentClonable = (ICloneable<>)current; // Compile error
clonedObjects.Add(currentClonable.Clone());
}
}

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

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

נוכל לפתור בעיה זו באמצעות שימוש בReflection וקריאה לMethodInfo המתאים:

ניצור פונקציה כזאת:

1
2
3
4
public static T Clone<T>(ICloneable<T> source)
{
return source.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
ICollection<object> clonedObjects = new List<object>();
foreach (object current in enumerable)
{
IEnumerable<Type> genericClonableTypes =
from type in current.GetType().GetInterfaces()
where type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(ICloneable<>)
select type;
if (genericClonableTypes.Any())
{
Type genericArgument =
genericClonableTypes.First().GetGenericArguments()[0];
MethodInfo cloneMethod =
typeof(SuperMethods).GetMethod("Clone").MakeGenericMethod(genericArgument);
object cloned =
cloneMethod.Invoke(null, new object[] {current});
clonedObjects.Add(cloned);
}
}

טוב, יש פה די הרבה קוד, אבל מה שקורה כאן זה שאנחנו בודקים עבור כל טיפוס אם הוא מממש ICloneable<> כלשהו, ובמידה וכן, אנחנו קוראים לפונקציה Clone (זו שכתובה למעלה) עם הפרמטר הגנרי של אותו ICloneable<>. כפי שכתבתי קודם, אם הטיפוסים מעוצבים מספיק טוב, לא כל כך נתקלים בבעיה זו.

למשל, אם היינו דואגים שיהיה גם ממשק לא גנרי, למשל:

1
2
3
4
public interface ICloneable
{
object Clone();
}

אם היינו דואגים שכל מי שיממש את הטיפוס ICloneable<T> יממש גם ICloneable, היינו יכולים פשוט לעשות הסבה לICloneable ולהשתמש בו.

אבל למה הכוונה לדאוג? אפשר לעשות שICloneable<T> יממש ICloneable, אבל אז צריך לממש את אותה מתודה פעמיים. (ומי מבטיח לנו שמממשים אותה נכון?)

אפשר לסמוך על המממשים שיממשו גם ICloneable וגם ICloneable<T>, אבל מה אם הם לא יממשו?

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

אנחנו נראה בעתיד עוד דרכים לפתור את הבעיה הזו. (ראו גם טיפ מספר 231)

סופ"ש גנרי משתקף לטובה

שתף