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 לא טוב.

שתף