300. StopwatchBlock

[נכתב ע”י שני אלחרר]

אהלן.

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

לפעמים נרצה להשתמש בSyntax יותר יפה לבדיקת זמנים במחלקות שלנו. דרך אחת לעשות זאת היא לממש את המתודה Dispose של הממשק IDisposable כדי לכתוב את הזמן לConsole, בדרך כלל אני משתמש בשיטה הזו מכיוון שהיא נוחה יותר, ולקוד מהיר יותר נוח לי להשתמש בה בטווח הבינוני.

אז איך נעשה את זה?

  • ניצור מחלקה חדשה שבC’tor שלה יוצרת Stopwatch
  • ב Disposeניצא את הזמן לפונקציה אחרת

זה נראה בערך כך :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StopwatchBlock : IDisposable
{
private readonly Stopwatch mStopwatch = Stopwatch.StartNew();
private readonly Action<TimeSpan> mOnEnd;
public StopwatchBlock(Action<TimeSpan> onEnd)
{
mOnEnd = onEnd;
}
#region IDisposable Members
public void Dispose()
{
mOnEnd(mStopwatch.Elapsed);
}
#endregion
}

ולצורך העניין, אם יש לי מתודה כזו :

1
2
3
4
static Action<TimeSpan> GetWriteTimeToConsole(string withName)
{
return timeSpan => Console.WriteLine("{0} Took {1}", withName, timeSpan);
}

אוכל להשתמש במחלקה שלי כך :

1
2
3
4
using (new StopwatchBlock(GetWriteTimeToConsole("DoHeavyWeightStuff")))
{
DoHeavyWeightStuff();
}

מרחיקי הלכת יצרו מתודות-עזר-סטטיות כאלו :

1
2
3
4
static IDisposable GetConsoleStopwatchBlock(string withName)
{
return new StopwatchBlock(GetWriteTimeToConsole(withName));
}

ואז ישתמשו בהן כך :

1
2
3
4
using (GetConsoleStopwatchBlock("DoHeavyWeightStuff"))
{
DoHeavyWeightStuff();
}

נחמד, לא?

החסרון בשיטה הזו נובע מביצועים, אך מדובר בהבדל זניח לעומת שימוש Native במחלקה Stopwatch. כמו כן, אי אפשר לעשות Reuse לStopwatchBlock.

בברכת Syntax קריא יותר - קוד בריא יותר.

שתף

299. Compiler switches

[נכתב ע”י עודד לזר]

אהלן,

עמית שאל אותי היום אם אני יודע איך יודעים אם assembly קומפל בrelease או בdebug ואני חושב שהתשובה שלי תוכל לעזור להרבה אנשים…

למי שלא יודע, debug / release הם בסה”כ אוסף של הגדרות לcompiler (או בלעז: compiler switches) שעוזרים לו להבין איך לקמפל את הassembly לil.

בהגדרה, קימפול ב-debug מבטל את אופטימיזציות של הקומפיילר / JIT ומאפשר edit and continue,

לעומתו, release מפעיל את כל האופטימיזציות (גם אופטמיזציות של הקומפיילר וגם אופטימיזציות בruntime)

חשוב שתדעו שלא חייבים לקמפל את הקוד בdebug\release אלא אפשר ממש לתת לו את הדגלים שאיתם רוצים לקמפל.

בכל מקרה, המידע כיצד קומפל הassembly יושב בmanifest וניתן לגשת אליו דרך il dissassembler:

אם תסתכלו בmanifest, תראו שהוצמד אליו Attribute בשם DebuggubleAttribe שמחזיק enum עם הcompiler switches שהשתמשתם בהם.

הenum נראה ככה:

1
2
3
4
5
6
7
8
public enum DebuggingModes
{
None = 0,
Default = 1,
IgnoreSymbolStoreSequencePoints = 2,
EnableEditAndContinue = 4,
DisableOptimizations = 256,
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
static void Main(string[] args)
{
string filePath = @"d:\demo\debug.exe";
var assembly = Assembly.LoadFile(filePath);
var debuggubleAttributes = (from attribute in assembly.GetCustomAttributes(false)
where attribute is DebuggableAttribute
select attribute as DebuggableAttribute).Single();
Console.WriteLine("IsJITOptimizerDisabled - " + debuggubleAttributes.IsJITOptimizerDisabled);
Console.WriteLine("IsJITTrackingEnabled - " + debuggubleAttributes.IsJITTrackingEnabled);
Console.WriteLine("DebuggableAttributes - " + debuggubleAttributes.DebuggingFlags.ToString());
}

שיהיה יום בדיקת compiler switches מעולה.

שתף

298. Stopwatch

[נכתב ע”י שני אלחרר]

אהלן.

לפעמים אנחנו מעוניניים לדעת מה כ”כ איטי בתוכנה שלנו, בזמן 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.

יום בדיקת זמנים מעולה.

שתף

297. StrongBox

[נכתב ע”י שני אלחרר]

אהלן.

היום אדבר על פתרון נחמד לבעיה הבאה :

לפעמים אנחנו רוצים לעבור על 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.

בברכת ערב קופסא חזקה.

שתף

296. Decorator and var

הכרנו בעבר את הDesign Pattern המגניב בשם Decorator (ראו גם טיפ מספר 85)

הבעיה העיקרית בDesign Pattern זה הוא שתמיד אנחנו רואים רק את הטיפוס האחרון, ואז לפעמים קשה להבין (למשל דיבוג) מאיזה טיפוס הDecorator שלנו.

אפשר לפתור בעיה זו בדרך הבאה:

נניח שיש לנו את הממשק של Logger:

1
2
3
4
public interface ILog
{
void Log(string content);
}

כעת יצרנו לו כמה עטיפות:

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
27
28
29
30
31
32
33
34
35
36
public class DateLogger : ILog
{
private readonly ILog mSource;
public DateLogger(ILog source)
{
mSource = source;
}
public void Log(string content)
{
mSource.Log
(string.Format("Content: {0}, Date: {1}",
content,
DateTime.Now));
}
}
public class ConsoleLogger : ILog
{
private readonly ILog mSource;
private readonly ConsoleColor mColor;
public ConsoleLogger(ILog source, ConsoleColor color)
{
mSource = source;
mColor = color;
}
public void Log(string content)
{
Console.ForegroundColor = mColor;
Console.WriteLine(content);
mSource.Log(content);
}
}

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

נוסיף פרמטר גנרי שיהיה שווה לטיפוס הקודם של הLogger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DateLogger<TSource> : ILog
{
private readonly ILog mSource;
public DateLogger(ILog source)
{
mSource = source;
}
public void Log(string content)
{
mSource.Log
(string.Format("Content: {0}, Date: {1}",
content,
DateTime.Now));
}
}

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

1
2
3
4
5
public static DateLogger<TSource> WithDate<TSource>(this TSource source)
where TSource : ILog
{
return new DateLogger<TSource>(source);
}

נעשה אותו הדבר גם לDecorator השני ונקבל:

1
2
3
4
5
6
7
8
9
10
public class ConsoleLogger<TSource> : ILog
{
// ...
}
public static ConsoleLogger<TSource> WithConsole<TSource>(this TSource source, ConsoleColor color)
where TSource : ILog
{
return new ConsoleLogger<TSource>(source, color);
}

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

1
2
3
4
TextLogger myLogger = new TextLogger();
ConsoleLogger<DateLogger<TextLogger>> decoratedLogger =
myLogger.WithDate().WithConsole(ConsoleColor.Red);

ככה אפשר לראות מאוד בקלות מאיזה טיפוסים מורכב הLogger שלנו.

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

1
2
var decoratedLogger =
myLogger.WithDate().WithConsole(ConsoleColor.Red);

בנוסף, אפשר לתת אפשרות להוריד פונקציונאליות מהLogger ע"י הוספת Property מתאים:

1
2
3
4
5
public interface IDecoratedLog<TSource> : ILog
where TSource : ILog
{
TSource Source { get; }
}

ואז כל מחלקה שתמממש את הממשק הזה תוכל לעשות משהו כזה:

1
DateLogger<TextLogger> source = decoratedLogger.Source;

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

סופ"ש מקושט לטובה!

שתף

295. var and magic

הכרנו בעבר את מילת הקסם השמורה 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)

המשך יום קסום טוב!

שתף

294. Anonymous types and generic types

בהמשך ל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
private static List<T> CreateList<T>(T item)
{
return new 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 מתאימים, והדבר הזה יעבוד.

(ראו גם טיפים על Extension Methods, למשל 66-75)

המשך יום אנונימי טוב.

שתף

293. Generic constraint hack

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) ונראה ככה:

1
2
3
4
5
public abstract class BaseTempClass<TTemp>
{
public abstract TEnum[] GetValues<TEnum>()
where TEnum : TTemp;
}

כעת אפשר לעשות משהו כזה:

1
2
3
4
5
6
7
public class EnumProvider : BaseTempClass<Enum>
{
public override TEnum[] GetValues<TEnum>()
{
return (TEnum[]) Enum.GetValues(typeof (TEnum));
}
}

ככה תהיה ואלידיאציה בזמן קימפול:

1
2
EnumProvider provider = new EnumProvider();
BindingFlags[] flags = provider.GetValues<BindingFlags>();

הקוד הבא עם זאת, לא יתקמפל:

1
2
EnumProvider provider = new EnumProvider();
int[] flags = provider.GetValues<int>();

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

בכל מקרה זה בעיקר מגניב, אבל לא הייתי ממליץ לאף אחד ליצור מחלקת ביניים בשביל הFeature הזה.

(אלא אם כן יש לכם סיבה ממש ממש ממש טובה, וכנראה אין לכם)

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

הפתרון הזה נמצא ע"י אחד המנהלים מStackOverflow עם הכינוי SLaks

המשך יום מלא Enumים בטוחים טוב!

שתף

292. ArgumentNullException and more

[מבוסס על הפוסט הזה של Jon Skeet]

לפעמים מעבירים לנו לפונקציה בתור פרמטר את הערך null למרות שאנחנו לא יודעים איך להתמודד איתו.

בשביל זה קיים הException בשם ArgumentNullException שמציין שקיבלנו באחד הארגומנטים null, למרות שציפינו לקבל משהו שהוא לא null.

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

1
2
3
4
5
6
7
8
9
public static string Reverse(string source)
{
if (source == null)
{
throw new ArgumentNullException("source", "Can't reverse a null value.");
}
return new string(source.Reverse().ToArray());
}

בסה"כ נחמד, אבל יש פה כמה בעיות קטנות (שאפשר כמובן לחיות איתן):

  • צריך להשקיע 4 שורות קוד בשביל בדיקה שערך מסוים הוא null וזריקת Exception. אפשר להגיד שאפשר לכתוב את זה בשתי שורות או אחת, אבל בדרך מקובל לא להתקמצן על הסוגריים של ה if, כי זה תורם לקריאות.

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

  • בעיית תחזוקה: אמנם קשה לראות את זה מכאן, אבל השם של הארגומנט שלנו נמצא בתור מחרוזת. מה שזה אומר, זה שאם יבוא יום אחד מישהו וישנה את הקוד, ויחליף את השם של הארגומנט, כנראה הוא לא ישים לב שהוא נמצא גם בתור מחרוזת, והדבר הזה יגרום לכך שיעוף Exception עם שם לא נכון בתוכו. אלא אם כן יש לכם כלים חכמים כמו Resharper שיודעים לנתח את זה, כנראה שהדבר הזה יקרה. האם זה באג קריטי? לא, אבל חבל להכניס באגים כאלה מראש, במיוחד אם אתם כותבים תשתית שאנשים אחרים משתמשים בה, ואז הם לא מבינים מה בדיוק לא בסדר…

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

1
2
3
4
5
6
7
8
public static void EnsureNotNull<T>(T value, string name)
where T : class
{
if (value == null)
{
throw new ArgumentNullException(name);
}
}

ואז לקרוא לה ככה:

1
ThrowHelper.EnsureNotNull(source, "source");

אלא שהדבר הזה לא פותר את הבעיה השלישית. כאן רואים את הכפילות המיותרת ממש בבירור.

אז מה אפשר לעשות?

אפשר לנצל את הFeatureים של C# 3.0 לעשות משהו כזה:

1
ThrowHelper.EnsureNotNull(new {source});

מה קורה כאן???

יצרנו טיפוס אנונימי עם הארגומנט שלנו כProperty. מאחר ולא נתנו שם לProperty, השם של הProperty הוא השם של הארגומנט.

כעת נותר רק לכתוב את המימוש של הפונקציה שבודקת שהארגומנט אינו null:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void EnsureNotNull<T>(T value)
where T : class
{
PropertyInfo[] properties = typeof(T).GetProperties();
foreach (PropertyInfo property in properties)
{
if (property.GetValue(value, null) == null)
{
throw new ArgumentNullException(property.Name);
}
}
}

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

1
2
3
4
5
public static Person FindPerson(string firstName, string lastName)
{
ThrowHelper.EnsureNotNull(new {firstName, lastName});
// ...
}

בסה"כ זה Hack די נחמד לבעיה הזו.

הדבר היחיד שעשוי להפריע לנו זה שאנחנו רצים כל פעם בReflection על הProperties כדי לגלות מי מהם null.

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

1
2
3
4
5
public static void EnsureNotNull<T>(T value)
where T : class
{
Check<T>.IsNull(value);
}

והבדיקה עצמה:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
internal static class Check<T>
{
private static Func<T, string> mNullChecker = GetNullChecker();
private static Func<T, string> GetNullChecker()
{
ParameterExpression parameter =
Expression.Parameter(typeof (T), "x");
Expression resultBody =
Expression.Convert(Expression.Constant(null),
typeof (string));
PropertyInfo[] properties = typeof (T).GetProperties();
foreach (PropertyInfo property in properties)
{
Expression checkPropertyNull =
Expression.Equal(Expression.Property(parameter, property),
Expression.Constant(null));
resultBody =
Expression.Condition(checkPropertyNull,
Expression.Constant(property.Name),
resultBody);
}
Expression<Func<T, string>> result =
Expression.Lambda<Func<T, string>>(resultBody, parameter);
return result.Compile();
}
public static void IsNull(T value)
{
string nullArgument = mNullChecker(value);
if (nullArgument != null)
{
throw new ArgumentNullException(nullArgument);
}
}
}

אנחנו יוצרים פה Delegate בפעם הראשונה שניגשים למחלקה שבודק איזה מהProperties הוא null ומחזיר את השם שלו.

במידה וחזר לנו משהו שהוא לא null, אנחנו זורקים Exception מתאים.

ראו גם טיפים 173-181, 90.

המשך יום עם ארגומנטים שהם לא null לא טוב.

שתף

291. About Contains extension method

יצא לנו להכיר את הExtension Methods שיש בLINQ.

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

ראינו, למשל, בטיפ מספר 15, שCount() ממומש ע”י גישה פשוטה לProperty בשם Count, במידה והIEnumerable הוא ICollection.

אולי זה מפתיע, אבל ישנו מקרה בו אופטימיזציות אלה גורמות להתנהגות מעט מוזרה.

המקרה הוא הExtension Method של Contains. זה נראה כמו Extension Method סטנדרטי:

1
2
3
4
5
6
7
8
9
10
11
public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
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)
{
"Jason",
"jameson",
"jeFferson",
"JacKson",
"Robert Lewis Stevenson"
};
IEnumerable<string> lazyNames = names.Select(x => x);
if (lazyNames.Contains("jason"))
{
// false
}

אלא שלא. כי הוא באמת משווה את האיברים כאיברים שמתקבלים ע"י ריצה על Enumerable.

את הבעיה הזו מצא Jon Skeet כשהוא ניסה לממש בעצמו את כל כל הפונקציות מLINQ, וראה שהUnit Testים שלו עוברים במקום שאלה של System.Linq נכשלים.

סופ"ש שמכיל דברים כאיברים המתקבלים בריצה על Enumerable טוב.

שתף