160. IsSpecialName

בהמשך לטיפ של אתמול,

כפי שציינתי, Property מתקמפל מאחורי הקלעים למתודה עבור כל Accessor. (כלומר מתודה לGetter ומתודה לSetter)

גם Eventים וIndexerים מתקמפלים למשהו כזה מאחורי הקלעים. איפה הבעיה? נחזור לטיפ 147, שם ראינו שאנחנו יכולים לקבל את כל המתודות הPublicיות של טיפוס.

אם נריץ אותו על הטיפוס הבא

1
2
3
4
5
6
7
8
9
10
11
public class Person
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public void Talk()
{
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 שלא כל כך רצינו.

שיהיה המשך יום מיוחד

שתף

159. GetGetMethod/GetSetMethod Methods

בהמשך לטיפ היומי של אתמול,

הכרנו קצת את PropertyInfo וראינו איך לעשות איתו דברים דומים לדברים שעשינו עם MethodInfo.

כידוע, Property הוא בסה”כ Syntactic Sugar למתודה שמחזירה ערך ולא מקבלת פרמטרים (מתודת Get) ומתודה שמקבלת ערך ולא מחזירה ערך (מתודת Set)

למשל, הProperty הזה

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person
{
private int m_Age;
public int Age
{
get
{
return m_Age;
}
set
{
m_Age = value;
}
}
}

מתקמפל מאחורי הקלעים למשהו כזה:

1
2
3
4
5
6
7
8
9
public void set_Age(int value)
{
m_Age = value;
}
public int get_Age()
{
return m_Age;
}

ייתכן ונרצה להשתמש בMethodInfo של המתודות האלה, למשל אם כתבנו פונקציה שיודעת לטפל בMethodInfo ייתכן ונרצה לשלוח אליה את הGetter/Setter של הProperty. במקרה כזה יחסך לנו שכפול קוד, במקום שנצטרך לכתוב מתודה נוספת שמשתמשת בGetValue במקום בInvoke שלMethodInfo.

כדי לעשות זאת, מסתבר שיש מתודות בשם GetGetMethod וGetSetMethod המאפשרות לנו להשיג את המתודות הנ"ל:

1
2
3
4
5
6
7
8
9
10
11
12
PropertyInfo ageInfo =
typeof (Person).GetProperty("Age");
MethodInfo getAge =
ageInfo.GetGetMethod();
Console.WriteLine(getAge.Name); // get_Age
MethodInfo setAge =
ageInfo.GetSetMethod();
Console.WriteLine(setAge.Name); // set_Age

לזכור ולא לשכוח

שתף

158. PropertyInfo

בדומה לMethodInfo המייצג Metadata של מתודה, קיים גם הטיפוס PropertyInfoהמייצג Metadata של Property.

השימוש בו הוא די פשוט:

1
2
PropertyInfo lengthInfo =
typeof (string).GetProperty("Length");

כעת נוכל לברר ככל העולה על רצוננו אודות Property זה:

(באופן דומה מאוד לMethodInfo, ראו גם טיפ מספר 146)

1
2
3
4
5
6
7
Console.WriteLine(lengthInfo.Name); // Length
Type returnType = lengthInfo.PropertyType; // typeof(int)
bool hasGet = lengthInfo.CanRead; // true
bool hasSet = lengthInfo.CanWrite; // false

באופן דומה לMethodInfo, נוכל גם להפעיל Property כזה בReflection על Instance של טיפוס שהProperty שייך לו, לדוגמה:

1
2
3
4
5
6
7
PropertyInfo lengthInfo =
typeof(string).GetProperty("Length");
object myString =
"It was the best of times, it was the worst of times... ";
int length = (int)lengthInfo.GetValue(myString, null); // 55

וגם Set:

1
2
3
4
5
6
7
8
9
PropertyInfo ageInfo =
typeof(Person).GetProperty("Age");
object myPerson =
new Person("Jarvis","Lorry");
ageInfo.SetValue(myPerson, 351, null);
Console.WriteLine(((Person)myPerson).Age); // 351

שיהיה שבוע משתקף לטובה

שתף

157. DefaultIfEmpty extension method

לפעמים יש לנו אוסף, ואנחנו מעוניינים שתמיד יהיה בו איזשהו איבר.

למשל, ייתכן שאנחנו רוצים לגשת האיבר הראשון של האוסף, ולכן חשוב לנו שיהיה לפחות איבר אחד באוסף.

מסתבר שיש Extension Method בשם DefaultIfEmpty שמחזיר לנו את האוסף במידה והוא לא ריק, ואחרת את הDefault:

1
2
3
4
IEnumerable<int> enumerable = new[] {4, 8, 15, 16, 23, 42};
IEnumerable<int> otherEnumerable =
enumerable.DefaultIfEmpty(); // otherEnumerable == enumerable

במידה והאוסף ריק:

1
2
3
4
5
6
IEnumerable<int> enumerable = new int[] {};
IEnumerable<int> otherEnumerable =
enumerable.DefaultIfEmpty();
Console.WriteLine(otherEnumerable.First()); // 0

קיים גם Overload שמאפשר לנו להגדיר את אותו הDefault (ולאו דווקא להשתמש בdefault(T)):

1
2
3
4
5
6
IEnumerable<int> enumerable = new int[] {};
IEnumerable<int> otherEnumerable =
enumerable.DefaultIfEmpty(5);
Console.WriteLine(otherEnumerable.First()); // 5

שימו לב שאפשר להשתמש בזה כדי לקבל Default אחר במקום להשתמש בפונקציות ששמן מסתיים בOrDefault (ראו גם טיפ מספר 154):

1
2
3
IEnumerable<int> enumerable = new int[] {};
int value = enumerable.DefaultIfEmpty(5).First(); // 5

לעומת:

1
int value = enumerable.FirstOrDefault(); // 0

סוף שבוע ראשון (אבל לא דיפולטי) אחרי פסח טוב

שתף

156.5. Determinating whether an enumerable has a given size

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

תזכורת: ראינו ששימוש בExtension Method ששמו Count הוא לא יעיל, מאחר ובמקרה הגרוע, זה עובר על כל האיברים באוסף כדי לספור כמה איברים יש בו.

כעת אפשר לשאול כיצד ניתן לדעת האם יש באוסף לפחות שני איברים, או לכל היותר 10 איברים.

כמו בטיפ מספר 15, הדרך הנאיבית:

1
2
3
4
5
6
7
8
9
if (enumerable.Count() >= 2)
{
// ...
}
if (enumerable.Count() <= 10)
{
// ...
}

לא יעילה.

איך נוכל לעשות זאת בדרך אחרת?

נוכל להשתמש בAny . אמנם היא רק מחזירה האם אוסף מכיל איברים, אך נוכל להשתמש בה בצורות הבאות:

אופציה מספר 1:

פשוט נפלטר על איברים שהאינדקס שלהם גדול מהמספר הנתון, ועל זה נריץ Any: (ראו גם טיפ מספר 93)

1
2
3
4
5
6
7
8
9
if (enumerable.Where((element, index) => (index >= 1)).Any())
{
// ...
}
if (!enumerable.Where((element, index) => (index >= 10)).Any())
{
// ...
}

הסבר: אנחנו פשוט בודקים האם יש (או אין) איברים עם אינדקס גדול מספיק.

אופציה מספר 2:

לעשות בערך אותו הדבר, אבל בצורה יותר קריאה: פשוט נדלג על האיברים הראשונים ונראה האם מתקבלות תוצאות. זאת באמצעות הExtension Method ששמו Skip:

1
2
3
4
5
6
7
8
9
if (enumerable.Skip(1).Any()) // Count() >= 2
{
// ...
}
if (!enumerable.Skip(10).Any()) // Count() <= 10
{
// ...
}

אפשר כמובן לכתוב Extension Methods נחמדים שיהפכו את הסינטקס לעוד יותר קריא.

המשך יום בגודל מוערך טוב

שתף

156. ElementAt Extension Method

לעתים אנחנו מעוניינים להשיג איבר שהוא לא בהכרח ראשון מתוך אוסף.

למרבה המזל, יש Extension Method בLINQ שמסוגל לעשות זאת:

1
2
3
4
5
6
IEnumerable<int> theNumbers =
new[] { 4, 8, 15, 16, 23, 42 };
Console.WriteLine(theNumbers.ElementAt(0)); // 4
Console.WriteLine(theNumbers.ElementAt(3)); // 16
Console.WriteLine(theNumbers.ElementAt(6)); // ArgumentOutOfRangeException

המימוש שלו יודע להתחשב באם מדובר בIList ובמידה וכן, משתמש בIndexer שלו:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, intindex)
{
IList<TSource> list = source as IList<TSource>;
if (list != null)
{
return list[index];
}
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
// Iterate index times.
}
}

אם אנחנו רוצים להמנע מException כאשר אנחנו חורגים מגבולות האוסף, נוכל להשתמש במקום בExtension Method ששמו ElementAtOrDefault, למשל:

1
2
Console.WriteLine(theNumbers.ElementAtOrDefault(3)); // 16
Console.WriteLine(theNumbers.ElementAtOrDefault(6)); // 0

המזכיר את הExtension Method ששמו FirstOrDefault (ראו גם טיפ מספר 154 שנשלח ביום חמישי האחרון)

המשך יום במקום

שתף

155. OfType and Cast

לפני 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 =
new object[] {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 כדי לבצע את המשימה:

1
2
IEnumerable<Circle> circles;
IEnumerable<Shape> shapes = circles.Cast<Shape>();

בנוסף לפונקציה Cast, קיימת גם הפונקציה OfType. פונקציה זו דומה לCast, אלא שהיא מסננת עבורנו רק את האיברים שהם מהטיפוס הגנרי שנעביר לה (בניגוד לCast שזורקת Exception ברגע שהיא לא מצליחה להסב לטיפוס הגנרי):

1
2
3
4
5
IEnumerable homogenousEnumerable =
new object[] {"This is a string", 42, DateTime.Now, "This is another string", 108};
IEnumerable<string> theStrings = homogenousEnumerable.OfType<string>();
// "This is a string", "This is another string".

היא שימושית גם עבור סינונים כללים, למשל הנה דוגמה הפוכה לדוגמה שרשמתי למעלה: נניח שיש לנו אוסף של צורות, ואנחנו מעוניינים לסנן משם את כל המעגלים. נוכל לעשות זאת כך:

1
2
IEnumerable<Shape> shapes;
IEnumerable<Circle> circles = shapes.OfType<Circle>();

חג פסח ב’ שמח!

שתף

154. The Difference between First and Single

לפעמים יש לנו איזשהו IEnumerable ואנחנו מעוניינים לגשת לאיבר הראשון שלו.

לפני C# 3.0, הדרך לעשות זאת הייתה די זוועתית. אני זוכר שכתבתי שורות כאלה פעם:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IEnumerable<string> strings =
new string[]
{
"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, היא תחזיר לנו את הדיפולט.

סופ"ש נעים

שתף

153. Enumerable Range

לפעמים אנחנו מעוניינים לקבל תחום מסוים של מספרים כאוסף ולעשות עליו כל מיני פעולות.

הדרך הקלאסית לעשות זאת היא כך:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<int> Range(int from, int to)
{
List<int> numbers = new List<int>();
for (int i = from; i <= to; i++)
{
numbers.Add(i);
}
return numbers;
}

גם לפונקציה זו יש מקבילה LINQית בשם Enumerable.Range. אלא שהיא מקבלת איבר ראשון ומספר איברים:

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

1
2
3
4
public static IEnumerable<int> Range(int from, int to)
{
return Enumerable.Range(from, to - from + 1);
}

לדוגמה, נניח ואנחנו מעוניינים לאתחל מערך ב108 מספרים הראשונים אחרי 1000, נוכל לעשות זאת כך:

1
2
int[] aLotOfNumbers =
Enumerable.Range(1000, 108).ToArray();

זה יוצר לנו מערך בגודל 108 עם כל המספרים הטבעיים בין 1000 ל1107 (כולל הקצוות)

שיהיה המשך יום עם טווח קצר

שתף

152. Enumerable Repeat

לפעמים אנחנו מעוניינים ליצור אוסף מסוים של איברים שמורכב בעצם משכפול של איבר מסוים.

הדרך הסטנדרטית לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<T> Repeat<T>(T item, int times)
{
List<T> repeated = new List<T>();
for (int i = 0; i < times; i++)
{
repeated.Add(item);
}
return repeated;
}

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

בLINQ הוסיפו לנו מתודה שעושה משהו כזה, וכמו כל דבר בLINQ, גם היא Lazy:

1
2
3
4
5
6
7
public static IEnumerable<T> Repeat<T>(T item, int times)
{
for (int i = 0; i < times; i++)
{
yield return item;
}
}

למשל, נניח ואנחנו רוצים ליצור מערך בגודל 1024 שכל האיברים בו מאותחלים במספר 1 (כי במערך של מספרים, דיפולטית כל האיברים מאותחלים להיות 0)

נוכל לעשות זאת כך:

1
2
int[] aLotOfOnes =
Enumerable.Repeat(1, 1024).ToArray();

חג פסח שמח!

שתף