null פירושו "אין ערך". טיפוסי הפניה (מחלקות, מחרוזות, מערכים) תמיד יכלו להחזיק אותו. טיפוסי ערך (int, decimal, bool, DateTime, structs) לא יכולים, אלא אם הופכים אותם ל-nullable עם ?. C# נותנת לכם אז קבוצה קטנה של אופרטורים לעבודה עם ערכים שעשויים לחסור: ??, ??= ו-?..
טיפוסי ערך nullable: int?
הוסיפו ? לטיפוס ערך כדי לאפשר null. int? הוא קיצור של ה-struct Nullable<int>, שמחזיק ערך יחד עם דגל שאומר אם הערך קיים:
פלט:
False
True
True
5.50
0
3
Rated 4 stars
4
מקורות נפוצים לטיפוסי ערך nullable הם שדות טופס אופציונליים, פרמטרים של שאילתה ועמודות NULL במסד נתונים: Order עם DateTime? ShippedAt עוד לא נשלח כל עוד הערך הוא null. החלופה, ערך קסם כמו -1 או DateTime.MinValue, נראית כמו נתון אמיתי, ובמוקדם או במאוחר מישהו משתמש בה ככזה.
int? מומר ל-int רק במפורש, כי ההמרה עלולה להיכשל. זה מביא אותנו לחריגה היחידה שטיפוסי ערך nullable זורקים.
"Nullable object must have a value"
קריאה של .Value כשאין ערך, או המרה של int? שהוא null ל-int, זורקת InvalidOperationException:
פלט:
InvalidOperationException: Nullable object must have a value.
0
0
not stocked
stock is int count היא תבנית: היא בודקת אם יש ערך ומחלצת אותו ל-count בצעד אחד, וזה קריא יותר מ-HasValue ועוד Value. בדף על pattern matching יש צורות נוספות.
חשבון והשוואות עם null
אופרטורים על טיפוסי ערך nullable הם מורמים (lifted): אם אחד האופרנדים הוא null, התוצאה היא null. השוואות סדר (<, >, <=, >=) הן היוצא מן הכלל: הן מחזירות false ולא null. (היוצא מן הכלל השני הוא bool? עם & ו-|, שבו null & false הוא false.) לדוגמה:
פלט:
True
[]
False
False
True
True
גם a > 5 וגם a <= 5 הם false, ולכן !(a > 5) אינו זהה ל-a <= 5 כש-a הוא null. קוד כמו if (!(age >= 18)) Deny(); מתייחס לגיל חסר כאל גיל מתחת ל-18, וזה אולי מה שהתכוונתם ואולי לא. החליטו קודם מה null אמור לייצג, בדרך כלל עם ??, ורק אז השוו.
אופרטור ה-null-coalescing ??
a ?? b מחזיר את a כש-a אינו null, ואחרת את b. הוא עובד על טיפוסי ערך nullable ועל טיפוסי הפניה, ואפשר לשרשר אותו משמאל לימין:
פלט:
dark
16px
20
ANONYMOUS
הצד הימני מחושב רק כשצריך, כך ש-cache ?? LoadFromDatabase() לא פונה למסד הנתונים כשיש ערך במטמון. ?? מקבל גם ביטוי throw, וזה יוצר שומר קומפקטי (ראו throw):
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
??= (C# 8)
x ??= y מציב את y ב-x רק אם x הוא null. זה הביטוי המקובל לאתחול עצל (lazy):
private List<string> tags;
public void AddTag(string tag)
{
tags ??= new List<string>(); // C# 8 and later
tags.Add(tag);
}
לפני C# 8, כתבו tags = tags ?? new List<string>(); או if (tags == null) tags = new List<string>();.
אופרטור ה-null-conditional ?.
a?.Member מחשב את Member רק אם a אינו null; אחרת כל הביטוי הוא null. הוא הופך שרשרת של בדיקות null לביטוי אחד:
פלט:
Braga
(no city)
(no customer)
0
True
handler: saved
פרטים שכדאי להכיר:
- האופרטור מקצר את שאר השרשרת: ב-
nobody?.Address?.City, ברגע ש-nobodyהוא null שום דבר מימינו לא רץ. - כשהאיבר מחזיר טיפוס ערך, התוצאה הופכת ל-nullable:
Orders?.Countהואint?, ולכן הוא צריך?? 0כדי לחזור להיותint. - לפני C# 14,
?.לא יכול להיות יעד של השמה:customer?.Name = "x"לא מתקמפל בגרסאות קודמות. handler?.Invoke(...)היא הדרך המקובלת להפעיל event או לקרוא ל-callback אופציונלי.
השתמשו ב-?. כש-null הוא מצב לגיטימי. פיזור שלו בכל מקום כדי להימנע מחריגות מסתיר את הבאג שיצר את ה-null הלא צפוי, והתוכנית ממשיכה עם נתונים חסרים במקום להיכשל במקום שבו נמצאת הבעיה.
בדיקת null
if (customer == null) { ... } // classic
if (customer is null) { ... } // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... } // C# 9
if (customer is { } c) { ... } // C# 8 property pattern: not null, and named
is null תמיד מבצע בדיקת null אמיתית, ואילו == קורא להעמסת האופרטור של הטיפוס אם יש כזו, כך שהעמסה כתובה גרוע יכולה לגרום ל-x == null לשקר. לארגומנטים שאסור שיהיו null, היכשלו מוקדם:
public void Ship(Order order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
// ...
}
טיפוסי הפניה nullable (C# 8)
הוספת ? לטיפוס ערך משנה את הטיפוס. הוספת ? לטיפוס הפניה, מאז C# 8, משנה רק את מה שהקומפיילר בודק. כשהתכונה מופעלת (<Nullable>enable</Nullable> בקובץ הפרויקט, ברירת המחדל בפרויקטים חדשים מאז .NET 6), string אומר "אף פעם לא אמור להיות null" ו-string? אומר "יכול להיות null", והקומפיילר מזהיר כשהקוד לא תואם:
#nullable enable
public class User
{
public string Email { get; set; } // warning CS8618: Non-nullable property 'Email'
// must contain a non-null value when exiting constructor.
public string? Nickname { get; set; } // allowed to be null
public int NicknameLength() => Nickname.Length;
// warning CS8602: Dereference of a possibly null reference.
public int SafeLength() => Nickname?.Length ?? 0; // no warning
}
string? input = Console.ReadLine();
string name = input; // warning CS8600: Converting null literal or possible
// null value to non-nullable type.
string checkedName = input ?? "guest"; // no warning
int len = input!.Length; // ! tells the compiler "trust me": no warning
הקומפיילר עוקב אחרי הבדיקות שלכם לאורך המתודה: אחרי if (input != null), input נחשב כלא-null. האופרטור ! (null-forgiving) משתיק אזהרה כשאתם יודעים יותר טוב מהניתוח; כל שימוש בו הוא טענה שהקומפיילר לא יכול לאמת, אז השתמשו בו לעיתים רחוקות.
שתי עובדות מבהירות את רוב הבלבול. ראשית, מדובר רק באזהרות: התוכנית רצה אותו דבר, ו-string עדיין יכול להחזיק null בזמן ריצה אם הוא הגיע מקוד בלי הערות nullable. שנית, string? אינו Nullable<string>; אין לו HasValue.
טעויות נפוצות
.Valueבלי בדיקה. זורקInvalidOperationExceptionכשהערך null. השתמשו ב-??, ב-GetValueOrDefault()או ב-is int n.- הנחה ש-
!(x > 5)אומרx <= 5. לא כש-xהוא null: כל השוואת סדר עם null היא false. - כיסוי של כל null עם
?.. זה משתיק את הסימפטום ומרחיק את הכישלון מהסיבה. - התייחסות לאזהרות nullable כאל רעש. כל אחת מסמנת שורה שיכולה לזרוק
NullReferenceException. תקנו את הזרימה או הצהירו על הכוונה עם?. - ערכי קסם במקום null.
-1,0ו-DateTime.MinValueבתור "לא ידוע" נראים כמו נתונים אמיתיים. השתמשו ב-int?וב-DateTime?.
שאלות נפוצות
מה המשמעות של int? ב-C#?
int? הוא קיצור של Nullable<int>: int שיכול להיות גם null. טיפוסי ערך כמו int, decimal, bool ו-DateTime מחזיקים בדרך כלל תמיד ערך; הוספת ? נותנת להם מצב של "אין ערך", וזה בדיוק מה ששדה טופס חסר או עמודת NULL במסד נתונים צריכים. בדקו אותו עם HasValue או != null וקראו אותו עם Value או GetValueOrDefault().
מה עושה ?? ב-C#?
a ?? b הוא אופרטור ה-null-coalescing: הוא מחזיר את a אם a אינו null, ואחרת את b. string name = input ?? "guest"; נותן ערך חלופי בביטוי אחד. אפשר לשרשר אותו (a ?? b ?? c לוקח את הערך הראשון שאינו null), ו-b מחושב רק כשצריך. ??= (C# 8) מבצע השמה של הצד הימני רק כשהמשתנה הוא null.
מה עושה ?. ב-C#?
a?.B הוא אופרטור ה-null-conditional: אם a הוא null, כל הביטוי הוא null ו-B לא מחושב בכלל, כך שאין NullReferenceException. אפשר לשרשר אותו (order?.Customer?.Address?.City), והוא עובד עם קריאות למתודות (logger?.Log(...)), אירועים (Changed?.Invoke(...)) ואינדקסרים (items?[0]). שלבו אותו עם ?? כדי לקבל ברירת מחדל: order?.Customer?.Name ?? "unknown".
מה גורם לשגיאה "Nullable object must have a value"?
קריאה של .Value מטיפוס ערך nullable שהוא null, או המרה שלו לטיפוס שאינו nullable ((int)maybe), זורקת InvalidOperationException עם ההודעה הזו. בדקו קודם את HasValue, או השתמשו ב-GetValueOrDefault(), ב-?? fallback או בתבנית: if (maybe is int n) { ... }.
איך בודקים null ב-C#?
גם x == null וגם x is null עובדים; על is null (C# 7) לא יכול להשפיע טיפוס שמעמיס את ==, ולכן בהרבה בסיסי קוד מעדיפים אותו. השלילה היא x != null, או x is not null ב-C# 9. כדי לדחות ארגומנטים שהם null, השתמשו ב-ArgumentNullException.ThrowIfNull(x) (.NET 6) או ב-_ = x ?? throw new ArgumentNullException(nameof(x)); בקוד ישן יותר.
מה הם טיפוסי הפניה nullable ב-C#?
תכונת קומפיילר של C# 8, שמופעלת עם <Nullable>enable</Nullable> (ברירת המחדל בפרויקטים חדשים של .NET 6 ואילך). כשהיא פעילה, string אומר "אף פעם לא null" ו-string? אומר "יכול להיות null", והקומפיילר מזהיר כשאתם עלולים לגשת דרך null (CS8602) או להציב null במשתנה שאינו nullable (CS8600). היא משנה רק אזהרות, לא את ההתנהגות בזמן ריצה.