ראינו פעם שעברה כיצד ניתן להזריק לType גנרי פרמטרים גנריים.
לפעמים אנחנו מעוניינים לעשות להפך – יש לנו Type גנרי שהוזרקו אליו הפרמטרים, ואנחנו מעוניינים לקבל את הטיפוס הגנרי (ללא הפרמטרים הגנריים המוזרקים), או לחלופין לקבל את הפרמטרים הגנריים המוזרקים.
כדי להשיג את הType הגנרי ללא הפרמטרים הגנריים המוזרקים, נוכל להשתמש בפונקציהGetGenericTypeDefinition:
1
2
Type closedDictionaryType = typeof (Dictionary<string,int>);
Type unboundDictionaryType = closedDictionaryType.GetGenericTypeDefinition(); // typeof(Dictionary<,>)
כדי לחלץ את הארגומנטים הגנריים נוכל להשתמש בפונקציה GetGenericArguments המחזירה לנו מערך של הארגומנטים הגנריים:
הכרנו קצת את Type וראינו איך אפשר ליצור instance חדש מType נתון.
נניח שיש לנו כמו אתמול Type שהשגנו אותו מאיזשהו מקום (למשל, מתוך קונפיגורציה).
איך נוכל ליצור List של טיפוסים כאלה?
מבחינת אינטואיטיבית היינו רוצים לעשות משהו כזה:
1
2
3
4
5
public IList CreateList(Type givenType)
{
List<givenType> list = new List<givenType>();
return list;
}
אלא שקוד זה לא יתקמפל:
The type or namespace name ‘givenType’ could not be found (are you missing a using directive or an assembly reference?)
הסיבה היא שטיפוסים גנריים מצפים לקבל בתור פרמטרים גנריים טיפוסים שידועים בזמן קימפול, ולא Type, שזהו לא טיפוס ידוע בזמן קימפול, אלא instance של מחלקת Type.
כדי לפתור בעיה זו, נוכל להשתמש בפונקציה MakeGenericType של Type, המקבלת Type של טיפוס גנרי Unbound, ומזריקה אליו את הפרמטרים הגנריים שלו.
מבצעת בסופו של דבר קריאה לConstructor כמו השורה הזאת:
1
Person meir = new Person("Meir", "Ariel");
הדבר ממש מגניב, אבל לכל דבר יש מחיר.
המחיר כאן הוא בביצועים – יצירת אובייקט בצורה דינמית באמצעות הפונקציהActivator.CreateInstance מוסיף מספר מילישניות לזמן שלוקח ליצור את האובייקט. זה די הרבה באופן יחסי, מאחר והCLR מאוד מהיר ביצירת אובייקטים (קריאה רגילה לnew לוקחת בערך 10 נאנו שניות).
כך שאם האפליקציה שלכם צריכה להתמודד עם קצבים מהירים, השימוש הנ"ל יכול להזיק לה.
אתמול הכרנו את הפונקציה GetType המאפשרת לנו להשיג את הType האמיתי של instance של אובייקט שיש לנו.
לעתים נרצה (מסיבות כאלה ואחרות) דווקא את הטיפוס של המשתנה בו מוחזק הInstance שלנו.
לדוגמה:
כתבנו פונקציה כזו:
1
2
3
4
publicstatic Type GetVariableType(objectvalue)
{
returnvalue.GetType();
}
עכשיו אנחנו קוראים לה ככה:
1
2
3
IList myList = new List<string>();
Type myListType = GetVariableType(myList);
// typeof(List<string>)
אלא שהעברנו לפונקציה משתנה מסוג IList, והיינו רוצים לדעת מה הסוג של המשתנה שהועבר לפונקציה… (כלומר מאיזה טיפוס המשתמש בפונקציה שלנו ראה אותו)
נוכל לפתור זאת בצורה הבאה:
נהפוך את המתודה לגנרית:
1
2
3
4
publicstatic Type GetVariableType<T>(T value)
{
returnvalue.GetType();
}
ובמקום להחזיר את GetType של הvalue, נחזיר את הסוג של T:
1
2
3
4
publicstatic Type GetVariableType<T>(T value)
{
returntypeof (T);
}
אם עכשיו נבצע את השורות שראינו מעלה נקבל באמת הטיפוס שציפינו לקבל:
1
2
IList myList = new List<string>();
Type myListType = GetVariableType(myList); // typeof(IList)
מה סוד הקסם?
אחד הדברים האהובים עלי במתודות גנריות, בניגוד למחלקות גנריות, הוא שהקומפיילר החכם יכול לגלות לבד לפי הטיפוס של המשתנה בקריאה, איזה טיפוס גנרי להעביר לפונקציה.
(טיפ מספר 28, וטיפ מספר 69)
מה שקורה כאן, הוא בגלל שהמשתנה שלנו מסוג IList, אז הטיפוס הגנרי שהקומפיילר מעביר לפונקציה.
ראינו ביום ראשון, שאם מעבירים struct למתודה שמקבלת struct, מועבר העתק שלו.
לכן, אם נרצה להעביר struct למתודה, כך שהמתודה תשנה אותו, לא נוכל לעשות זאת.
בעיה זו מוכרת עוד מימי C/C++ העליזים. שם פתרנו את הבעיה ע”י העברת פוינטר לstruct, או לחלופין השתמשנו בSyntax של & שהעביר את האובייקט כReference.
בC# יש שני keywords המאפשרים לנו להתמודד עם בעיה כזו. שמות הKeywords הם out וref והם מאפשרים לנו להעביר אובייקט לפונקציה “ולשנות אותו”.
יש שתי בעיות שהKeywords האלה פותרים:
הבעיה הראשונה: שינוי reference של אובייקט מסוג reference type בקריאה למתודה:
1
2
3
4
publicstaticvoidReverseString(stringvalue)
{
value = newstring(value.Reverse().ToArray());
}
פונקציה זו מקבלת מחרוזת והופכת אותה. נראה טוב, לא?
אלא שאם נריץ את הקוד הבא נקבל להפתעתנו
1
2
3
string myString = "This ain't a palindrome";
ReverseString(myString);
Console.WriteLine(myString); // This ain't a palindrome
הבעיה היא שאנחנו מקבלים בvalue רק reference שמצביע לmyString.
בפונקציה כשאנחנו משנים את value, אנחנו משנים את המקום שאליו הוא מצביע (כי אנחנו יוצרים שם אובייקט חדש). עם זאת, לא שינינו את ההצבעה של האובייקט myString, ולכן אין סיבה שהוא יצביע לאותו מקום שמצביע עכשיו value.
הבעיה השנייה היא מה שהזכרתי בתחילת הטיפ:
אם נעשה משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassPerson
{
publicint Age
{
get;
set;
}
// ...
}
publicstaticvoidChangePerson(Person person)
{
person.Age = 26;
}
אז כשנקרא לפונקציה עם Person כלשהו הגיל שלו ישתנה במידה והוא reference type:
1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(myPerson);
Console.WriteLine(myPerson.Age); // 26
למה? כי זה reference type, הפעם לא שינינו בפונקציה ChangePerson את הreference של person ולכן כאשר שינינו את person בפונקציה, הוא שינה גם את myPerson.
אבל אם נשנה את Person להיות value type:
1
2
3
4
5
6
7
8
9
publicstruct Person
{
publicint Age
{
get;
set;
}
// ...
}
אז בקריאה זו:
1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(myPerson);
Console.WriteLine(myPerson.Age); // 3
למה? כפי שראינו ביום ראשון, כשאנחנו מעבירים לפונקציה value type (כלומר struct), אנחנו מעבירים לה בעצם עותק שלו. לכן אנחנו משנים כאן רק את העותק person, ולא את המקור myPerson.
אז איך פותרים? יש שני keywords שמאפשרים לנו להתמודד עם בעיות כאלה: ref וout.
אם נסמן במתודה שפרמטר מסוים הוא ref, במידה והוא Reference type, יועבר לנו מצביע לReference שלו (כלומר מצביע למצביע), ואם נשנה אותו, ישתנה גם האובייקט המקורי:
1
2
3
4
publicstaticvoidReverseString(refstringvalue)
{
value = newstring(value.Reverse().ToArray());
}
אכן:
1
2
3
string myString = "This ain't a palindrome";
ReverseString(ref myString);
Console.WriteLine(myString); // emordnilap a t'nia sihT
במידה והוא Value type נקבל אותו By reference (כלומר, נקבל מצביע למקום בו הוא יושב בזכרון) וכך נוכל לשנות אותו:
1
2
3
4
publicstaticvoidChangePerson(ref Person person)
{
person.Age = 26;
}
ואכן:
1
2
3
Person myPerson = new Person(){Age = 3};
ChangePerson(ref myPerson);
Console.WriteLine(myPerson.Age); // 26
יש לנו עוד Keyword ושמו out. הוא מאוד דומה לref, אבל נועד למטרה אחרת: נניח שאנחנו רוצים לאתחל פרמטר בפונקציה, אזי נסמן אותו בout:
1
2
3
4
5
publicstaticintDivide(int a, int b, outint remainder)
{
remainder = a%b;
return a/b;
}
הפונקציה הזאת מחזירה לנו את השארית ואת החלוקה של מספר מסוים במספר אחר.
תכלס, יכולנו לעשות את זה גם עם ref, לא?
ובכן נראה ששני הKeywords האלה קיימים מטעמי סמנטיקה, כדי שנוכל להגיד אם אנחנו רוצים לאתחל פרמטר בתוך הפונקציה, או לשנות אותו.
זה לא רחוק מהאמת, אבל קיימים מספר הבדלים בין הKeywordים:
אם למשל נוריד שורה מהמתודה האחרונה:
1
2
3
4
publicstaticintDivide(int a, int b, outint remainder)
{
return a/b;
}
הקוד שלנו לא יתקמפל, מאחר ולא אתחלנו משתנה שמסומן בout.
The out parameter ‘remainder’ must be assigned to before control leaves the current method
לעומת זאת, אם נחליף את הout בref, הקוד דווקא כן יתקמפל.
זאת משום שאנחנו מכריזים בקבלת משתנה עם out שאנחנו אחראים לאתחל אותו עד סוף הפונקציה.
בכיוון ההפוך: אם נשאר עם החתימה הזאת:
1
2
3
4
5
publicstaticintDivide(int a, int b, outint remainder)
{
remainder = a%b;
return a/b;
}
ונכתוב קוד כזה:
1
2
3
int remainder;
Divide(100, 9, out remainder);
Console.WriteLine(remainder); // 1
הקוד יתקמפל ויעבוד. מה שמיוחד כאן הוא שהצהרנו על remainder, אבל לא אתחלנו אותו. עם זאת, זה מתקמפל כי out מוודא שאנחנו מאתחלים את remainder בפונקציה.
אם, לעומת זאת, נחליף את הפונקציה לפונקציה עם החתימה הבאה:
1
2
3
4
5
publicstaticintDivide(int a, int b, refint remainder)
{
remainder = a % b;
return a/b;
}
הקוד הבא כבר לא יתקמפל:
1
2
3
int remainder;
Divide(100, 9, ref remainder); // Compile error
Console.WriteLine(remainder);
נקבל את שגיאת הקימפול:
Use of unassigned local variable ‘remainder’
מה קורה כאן? כמו שout דואג לוודא שאנחנו מאתחלים את הפרמטר לפני שאנחנו יוצאים מהפונקציה, ref דואג שאנחנו נאתחל את המשתנה לפני שנכנס לפונקציה.
זאת מאחר והפונקציה עשויה להשתמש בremainder, ואם היינו רוצים להגיד שהיא לא מסתמכת עליו, היינו צריכים לסמן אותו בout.
למי שלמד על המושגים in parameter, out parameter וin/out parameter , הייתי מציע לעשות את האנלוגיה הבאה:
בלי לסמן כלום על פרמטר – זה אנלוגי להגיד שהוא in parameter
סימון של out – זה אנלוגי להגיד שהוא out parameter
סימון של ref – זה אנלוגי להגיד שהוא in/out parameter
שימו לב שיש פה אנלוגיה, אבל זה לא הכי מדויק, כיוון שכפי שראינו למעלה, גם reference types שאנחנו לא מסמנים באף אחד מהkeywords האלה אפשר לשנות, אבל האנלוגיה מובנת.
כפי שרואים נוספה פקודה של constrained לפני הפקודה callvirt, הסיבה היא שהמהדר לא יודע האם TPrintable הוא value type או reference type אבל הוא כן יודע שצריך להעביר כתובת כלשהי למתודה בתור ה-this שלה מכיוון שמדובר במתודה של ממשק אשר לא יכול להיות סטטית (כלומר חייב להיות לה this).
לאחר שקיבלנו את הכתובת של הפרמטר ודחפנו אותו למחסנית (IL_0000), כאשר אנו קוראים ל-callvirt שלפניו יש constrained אז במידה ו-TPrintable הוא reference type אז על המחסנית בעצם נמצא pointer שמצביע ל-pointer שכן הפרמטר מכיל כתובת ולכן מתבצע dereference כדי שה-this יצביע לאן שצריך וניתן לקרוא למתודה בעזרת Callvirt.
במידה ו-TPrintable הוא value type אז הכתובת של הפרמטר בעצם מכילה את הכתובת של ה-value type וניתן להעביר אותה כמו שהיא למתודה בעזרת Call.
ראינו בעבר (טיפ מספר 89) כי בC# 3.0 נוספה אפשרות להגדיר Properties בצורה יותר נוחה.
הדבר עובד טוב למחלקות, אך האם הוא עובד גם לstructים?
במבט ראשון, נראה שזה עובד:
1
2
3
4
5
6
publicstruct Point3D
{
publicint X { get; set; }
publicint Y { get; set; }
publicint Z { get; set; }
}
הקוד מתקמפל ועובד.
כעת נקשה על העניין, נהפוך את הsetterים להיות private:
1
2
3
4
5
6
publicstruct Point3D
{
publicint X { get; privateset; }
publicint Y { get; privateset; }
publicint Z { get; privateset; }
}
עדיין מתקמפל, אבל עכשיו כבר אי אפשר לאתחל את הProperties מבחוץ.
אז נוסיף Constructor שיאתחל לנו אותם:
1
2
3
4
5
6
7
8
9
10
11
12
13
publicstruct Point3D
{
publicPoint3D(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
publicint X { get; privateset; }
publicint Y { get; privateset; }
publicint Z { get; privateset; }
}
אבל עכשיו נראה שהקוד לא מתקמפל יותר:
Backing field for automatically implemented property ‘Point3D.Z’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer. Backing field for automatically implemented property ‘Point3D.Y’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer. Backing field for automatically implemented property ‘Point3D.X’ must be fully assigned before control is returned to the caller. Consider calling the default constructor from a constructor initializer. The ‘this’ object cannot be used before all of its fields are assigned to
מה קורה פה?
זוכרים שאתמול הזכרנו שבConstructor שאינו דיפולטי של struct אנחנו מחויבים לאתחל את כל השדות?
ובכן, הAuto-Properties בעצם יוצרים לנו שדות מאחורי הקלעים, שאותם לא איתחלנו בConstructor. לכן הקוד אינו מתקמפל.
איך נוכל לפתור את הבעיה?
קיימות מספר אופציות:
אופציה ראשונה הוא מה שמציע לנו הקומפיילר: נקרא לConstructor הדיפולטי:
1
2
3
4
5
6
7
publicPoint3D(int x, int y, int z) :
this()
{
this.X = x;
this.Y = y;
this.Z = z;
}
זה יעבוד, אבל יש פה התנהגות מוזרה: אנחנו מאתחלים תחילה את כל השדות בערכים דיפולטים, ואחר כך בערכים האמיתיים. אולי ככה התרגלנו לעשות במחלקות, אבל משהו לא טוב כאן. במיוחד עבור מי שרוצה להשתמש בstruct דווקא משיקולי ביצועים, שהרי ככה הערכים מאותחלים פעמיים.
אופציה שנייה היא להסיר את הConstructor הזה. ככה אי-אפשר לאתחל את הProperties האלה
אופציה שלישית היא לעשות מימוש שהוא לא Auto-Property ואז לאתחל ישירות את השדות בערכים האמיתיים שלהם, אבל רק אתחול אחד
הייתי הולך על הפתרון השלישי, אבל כל אחד ומה שנראה לו.