231. Calling generic methods with unknown generic type, strikes back

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

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

תזכורת: יש לנו איזשהו ממשק גנרי:

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

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

הדרך שלא עובדת:

1
2
3
4
5
6
7
8
9
10
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
if (current is ICloneable<>)
{
ICloneable<> cloneable = (ICloneable<>) current;
result.Add(cloneable.Clone());
}
}

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

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

כעת נראה איך אפשר לפתור בעיה זו בעזרת Dynamic Binding של C# 4.0.

הפתרון הראשון הוא כזה:

1
2
3
4
5
6
7
8
9
10
11
12
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
try
{
dynamic dynamicCurrent = current;
result.Add(dynamicCurrent.Clone());
}
catch (RuntimeBinderException e)
{
}
}

פתרון זה בעייתי ממספר סיבות:

  1. הוא ממש מכוער – אנחנו מוודאים האם הפונקציה קיימת או לא ע"י תפיסת Exception.
  2. הוא שגוי לוגית – בשום מקום אנחנו לא מוודאים שהטיפוס מממש את ICloneable<T>, כך שאם יש לאובייקט שאנחנו מריצים עליו את הפונקציה פונקציה Clone אחרת, עדיין היא תכנס לתוצאה.
  3. הפתרון הזה יכול לא לעבוד אפילו אם הטיפוס שלנו מממש ICloneable<T>. מה זאת אומרת? כשדיברנו על Dynamic Binding לראשונה, ציינתי שDynamic Binding יודע למצוא רק מתודות שהן public. ובכן, אם הטיפוס שלנו מממש את הממשק, אבל באופן Explicitly, יזרק לנו Exception, ולכן הוא לא עושה את מה שרצינו. (ראו גם טיפ מספר 82)

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

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

נוכל לנצל עובדה זו כדי לפתור את הבעיה הנ"ל:

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

1
2
3
4
private static T Clone<T>(ICloneable<T> source)
{
return source.Clone();
}

ואז נכתוב את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
12
13
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
try
{
dynamic dynamicCurrent = current;
result.Add(Clone(dynamicCurrent));
}
catch (RuntimeBinderException e)
{
}
}

כעת התגברנו על בעיות 2 ו3 – לא נכנס לפונקציה Clone במידה ואנחנו לא מממשים את הממשק. במידה וכן, הInstance שלנו הוא כבר מסוג ICloneable<T> ולכן נוכל לקרוא לפונקציה של הממשק.

עם זאת לא התגברנו על הבעיה המעצבנת של תפיסת Exception.

נוכל לפתור בעיה זו בצורה אלגנטית ע"י הוספת פונקציה כזו:

1
2
3
4
private static object Clone(object source)
{
return null;
}

כעת נוכל לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
ICollection<object> result = newList<object>();
foreach (object current in collection)
{
dynamic dynamicCurrent = current;
object cloneResult = Clone(dynamicCurrent);
if (cloneResult != null)
{
result.Add(cloneResult);
}
}

מה קורה כאן? כאשר אנחנו קוראים לClone, בזמן ריצה נבחר הOverload המתאים ביותר.

אם אנחנו מממשים את הממשק ICloneable<T>, יבחר הoverload הראשון. אחרת יבחר הoverload השני.

לכן אם אנחנו לא מממשים את הממשק ICloneable<T>, נקבל פשוט null.

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

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

שתף