61. finally keyword

לעתים בכתיבת קוד, אנחנו כותבים קוד מהצורה הבאה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FileStream stream = null;
try
{
stream =
new FileStream("DailyTip.txt", FileMode.Open);
// Do a lot of dangerous things here
stream.Close();
}
catch (Exception e)
{
// Write to log
if (stream != null)
{
stream.Close();
}
}

כלומר, בסוף אנחנו בכל מקרה רוצים לסגור את הקובץ.

השיטה הנכונה היא להשתמש ב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.

שבוע גשום טוב

שתף

60. System IO Path

בסימן הטיפים היומיים הקודמים,

נניח שיש לנו נתיב של תיקיה ושם של קובץ בתוך אותה התיקייה, ואנחנו רוצים לדעת את הנתיב של הקובץ.

1
2
3
4
string myDocuments =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string myFileName = "TextFile.txt";

הדרך הלא נכונה:

1
string myFilePath = myDocuments + @"\" + myFileName;

הדרך הנכונה:

1
string myFilePath = Path.Combine(myDocuments, myFileName);

למי שממש רוצה להשתמש בדרך הלא נכונה, לפחות שיכתוב ככה:

1
string myFilePath = myDocuments + Path.DirectorySeparatorChar + myFileName;

שימו לב שזה לא תלוי במערכת ההפעלה!

באופן כללי המחלקה הסטטית Path שנמצאת בNamespace ששמו System.IO, עוזרת לנו בהרבה מאוד מקרים כאלה של עבודה עם נתיבים.

למשל, נניח שאנחנו יודעים נתיב מלא של קובץ, ואנחנו מעוניינים לדעת את הנתיב של התיקייה שבה הוא נמצא:

הדרך הלא נכונה:

1
2
string folderPath =
myFilePath.Substring(0, myFilePath.LastIndexOf(@"\"));

הדרך היותר נכונה:

1
2
string folderPath =
new FileInfo(myFilePath).Directory.FullName;

הדרך הנכונה באמת:

1
2
string folderPath =
Path.GetDirectoryName(myFilePath);

ניתן להשתמש בזה גם כדי למצוא אב של תיקייה:

1
2
string parentFolderPath =
Path.GetDirectoryName(folderPath);

עוד דברים מגניבים שאפשר לעשות עם זה:

להשיג סיומת של קובץ:

1
string extension = Path.GetExtension(myFilePath); // .txt

להחליף סיומת של קובץ:

1
string myDocumentPath = Path.ChangeExtension(myFilePath, "doc");

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

סוף שבוע מצוין

שתף

59. Environment GetFolderPath

בסימן הטיפים היומיים הקודמים,

לפעמים נרצה לגשת לתיקיות מיוחדות אחרות (יש שיגידו פחות מיוחדות), למשל Program Files, My Documents וכו’.

ניתן לעשות זאת בצורה הבאה:

1
2
3
4
5
6
7
8
string programFiles =
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string startMenu =
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
string myDocuments =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

Environment.SpecialFolder הוא enum שמכיל לא מעט תיקיות, ביניהן:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ApplicationData,
CommonApplicationData,
CommonProgramFiles,
Cookies,
Desktop,
DesktopDirectory,
Favorites,
History,
InternetCache,
LocalApplicationData ,
MyComputer,
MyDocuments,
MyMusic,
MyPictures,
Personal,
ProgramFiles,
Programs,
Recent,
SendTo,
StartMenu,
Startup,
System,
Templates

בframework 4.0 זה מכיל עוד כמה תיקיות.

בסה"כ שימושי.

יום טוב

שתף

58. RuntimeEnvironment

בהמשך לטיפים הקודמים:

רוצים לדעת איפה מותקן הCLR?

הדרך הלא נכונה:

1
2
string runtimeDirectory =
@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727";

הדרך הנכונה:

1
2
string runtimeDirectory =
RuntimeEnvironment.GetRuntimeDirectory();

יש לעשות using לSystem.Runtime.CompilerServices כדי להשתמש בזה.

יום טוב

שתף

57. MethodBase.GetCurrentMethod

רוצים לדעת מה שם המתודה שאתם נמצאים בה עכשיו?

אפשר לעשות זאת בצורה הבאה:

1
2
string methodName =
MethodBase.GetCurrentMethod().Name;

לדוגמה:

1
2
3
4
5
6
7
public static void MyMethod(string givenString,
int givenNumber)
{
Console.WriteLine
(MethodBase.GetCurrentMethod().Name);
// Prints "MyMethod".
}

שימוש נחמד לGetCurrentMethod הוא יצירה של Logger מבלי לציין את שם הType הנוכחי שאנחנו נמצאים בו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClassWithStaticLogger
{
// Static logger for the current class.
private static readonly ILog Logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Writes "Hello world" to the log
/// </summary>
public void HelloWorld()
{
if (Logger.IsDebugEnabled)
{
Logger.Debug("Hello world");
}
}
}

יום טוב

שתף

56. AppDomain.BaseDirectory

רוצים לדעת מאיפה האפליקציה שלכם רצה?

הדרך הלא נכונה:

1
2
string directory =
Environment.CurrentDirectory;

הדרך הנכונה:

1
2
string directory =
AppDomain.CurrentDomain.BaseDirectory;

הסיבה היא שCurrentDirectory יכול להשתנות במהלך האפליקציה.

שבוע טוב

שתף

55. yield return implementation

פעם שעברה, הכרנו את yield return.

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

נחזור לדוגמה של הסדרה החשבונית של אתמול:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<int> ArithmeticSequence
(int first, int difference, int count)
{
int currentTerm = first;
for (int i = 1; i <= count; i++)
{
yield return currentTerm;
currentTerm += difference;
}
}

אם נסתכל בReflector עליו נראה איזה IEnumerable שהוא גם IEnumerator מג’ונרט.

בתוכו יש מתודה כזאת:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private bool MoveNext()
{
switch (this.state)
{
case 0:
this.state = -1;
this.currentTerm = this.first;
this.i = 1;
while (this.i <= this.count)
{
this.current = this.currentTerm;
this.state = 1;
return true;
NotFirstIteration:
this.state = -1;
this.currentTerm += this.difference;
this.i++;
}
break;
case 1:
goto NotFirstIteration;
}
return false;
}

ננסה להסביר מה קורה:

כשיוצרים את הIEnumerable הזה בהתחלה, הוא מאותחל עם state = 0.

כשעושים MoveNext לראשונה הוא נכנס לחלק הראשון בו הוא מאתחל את currentTerm, לfirst ואת i ל1.

אחרי זה הוא נכנס ללולאה וקובע שהcurrent הוא currentTerm והופך את הstate להיות 1, ומחזיר שהMoveNext הצליח.

בפעם הבאה שקוראים לMoveNext, הוא עם state = 1.

הוא משנה את הState להיות -1, מגדיל את currentTerm בdifference, מגדיל את i ב1 וממשיך בלולאה.

כך באופן חוזר חלילה הוא משנה את current להיות currentTerm ומחזיר שהMoveNext הצליח, עד שi מגיע לcount.

כלומר מה שקורה הוא שהאובייקט בעצם שומר את הstate של המתודה שאנחנו נמצאים בה, וכל פעם שאנחנו עושים לו MoveNext הוא קורא למתודה מחדש עם הState שלה.

בסה"כ די מגניב.

שיהיה אחלה סופ"ש בר-מנייה

שתף

54. yield return

ראינו בפעמים הקודמות מהו IEnumerable ולמה הוא משמש.

נניח שאנחנו רוצים לייצר סדרה חשבונית. דרך אחת לעשות זאת היא לממש IEnumerable משלנו.

אבל מה שזה אומר, זה לממש IEnumerator משלנו מפני שכל מה שיש בIEnumerable זו פונקציה של GetEnumerator.

אבל בIEnumerator יש 3 פונקציות שאנחנו צריכים לממש, ואנחנו צריכים לכתוב 2 מחלקות שלמות משלנו רק בשביל ליצור IEnumerable משלנו.

בשביל לפתור את הבעיה הזאת, המציאו את הkeyword האדיר yield return בC# 2.0.

הדבר מאפשר לנו לכתוב Enumerableים משלנו בקלות יחסית.

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

נוכל לכתוב את זה ככה:

1
2
3
4
5
6
7
public static IEnumerable<int> Range(int from, int to)
{
for (int i = from; i <= to; i++)
{
yield return 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".

אוקיי, אבל האם יכולתם להחזיר מערך שמכיל את כל המספרים הטבעיים?

בדרך זו אפשר!

1
2
3
4
5
6
7
public static IEnumerable<int> NaturalNumbers()
{
for (int i= 1; ; i++)
{
yield return i;
}
}

עוד דוגמאות:

למשל, יצירת סדרה חשבונית באורך סופי:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<int> ArithmeticSequence
(int first, int difference, int count)
{
int currentTerm = first;
for (int i = 0; i < count; i++)
{
yield return currentTerm;
currentTerm += difference;
}
}

או באורך אינסופי:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<int> ArithmeticSequence
(int first, int difference)
{
int currentTerm = first;
for (int i = 1; ; i++)
{
yield return currentTerm;
currentTerm += difference;
}
}

יצירת סדרת פיבונאצ’י באורך סופי:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static IEnumerable<int> Fibbonacci(int count)
{
int current = 1;
int previous = 0;
for (int i = 0; i < count; i++)
{
yield return current;
int temp = current;
current = current + previous;
previous = temp;
}
}

או באורך אינסופי:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static IEnumerable<int> Fibbonacci()
{
int current = 1;
int previous = 0;
for (int i = 0; ; i++)
{
yield return current;
int temp = current;
current = current + previous;
previous = temp;
}
}

וככה נוכל למשל למצוא את המספר הראשון בסדרת פיבונאצ’י שמתחלק ב23:

1
2
3
4
5
6
7
8
9
10
IEnumerable<int> fibbonacciNumbers = Fibbonacci();
foreach (int number in fibbonacciNumbers)
{
if (number % 23 == 0)
{
Console.WriteLine(number);
break;
}
}

התשובה היא 46368 ד"א.

בכל אופן מדובר בכלי חזק ביותר.

המשך יום בר מניה טוב

שתף

53. foreach for ducked types

ראינו ביום ראשון שכדי שיהיה אפשר לעשות על Type כלשהו foreach, צריך למממש IEnumerable.

מסתבר שזה לא מדויק 😃

אם לא נממש IEnumerable, אבל תהיה לנו מחלקה מהסוג הזה:

1
2
3
4
5
6
7
public class ForeachHack
{
public IEnumerator GetEnumerator()
{
// ...
}
}

שלא מממשת IEnumerable(!) אז הקוד הבא יתקמפל:

1
2
3
4
5
6
ForeachHack hack = new ForeachHack();
foreach (object current in hack)
{
// Hack things
}

תכלס, זה סתם מגניב ולא כדאי לעשות את זה.

אם אתם באים לממש IEnumerable משלכם, עדיף שתממשו IEnumerable, ואפילו IEnumerable<T>.

יום בר מנייה טוב

שתף

52. IEnumerable pitfalls

פעם קודמת ראינו איך ממומש foreach.

נזכיר, foreach, לדוגמה כזה:

1
2
3
4
foreach (Person person in people)
{
// Do stuff
}

זה בעצם syntactic sugar ל

1
2
3
4
5
6
7
IEnumerator enumerator = people.GetEnumerator();
while (enumerator.MoveNext())
{
Person person = (Person) enumerator.Current;
// Do stuff
}

הבעיה – כפי שאנחנו רואים יכול להיות לנו 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
public interface IPerson
public class Person : IPerson
public class Fruit

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

1
2
3
4
5
6
IEnumerable<IPerson> people;
foreach (Fruit person in people)
{
// Do stuff
}

הקוד שוב יתקמפל!

הסיבה היא שאמנם Fruit אינו מממש IPerson, אבל אולי יש מישהו שיורש מFruit שכן מממש IPerson?


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

1
public sealed class Fruit

במקרה כזה:

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.


מסקנות שכדאי לקחת מכאן:

  1. כאשר אתם עובדים עם דברים שמממשים סתם IEnumerable, צריך מאוד להיזהר על הType שעושים איתו foreach. הנושא כאוב מאוד לגבי Collectionים מFramework 1.0 שלא הועברו לממשק הגנרי, ביניהם הCollectionים שבאו מSystem.Data (ביניהם DataTable וכל החברים), והCollectionים שבאו מSystem.Windows.Forms.
  2. כאשר אתם עובדים עם IEnumerable של ממשק, גם עליכם להיזהר!
  3. תשתדלו להפוך מחלקות לsealed אם אתם לא רוצים שירשו מהן.

המשך יום בר מנייה טוב

שתף