Menu

C# Nullable: int?, האופרטור ??, ?. ובדיקות null

כל מה שצריך לדעת על null ב-C#: טיפוסי ערך nullable (int?, Nullable<T>) עם HasValue, Value ו-GetValueOrDefault, האופרטורים ?? ו-??=, האופרטור ?., בדיקות null וטיפוסי הפניה nullable (string?) מ-C# 8.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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). היא משנה רק אזהרות, לא את ההתנהגות בזמן ריצה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל