30. Advanced generic constraints

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

נניח שיש לנו ממשק של משהו שמזכיר מספר

1
2
3
4
public interface IAddable
{
IAddable Add(IAddable other);
}

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

איזה דברים יכולים לממש את זה?

הרבה דברים מתמטיים:

בעצם כל דבר שסגור לחיבור:

מחלקה של מספרים שלמים שתכתבו, מחלקה של מספרים ממשיים/רציונליים/מרוכבים שתממשו, מחלקה של מטריצות מסדר latex 5 \times 6 $ שתממשו, מחלקה של וקטורים ממימד 3 שתממשו, ועוד ועוד.

הכל נראה טוב ויפה, עד שנגיע לבעיה הבאה:

יש לנו שתי מחלקות

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Matrix4x4 : IAddable
{
public IAddable Add(IAddable other)
{
// ...
}
}
public class RationalNumber : IAddable
{
public IAddable Add(IAddable other)
{
// ...
}
}

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

1
2
3
4
RationalNumber rationalNumber;
Matrix4x4 matrix;
IAddable result = rationalNumber.Add(matrix); // Compiles!

אני אסביר את הבעיה:

באמת אפשר לחבר מטריצות (אחת עם השנייה), ואפשר גם לחבר מספרים רציונליים (אחד עם השני), אבל אי אפשר לחבר מספר רציונלי עם מטריצה בגודל $ 4 \times 4 $!

מאחר וכל המחלקות שלנו מממשות IAddable, אפשר לחבר אותן אחת עם השנייה.

בפועל לרוב נקבל שגיאה בזמן ריצה מאחר ובפונקציה Add המממושת בRationalNumber בוודאי תהיה הסבה של other (המשתנה שאנחנו מקבלים בפונקציה) לRationalNumber.

פתרון:

שלב 1: נהפוך את הממשק לגנרי:

1
2
3
4
public interface IAddable<T>
{
T Add(T other);
}

כעת המימושים יראו כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Matrix4x4 : IAddable<Matrix4x4>
{
public Matrix4x4 Add(Matrix4x4 other)
{
// ...
}
}
public class RationalNumber : IAddable<RationalNumber>
{
public RationalNumber Add(RationalNumber other)
{
// ...
}
}

והשורה השטנית

1
object result = rationalNumber.Add(matrix);

כבר לא מתקמפלת!

The best overloaded method match for ‘RationalNumber.Add(RationalNumber)’ has some invalid arguments
Argument ‘1’: cannot convert from ‘Matrix4x4’ to ‘RationalNumber’

הכל טוב ויפה, לא?

לא.

למה לא?

כי נוכל למשל לעשות שטויות מהסוג הזה:

1
2
3
4
5
6
7
public class DumbAddable : IAddable<int>
{
public int Add(int other)
{
// ...
}
}

מה הבעיה? היינו מצפים שDumbAddable יקבל בפונקציית הAdd שלו DumbAddable ויחזיר DumbAddable, כמו שהיה בכל הדוגמאות הקודמות. בעצם הפסדנו את היתרון של "הסגירות" שהיה לנו בIAddable הלא הגנרי.

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

זה לא נכון, משתי הסיבות הבאות:

  1. תשתית אמורה להגן כמה שניתן מהמתכנת מלעשות דברים לא נכונים.
  2. זה פוגע גם בתשתית. נניח שנכתוב פונקציה כזאת:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static IAddable<T> AddMany<T>(IAddable<T>[] parameters)
{
if (parameters.Length == 0)
{
throw new ArgumentException
("Excepted to recieve at least one parameter",
"parameters");
}
else
{
IAddable<T> currentSum = parameters[0];
for (int i = 1; i < parameters.Length; i++)
{
currentSum = currentSum.Add(parameters[i]);
}
return currentSum;
}
}

פונקציה לגיטימית שמחברת הרבה IAddable<T>.

זה לא יתקמפל לנו:

The best overloaded method match for ‘IAddable.Add(T)’ has some invalid arguments
Argument ‘1’: cannot convert from ‘IAddable’ to ‘T’

שלב ב’ של הפתרון:

נשנה את הממשק כך:

1
2
3
4
public interface IAddable<T> where T : IAddable<T>
{
T Add(T other);
}

עכשיו אתם אמורים לחשוב לעצמכם, רגע, זה לא רקורסיבי????

התשובה הפשוטה היא שזה לא.

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

כאשר מממשים רשימה כותבים בד"כ משהו כזה

1
2
3
4
5
struct listNode
{
int value;
listNode* next;
}

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

נחזור לשלב ב’ של הפתרון. כעת לא תתקמפל המחלקה DumbAddable:

The type ‘int’ cannot be used as type parameter ‘T’ in the generic type or method ‘IAddable’. There is no boxing conversion from ‘int’ to ‘IAddable’

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static T AddMany<T>(T[] parameters)
where T : IAddable<T>
{
if (parameters.Length == 0)
{
throw new ArgumentException
("Excepted to recieve at least one parameter",
"parameters");
}
else
{
T currentSum = parameters[0];
for (int i = 1; i < parameters.Length; i++)
{
currentSum = currentSum.Add(parameters[i]);
}
return currentSum;
}
}

שימו לב שעכשיו המתודה גם נראית הרבה יותר טוב, אין בה בשום מקום IAddable<T>, חוץ מבConstraint.

סופ"ש גנרי מצוין

שתף