כפי שציינתי, Property מתקמפל מאחורי הקלעים למתודה עבור כל Accessor. (כלומר מתודה לGetter ומתודה לSetter)
גם Eventים וIndexerים מתקמפלים למשהו כזה מאחורי הקלעים. איפה הבעיה? נחזור לטיפ 147, שם ראינו שאנחנו יכולים לקבל את כל המתודות הPublicיות של טיפוס.
אם נריץ אותו על הטיפוס הבא
1
2
3
4
5
6
7
8
9
10
11
publicclassPerson
{
publicint Age { get; set; }
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicvoidTalk()
{
Console.WriteLine("Hello");
}
}
נקבל את הפלט המפתיע הבא:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
IEnumerable<MethodInfo> publicMethods =
typeof (Person).GetMethods();
foreach (MethodInfo methodInfo in publicMethods)
{
Console.WriteLine(methodInfo);
}
// Int32 get_Age()
// Void set_Age(Int32)
// System.String get_FirstName()
// Void set_FirstName(System.String)
// System.String get_LastName()
// Void set_LastName(System.String)
// Void Talk()
// System.String ToString()
// Boolean Equals(System.Object)
// Int32 GetHashCode()
// System.Type GetType()
בואו ננתח מה מפתיע כאן: נתחיל מהסוף – 4 המתודות האחרונות הן פשוט מתודות publicיות שקיבלנו מירושה מobject. המתודה החמישית מהסוף היא המתודה שמופיעה במחלקה שלנו. שאר מהמתודות הן המתודות שדיברנו עליהן אתמול שהProperties שלנו מתקמפלים אליהם.
אלא, שמבחינתנו לפעמים נרצה להבדיל בין איך נראה הקוד ואיך הוא מיוצג מאחורי הקלעים. כלומר, נרצה להתעלם לפעמים מהמתודות שProperty מתקמפל אליהן ולהתייחס רק למתודות שתוכנתו כמתודות.
למרבה המזל, נוכל לעשות זאת באמצעות הProperty של MethodInfo ששמו IsSpecialName. Property זה מציין לנו האם למתודה שם מיוחד, כלומר שם שנוצר כתוצאה מקימפול "קוד מטיפוס אחר" (כמו שהוסבר בהתחלה, Properties, Events וכו’)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerable<MethodInfo> publicMethods =
typeof (Person).GetMethods();
foreach (MethodInfo methodInfo in publicMethods)
{
if (!methodInfo.IsSpecialName)
{
Console.WriteLine(methodInfo);
}
}
// Void Talk()
// System.String ToString()
// Boolean Equals(System.Object)
// Int32 GetHashCode()
// System.Type GetType()
כך נמענו מהפונקציות של הProperties שלא כל כך רצינו.
הכרנו קצת את PropertyInfo וראינו איך לעשות איתו דברים דומים לדברים שעשינו עם MethodInfo.
כידוע, Property הוא בסה”כ Syntactic Sugar למתודה שמחזירה ערך ולא מקבלת פרמטרים (מתודת Get) ומתודה שמקבלת ערך ולא מחזירה ערך (מתודת Set)
למשל, הProperty הזה
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicclassPerson
{
privateint m_Age;
publicint Age
{
get
{
return m_Age;
}
set
{
m_Age = value;
}
}
}
מתקמפל מאחורי הקלעים למשהו כזה:
1
2
3
4
5
6
7
8
9
publicvoidset_Age(intvalue)
{
m_Age = value;
}
publicintget_Age()
{
return m_Age;
}
ייתכן ונרצה להשתמש בMethodInfo של המתודות האלה, למשל אם כתבנו פונקציה שיודעת לטפל בMethodInfo ייתכן ונרצה לשלוח אליה את הGetter/Setter של הProperty. במקרה כזה יחסך לנו שכפול קוד, במקום שנצטרך לכתוב מתודה נוספת שמשתמשת בGetValue במקום בInvoke שלMethodInfo.
כדי לעשות זאת, מסתבר שיש מתודות בשם GetGetMethod וGetSetMethod המאפשרות לנו להשיג את המתודות הנ"ל:
לפני Framework 2.0, היה ממשק בשם IEnumerable. אז לא היו טיפוסים גנריים, ולכן לא ניתן לדעת מהו הטיפוס הספציפי של האובייקטים באוסף זה.
בFramework 2.0 נוספו טיפוסים גנריים, ונוסף גם הממשק IEnumerable. (ראו גם טיפים מספר 51-55)
לצרכי תאימות לאחור ושיקולים נוספים (טיפ מספר 150 למשל), רוב הטיפוסים בFramework שממשים IEnumerable מממשים גם IEnumerable.
אלא, שלמרבה הצער, לא ניתן להשתמש ברוב הפונקציות של LINQ (כולל הQuery syntax) על הטיפוס IEnumerable, אלא רק על הגרסה הגנרית שלו.
דבר זה כואב מאוד למי שיוצא לו לעבוד עם חלקים מהFramework שהגיחו בFramework 1.0 ולא שודרגו מאז, לפחות מבחינת הAPI, ביניהם ADO.net (עבודה עםDataSet/DataTableים) וWindows Forms.
קיימים הרבה מאוד טיפוסים מתקופה זו שמממשים IEnumerable ולא IEnumerable. דוגמאות בולטות לכך הן DataRowCollection וControlCollection.
עם זאת, היוצרים של LINQ היו מספיק נחמדים בשביל לחשוב על פתרון גם למי שנאלץ להשתמש בטיפוסים אלה:
קיימות שתי Extension Methods של IEnumerable המסייעות לנו בעניינים אלה:
הראשונה היא Cast המאפשרת לנו להמיר את הIEnumerable שלנו לIEnumerable עבור T שנציין:
1
2
3
4
5
6
7
IEnumerable theNumbersAreEvil =
newobject[] {4, 8, 15, 16, 23, 42};
IEnumerable<int> theNumbers =
theNumbersAreEvil.Cast<int>();
Console.WriteLine(theNumbers.Sum()); // 108
מה שהפונקציה הזו עושה היא רצה על כל האיברים של האוסף המקורי, ויוצרת אוסף חדש ע"י הסבה לטיפוס שציינו.
כמובן, כפי שבוודאי יכולתם לנחש, פונקציה זו היא Lazy (כמו כל דבר בLINQ), ולכן גם אם האוסף המקורי שלנו משתנה, האוסף שנוצר ממנו יושפע מכך. (ההסבות תתבצענה רק כאשר נקרא לפונקציה MoveNext של הEnumerator, או לחלופין כשנרוץ על האוסף)
נוכל להשתמש בפונקציה זו גם לצרכים אחרים. נניח שאנחנו עובדים בFramework 3.5, וקיימת פונקציה שמצפה לקבל IEnumerable<Shape>, אבל יש לנו IEnumerable<Circle>. לא נוכל להעביר אוסף זה ישירות (ראו טיפים 36-40), אבל נוכל להשתמש בCast כדי לבצע את המשימה:
בנוסף לפונקציה Cast, קיימת גם הפונקציה OfType. פונקציה זו דומה לCast, אלא שהיא מסננת עבורנו רק את האיברים שהם מהטיפוס הגנרי שנעביר לה (בניגוד לCast שזורקת Exception ברגע שהיא לא מצליחה להסב לטיפוס הגנרי):
1
2
3
4
5
IEnumerable homogenousEnumerable =
newobject[] {"This is a string", 42, DateTime.Now, "This is another string", 108};
היא שימושית גם עבור סינונים כללים, למשל הנה דוגמה הפוכה לדוגמה שרשמתי למעלה: נניח שיש לנו אוסף של צורות, ואנחנו מעוניינים לסנן משם את כל המעגלים. נוכל לעשות זאת כך:
לפעמים יש לנו איזשהו IEnumerable ואנחנו מעוניינים לגשת לאיבר הראשון שלו.
לפני C# 3.0, הדרך לעשות זאת הייתה די זוועתית. אני זוכר שכתבתי שורות כאלה פעם:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IEnumerable<string> strings =
newstring[]
{
"A new hope",
"The empire strikes back",
"The return of the Jedi"
};
IEnumerator<string> stringsEnumerator =
strings.GetEnumerator();
string first = null;
if (stringsEnumerator.MoveNext())
{
first = stringsEnumerator.Current; // A new hope
}
יש פה די הרבה קוד בשביל בסה"כ לגשת לאיבר הראשון. יש פה גם כמה בעיות. מה אם האוסף ריק?
בLINQ הוסיפו לנו ארבע Extension Methods שעושות לנו את החיים טיפה יותר קלים.
המתודה הראשונה היא First:
1
string first = strings.First();
היא מחזירה לנו את האיבר הראשון באוסף. מה אם אין כזה? היא תזרוק לנו Exception. איך נוכל למנוע אותו?
באמצעות המתודה Any:
1
2
3
4
5
6
string first = null; // Put your default here :)
if (strings.Any())
{
first = strings.First();
}
(ראו גם טיפ מספר 15)
או באמצעות אחותה המתודה ששמה FirstOrDefault המחזירה לנו את האיבר הראשון באוסף או את default(T), במידה והאוסף ריק (כאשר T הטיפוס של אוסף), ראו גם טיפ מספר 32.
1
string first = strings.FirstOrDefault();
יש לזה שני חסרונות על השיטה הקודמת: הראשונה היא שאנחנו לא יכולים לקבוע מה יהיה הערך הדיפולטי (במידה ולא היו איברים ברשימה). החסרון השני הוא שאנחנו לא נדע אם נבחר האיבר הראשון או האיבר הדיפולטי (אם למשל נעבוד עם טיפוסים שהם Value types זה יכול לשנות לנו, כי אם האוסף יהיה למשל מסוג int אנחנו יכולים לקבל 0, ויכול להיות שזה הראשון, ולאו בהכרח הדיפולטי)
בנוסף, קיימות שתי פונקציות בעלות שמות דומים והם Single וSingleOrDefault. מה ההבדל ביניהן לשתי הפונקציות שכבר ראינו?
פונקציות אלה מצפות לאיבר יחיד, לכן אם יש יותר מאיבר אחד באוסף, יזרק Exception. זה יכול להיות שימושי כשאנחנו מצפים לקבל, למשל, תוצאה אחת בדיוק משאילתת LINQ. למשל, חיפוש אדם עפ"י מספר תעודת הזהות שלו.
הפונקציה Single תזרוק לנו Exception אם אין לנו איבר יחיד באוסף (כלומר יש לנו 0 איברים או יותר מ1)
הפונקציה SingleOrDefault תזרוק לנו Exception אם יש לנו יותר מאיבר אחד ברשימה, אבל אם יש לנו 0, היא תחזיר לנו את הדיפולט.