השיטה הנכונה היא להשתמש בkeyword ששמו finally שתפקידו הוא לוודא שפעולה שאנחנו רוצים קורית בסופו של דבר אחרי block של try-catch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try
{
stream =
new FileStream("DailyTip.txt", FileMode.Open);
// Do a lot of dangerous things here
}
catch (Exception e)
{
// Write to log
}
finally
{
if (stream != null)
{
stream.Close();
}
}
שימו לב שאפשר לשאול מה יקרה אם יעוף exception בClose בתוך הfinally. ובכן, ברוב הפונקציות שאנחנו נרצה לקרוא להן גם במקרה של הצלחה וגם במקרה של כשלון (כלומר בfinally) חשבו על זה, ולכן הפונקציות האלה לא זורקות exceptionים. 😃
לדוגמה:
A call to Close is required for proper operation of a stream. Following a call to Close, other operations on the stream could throw exceptions. If the stream is already closed, a call to Close throws no exceptions.
ראינו בפעמים הקודמות מהו IEnumerable ולמה הוא משמש.
נניח שאנחנו רוצים לייצר סדרה חשבונית. דרך אחת לעשות זאת היא לממש IEnumerable משלנו.
אבל מה שזה אומר, זה לממש IEnumerator משלנו מפני שכל מה שיש בIEnumerable זו פונקציה של GetEnumerator.
אבל בIEnumerator יש 3 פונקציות שאנחנו צריכים לממש, ואנחנו צריכים לכתוב 2 מחלקות שלמות משלנו רק בשביל ליצור IEnumerable משלנו.
בשביל לפתור את הבעיה הזאת, המציאו את הkeyword האדיר yield return בC# 2.0.
הדבר מאפשר לנו לכתוב Enumerableים משלנו בקלות יחסית.
לדוגמה, נניח שאנחנו מעוניינים ליצור IEnumerable שמחזיר את כל המספרים בטווח שנקבל כקלט.
נוכל לכתוב את זה ככה:
1
2
3
4
5
6
7
publicstatic IEnumerable<int> Range(intfrom, int to)
{
for (int i = from; i <= to; i++)
{
yieldreturn i;
}
}
שימו לב שyield return לא בדיוק יוצא מהפונקציה כמו return. מה שקורה בעצם זה שכל הstate של הפונקציה (כל המשתנים הלוקאליים, הcallstack והשאר), נשמרים באיזשהו מקום, ובכל פעם שאנחנו עושים MoveNext על הEnumerator, אנחנו חוזרים לפונקציה עם אותו הstate הקודם!!!
דוגמה לשימוש:
1
2
3
4
5
6
IEnumerable<int> numbers = Range(100, 200);
foreach (int number in numbers)
{
// ...
}
ייתכן וחלקכם חושבים עכשיו, "יופי, יכולתי גם ליצור מערך שמכיל את המספרים מ100 עד 200".
אוקיי, אבל האם יכולתם להחזיר מערך שמכיל את כל המספרים הטבעיים?
הבעיה – כפי שאנחנו רואים יכול להיות לנו IEnumerator שמכיל Type אחד, והforeach יעבוד לנו גם כשנרוץ עליו עם Type אחר.
למשל, הקוד הבא יתקמפל:
1
2
3
4
5
6
IEnumerable people;
foreach (Fruit person in people)
{
// Do stuff
}
ונחטוף Exception על הסבה לא נכונה בזמן ריצה.
בC# 2.0, כידוע, נוספו Generics לשפה והיינו מצפים שהם ידעו לדאוג לכך שהקוד לא יתקמפל.
אם באמת נעשה משהו כזה:
1
2
3
4
5
6
IEnumerable<Person> people;
foreach (Fruit person in people)
{
// Do stuff
}
זה אכן לא יתקמפל.
Cannot convert type ‘Person’ to ‘Fruit’
האם Generics הצילו אותנו?
לא לגמרי
הסבות הן מהשטן,
אם יהיה לנו משהו כזה:
1
2
3
publicinterfaceIPerson
publicclassPerson : IPerson
publicclassFruit
וכעת נעשה משהו כזה:
1
2
3
4
5
6
IEnumerable<IPerson> people;
foreach (Fruit person in people)
{
// Do stuff
}
הקוד שוב יתקמפל!
הסיבה היא שאמנם Fruit אינו מממש IPerson, אבל אולי יש מישהו שיורש מFruit שכן מממש IPerson?
נוכל לפתור את הבעיה האחרונה אם המחלקה שלנו היא sealed:
1
publicsealedclassFruit
במקרה כזה:
1
2
3
4
5
6
IEnumerable<IPerson> people;
foreach (Fruit person in people)
{
// Do stuff
}
לא מתקמפל. אנחנו מקבלים את השגיאה הבאה בזמן קימפול:
Cannot convert type ‘IPerson’ to ‘Fruit’
מאחר וFruit אינו מממש IPerson, והוא sealed, לא ייתכן שיש מישהו שיורש מFruit שכן מממש IPerson.
מסקנות שכדאי לקחת מכאן:
כאשר אתם עובדים עם דברים שמממשים סתם IEnumerable, צריך מאוד להיזהר על הType שעושים איתו foreach. הנושא כאוב מאוד לגבי Collectionים מFramework 1.0 שלא הועברו לממשק הגנרי, ביניהם הCollectionים שבאו מSystem.Data (ביניהם DataTable וכל החברים), והCollectionים שבאו מSystem.Windows.Forms.
כאשר אתם עובדים עם IEnumerable של ממשק, גם עליכם להיזהר!
תשתדלו להפוך מחלקות לsealed אם אתם לא רוצים שירשו מהן.