לפעמים נרצה להשתמש בSyntax יותר יפה לבדיקת זמנים במחלקות שלנו. דרך אחת לעשות זאת היא לממש את המתודה Dispose של הממשק IDisposable כדי לכתוב את הזמן לConsole, בדרך כלל אני משתמש בשיטה הזו מכיוון שהיא נוחה יותר, ולקוד מהיר יותר נוח לי להשתמש בה בטווח הבינוני.
לפעמים אנחנו מעוניניים לדעת מה כ”כ איטי בתוכנה שלנו, בזמן Debugging ניתן להתחבר בAttach, לעשות Pause – וססטטיסטית, אם נחזור על הפעולה הזו כמה פעמים, נבין איפה נמצאים החלקים הכבדים בקוד.
אנשים שעוד לא גילו את הProfiler (או שהבינו שהוא יותר מדי שקרן), או אנשים שהם עצלנים כמוני, בדר”כ כותבים כל מיני קטעים בקוד שקשורים לבדיקות זמנים.
הם כותבים קטע כזה בערך :
1
2
3
4
5
6
7
DateTime startTime = DateTime.Now;
DoHeavyWeightStuff();
DateTime endTime = DateTime.Now;
Console.WriteLine("Time took for DoHeavyWeightStuff : {0}", endTime - startTime);
מי שכבר יצא לו יותר מדי פעמים לבצע את החיסור הזה, בטח כבר חפר בStackoverflow/Google/MSDN (או שלא) ומצא שיש מחלקה בשם Stopwatch שנמצאת בNamespace System.Diagnostics ומאפשרת לנו לבדוק זמנים כך :
1
2
3
4
5
6
7
Stopwatch watch = new Stopwatch();
watch.Start();
DoHeavyWeightStuff();
Console.WriteLine("Time took for DoHeavyWeightStuff : {0}", watch.Elapsed);
ניתן לקצר את זה ואפילו לכתוב כך:
1
2
3
4
5
Stopwatch watch = Stopwatch.StartNew();
DoHeavyWeightStuff();
Console.WriteLine("Time took for DoHeavyWeightStuff : {0}", watch.Elapsed);
מה שנחמד במחלקה הזו, זה שניתן להשתמש בStopwatch אחד כדי לבדוק זמן ריצה של מרכיב אחד בתוכנית שלנו לאורך כל התוכנית, בעזרת הפעולות Start וStop. כלומר, אפשר להשתמש באובייקט הזה למשך הרבה זמן ולעשות לו Reuse. והוא גם יעיל (לא משתמש במערך/רשימה בתוכו) 😃
ד.א – לא תמיד ניתן להשתמש בProfiler לבדיקת זמנים (אפליקציות מבצעיות וכו’), במקרים כאלה אשתמש בStopwatch.
לפעמים אנחנו רוצים לעבור על IDictionary ובו”ז לשנות בו את הערכים :
1
2
3
4
5
6
IDictionary<ulong, ulong> dictionary = new Dictionary<ulong, ulong>();
foreach (var kvp in dictionary)
{
dictionary[kvp.Key]++;
}
אך זה לא מתאפשר, כיוון שאי אפשר לשנות את המידע שנמצא מאחורי IEnumerator כשאנחנו מבצעים בו איטרציות. במקרה כזה יזרק לנו InvalidOperationException עם ההודעה :
Collection was modified; enumeration operation may not execute.
במקרה כזה, אני לפעמים משתמש בטיפוס שנקרא StrongBox<T>.
הטיפוס הזה מאפשר לנו לשים בתוכו ערך מסוג T, לעדכן אותו ולתשאל עליו. שימו לב, זה נורא דומה לBoxing של .net, ואפילו יקר יותר מבחינת ביצועים, אך לפעמים זה נוח יותר מאשר ליצור את מבנה הנתונים מחדש או לעדכן אותו בצורה אחרת :
1
2
3
4
foreach (var kvp in dictionary.ToArray())
{
dictionary[kvp.Key]++;
}
שימו לב לטוויסט בעלילה, לקחתי את המילון שבעצם מממש את IEnumerable<KeyValuePair<TKey, TValue>>, והשתמשתי בExtension Method – ToArray כדי לשמור את את המפתחות והערכים של מבנה הנתונים בצד ולעבור עליהן במקום לעבור על מבנה הנתונים בעצמו, כך שאני אוכל לעדכן אותו בזמן שאני עובר "עליו".
הפתרון הזה לא מאפשר לי להיות Thread Safe, כיוון שבזמן שהמתודה ToArray נקראת, מישהו אחר יכול לעדכן לי את המילון – ואז יזרק לי אותו InvalidOperationException מוכר.
אם אבחר להשתמש בStrongBox, את הדוגמה הראשונה אני הופך לכזו :
1
2
3
4
5
6
IDictionary<ulong, StrongBox<ulong>> dictionary = new Dictionary<ulong, StrongBox<ulong>>();
foreach (var kvp in dictionary)
{
dictionary[kvp.Key].Value++;
}
המקרה הזה לא יעיל בפני מחיקות והוספות של ערכים חדשים למילון, אך במידה שאנחנו רק עורכים את המידע שנמצא במילון, הקוד שלנו אפילו Thread Safe.
הכרנו בעבר את מילת הקסם השמורה var המאפשרת לנו לא לציין את הטיפוס של המשתנה שלנו, אלא לגלות אותו בצורה implicitly לפי הערך שאיתו אנחנו מאתחלים את המשתנה.
אמרתי שאני אישית לא אוהב כל כך הKeyword הזה.
לפעמים מקבלים Side Effect ע”י שימוש בKeyword הזה:
נניח שכל המשתנים שלנו מוגדרים ע”י var, והקוד שלנו הוא בסגנון הזה:
1
2
3
4
5
6
7
8
9
10
11
var listener = GetListener();
var stations = GetElements("Radio Stations");
foreach (var station in stations)
{
if (station.IsActive)
{
listener.Notify(station.Name);
}
}
מה שיכול לקרות עכשיו זה שאיכשהו שינו את ערכי ההחזר של הפונקציות GetListener וGetElements.
במידה והיינו מגדירים באופן explicitly את הטיפוסים של המשתנים, אז הקוד בהכרח לא היה מתקמפל יותר.
במידה והשתמשנו באופן implicitly בעזרת הKeyword ששמו var, אז לקוד יש עוד סיכוי להתקמפל, במידה ולא השתנה הAPI (כלומר עדיין GetElements מחזיר Enumerable של איברים עם Properties שלIsActive וName, וGetListener מחזיר ממשק שיש לו פונקציית Notify)
זה איזשהו Side Effect נחמד, שיכול לחסוך לנו מעט עבודה, אבל לא מספיק שווה כדי להשתמש באופן שוטף בKeyword ששמו var, מאחר וכאמור, זה פוגע בקריאות. (ראו גם טיפ מספר 86)
בהמשך לHackים הקודמים, לפעמים יש לנו טיפוס אנונימי ואנחנו מעוניינים ליצור ממנו מבנה אחר, למשל ליצור רשימה שמכילה מספר אובייקטים אנונימיים כאלה.
(ראו גם טיפ מספר 90)
אני אציין שזה מקרה מאוד חריג, ובד”כ אם אנחנו רוצים לעשות דבר כזה, הדבר הנכון לעשות הוא לכתוב מחלקה אמיתית (לא אנונימית) לטיפוס הזה.
כדי לעשות משהו כזה, היינו רוצים כתוב משהו כמו הקוד הזה:
1
2
3
4
5
6
7
8
var anonymous =
new
{
Name = "Benjamin",
LastName = "Button"
};
var list = new List<anonymous>() {anonymous};
כמובן, הקוד הזה לא מתקמפל (ראו גם טיפים קודמים, למשל 150, 231)
אם בכל זאת חפצה נפשנו בלעשות דבר כזה, אפשר בשיטה הבאה:
ניצור פונקציה גנרית שמקבלת את האיבר כפרמטר:
1
2
3
4
privatestatic List<T> CreateList<T>(T item)
{
returnnew List<T>();
}
ונקרא לה עם האיבר:
1
2
3
4
5
6
7
8
var anonymous =
new
{
Name = "Benjamin",
LastName = "Button"
};
var list = CreateList(anonymous);
כעת נוכל לעשות כאוות נפשנו עם הרשימה:
1
list.Add(anonymous); // Adding anonymous twice
איך זה עובד? הטריק האהוב עלי במתודות גנריות, הוא שהרבה פעמים לא צריך לציין את הפרמטר הגנרי, אלא הקומפיילר יכול לזהות אותו implicitly. זה קורה גם כאן, וזו הסיבה שהדבר הזה עובד 😃
(ראו גם טיפ מספר 28)
אפשר גם ליצור Extension Methods מתאימים, והדבר הזה יעבוד.
Feature מסוים שאין לנו תמיכה בו בC# הוא לשים Constraintים על פרמטרים גנריים כך שירשו מהטיפוסים Array, Enum, Delegate או ValueType.
(ראו גם טיפ מספר 29)
תכלס, בשביל שני האחרונים אפשר להסתדר: במקום ValueType, אפשר לכתוב
1
where T : struct
(ראו גם טיפ מספר 31)
ובמקום Array, אפשר לדאוג שהפונקציה תקבל T[] (או אולי T[,] או T[,,] וכו’, תלוי במימד של המערך)
עם זאת, השניים הראשונים לפעמים מתבקשים. עשינו בעבר מספר טריקים, למשל שמנו Constraint ש
1
where T : IConvertible
כדי לצמצם את מספר האפשרויות שישלחו טיפוס שהוא לא Enum.
(ראו גם טיפים מספר 72, 209)
מסתבר שבאופן כללי, הCLR דווקא כן תומך בFeature הזה. אם תקחו קוד IL עם Constraint רגיל, ותשנו את הIL שהטיפוס שממנו יורש T הוא Enum, זה דווקא יעבוד לכם! (כלומר, אם תוסיפו Reference לDLL הזה, באמת תהיה לכם את האכיפה)
מישהו מצא באינטרנט Hack מתוחכם בשפה המאפשר להשתמש בConstraint הזה בכל זאת:
השימוש הוא באמצעות Naked Constraints (ראו גם טיפ מספר 34) ונראה ככה:
הדבר הזה אמור לעבוד גם על Delegateים, אבל לא ניסיתי את זה.
בכל מקרה זה בעיקר מגניב, אבל לא הייתי ממליץ לאף אחד ליצור מחלקת ביניים בשביל הFeature הזה.
(אלא אם כן יש לכם סיבה ממש ממש ממש טובה, וכנראה אין לכם)
שימו לב שזה לא מאפשר לנו לעשות Extension Methods לEnum, כי אנחנו צריכים פה ירושה, ואין ירושה במחלקות סטטיות. גם אם הייתה, זה היה בלתי אפשרי, מאחר ולא ניתן להגדיר Extension Methods במחלקות גנריות סטטיות.
הפתרון הזה נמצא ע"י אחד המנהלים מStackOverflow עם הכינוי SLaks
לפעמים מעבירים לנו לפונקציה בתור פרמטר את הערך null למרות שאנחנו לא יודעים איך להתמודד איתו.
בשביל זה קיים הException בשם ArgumentNullException שמציין שקיבלנו באחד הארגומנטים null, למרות שציפינו לקבל משהו שהוא לא null.
כאשר אנחנו זורקים אותו, אנחנו יכולים לציין את השם של המשתנה והודעה, למשל:
1
2
3
4
5
6
7
8
9
publicstaticstringReverse(string source)
{
if (source == null)
{
thrownew ArgumentNullException("source", "Can't reverse a null value.");
}
returnnewstring(source.Reverse().ToArray());
}
בסה"כ נחמד, אבל יש פה כמה בעיות קטנות (שאפשר כמובן לחיות איתן):
צריך להשקיע 4 שורות קוד בשביל בדיקה שערך מסוים הוא null וזריקת Exception. אפשר להגיד שאפשר לכתוב את זה בשתי שורות או אחת, אבל בדרך מקובל לא להתקמצן על הסוגריים של ה if, כי זה תורם לקריאות.
בנוסף, תחשבו שאם יש לכם יותר ארגומנטים שיכולים להיות null, הדבר הזה מבזבז לכם הרבה שורות מהפונקציה שלכם.
בעיית תחזוקה: אמנם קשה לראות את זה מכאן, אבל השם של הארגומנט שלנו נמצא בתור מחרוזת. מה שזה אומר, זה שאם יבוא יום אחד מישהו וישנה את הקוד, ויחליף את השם של הארגומנט, כנראה הוא לא ישים לב שהוא נמצא גם בתור מחרוזת, והדבר הזה יגרום לכך שיעוף Exception עם שם לא נכון בתוכו. אלא אם כן יש לכם כלים חכמים כמו Resharper שיודעים לנתח את זה, כנראה שהדבר הזה יקרה. האם זה באג קריטי? לא, אבל חבל להכניס באגים כאלה מראש, במיוחד אם אתם כותבים תשתית שאנשים אחרים משתמשים בה, ואז הם לא מבינים מה בדיוק לא בסדר…
אז מה אפשר לעשות? אפשר מצד אחד לכתוב פונקציה נחמדה כזאת:
ICollection<TSource> is2 = source as ICollection<TSource>;
if (is2 != null)
{
return is2.Contains(value);
}
return source.Contains<TSource>(value, null);
}
יש כאן אופטימיזציה: אם הטיפוס הוא ICollection<TSource>, פשוט מבצעים הסבה וקוראים לContains. אלא שהדבר הזה יכול לגרור התנהגות לא ברורה:
אם נניח נריץ את הקוד הבא:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerable<string> names =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"Jason",
"jameson",
"jeFferson",
"JacKson",
"Robert Lewis Stevenson"
};
if (names.Contains("jason"))
{
// true
}
לכאורה תקין. האוסף אמנם לא מכיר את המחרוזת הLower case, אבל הוא מכיל את המחרוזת "Jason", ולכן אולי אפשר להניח שזה הגיוני.
קונספטואלית זה דווקא לא הגיוני. הרי Contains הוא Extension Method של IEnumerable<T> ולכן היינו מצפים שהוא בודק האם האיבר קיים כאיבר המתקבל בריצה על הEnumerable באמצעות הComparerהדיפולטי. אם היינו רוצים לציין לו EqualityComparer שהוא ישתמש בו, היינו משתמשים בOverload השני המקבל EqualityComparer.
ניתן לנסות לשכנע את עצמנו שזו כן ההתנהגות שהיינו רוצים, אבל הטיעון הבא לדעתי משכנע מאוד:
נניח שהEnumerable באמת מכיל את המחרוזת הLower case. אז היינו מצפים שגם הEnumerable הבא יכיל אותה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerable<string> names =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)