Jenis referensi yang bisa memiliki nilai null

Tip

Baru mengembangkan perangkat lunak? Mulailah dengan tutorial Memulai .

Berpengalaman dalam bahasa lain? Jika Anda pernah menggunakan tipe nullable Kotlin, TypeScript strictNullChecks, atau optional Swift, model ini akan terasa familier. C# menggunakan analisis statis dan diagnostik peringatan alih-alih tipe terpisah. Baca sekilas Mengekspresikan maksud dengan anotasi dan analisis status null, lalu lanjut ke Tutorial: Ekspresikan maksud desain Anda dengan jenis referensi nullable dan non-nullable untuk menerapkan fitur tersebut.

Tipe referensi yang dapat bernilai null adalah sekumpulan fitur yang meminimalkan kemungkinan kode Anda menghasilkan System.NullReferenceException. Anda menyatakan variabel mana yang dimaksudkan untuk menahan null dan mana yang tidak, dan pengkompilasi memperingatkan ketika deklarasi tersebut tidak cocok dengan cara kode Anda menggunakannya. Perilaku runtime program Anda tidak berubah. Tipe referensi yang dapat bernilai null sepenuhnya merupakan fitur pada waktu kompilasi.

Tiga blok bangunan bekerja sama:

  • Anotasi variabel (string vs. string?) mengekspresikan referensi mana yang dimaksudkan untuk memungkinkan null.
  • Analisis status null melacak apakah nilai ekspresi tidak null atau mungkin-null pada setiap titik dalam kode Anda.
  • Atribut pada API menjelaskan kontrak yang lebih bernuansa, seperti "argumen ini bisa berupa null, tetapi nilai pengembalian null hanya ketika argumen null."

Pengompilasi menggabungkan sinyal ini untuk menghasilkan diagnostik. Peringatan pada variabel yang tidak dapat diubah ke null berarti variabel mungkin menerima null. Peringatan tentang variabel yang dapat bernilai null berarti kode mungkin mengakses referensinya tanpa pemeriksaan null terlebih dahulu. Dereferensi berarti menggunakan nilai yang dirujuk oleh variabel. Misalnya, untuk memanggil metode di atasnya (variable.Method()), membaca properti (variable.Property), atau indeks ke dalamnya (variable[0]). Mendereferensikan variabel yang memiliki nilai null melemparkan pengecualian pada waktu proses. Salah satu jenis peringatan berarti perilaku kode tidak cocok dengan desain yang dinyatakan.

Konteks dapat bernilai null

Proyek yang dibuat dari templat .NET terbaru mengatur <Nullable>enable</Nullable> dalam file proyek, sehingga panduan dalam artikel ini berlaku seperti yang ditulis. Jika Anda bekerja di proyek yang lebih lama, buka .csproj dan periksa apakah <PropertyGroup> berisi baris berikut; tambahkan jika tidak ada:

<Nullable>enable</Nullable>

Untuk informasi selengkapnya tentang cara memigrasikan aplikasi besar, lihat artikel mengenai strategi migrasi untuk tipe nullable yang memuat pengaturan dan arahan tambahan.

Ungkapkan maksud dengan anotasi

Setiap variabel jenis referensi tidak dapat diubah ke null secara default. Tambahkan ? untuk mendeklarasikan jenis referensi nullable :

public static void Annotations()
{
    string required = "always set";   // non-nullable: assigning null produces a warning
    string? optional = null;          // nullable: holding null is allowed

    Console.WriteLine(required.Length);

    if (optional is not null)
    {
        Console.WriteLine(optional.Length);
    }
}

Anotasi tidak mengubah jenis runtime. string dan string? keduanya System.String. ? memberi tahu pengkompilasi tentang maksud desain Anda. Niat itu membentuk peringatan yang dihasilkan pengkompilasi:

  • Variabel yang tidak menerima nilai null memiliki status null bawaan berupa bukan null. Kompilator memperingatkan jika Anda memberikan nilai yang mungkin berupa null.
  • Variabel yang dapat bernilai null secara bawaan memiliki keadaan nullmaybe-null. Pengkompilasi memperingatkan jika Anda mendereferensikan variabel tanpa terlebih dahulu memeriksanya.

Gunakan anotasi untuk membuat nilai wajib dan opsional terlihat dalam sistem tipe. Jenis berikut Person menyatakan FirstName dan LastName sebagai tidak dapat diubah ke null—setiap orang harus memiliki keduanya—dan MiddleName sebagai nullable, karena tidak semua orang memilikinya:

public sealed class Person(string firstName, string lastName)
{
    public string FirstName { get; } = firstName;
    public string? MiddleName { get; init; }
    public string LastName { get; } = lastName;

    public override string ToString() => MiddleName is null
        ? $"{FirstName} {LastName}"
        : $"{FirstName} {MiddleName} {LastName}";
}

public static void DesignIntent()
{
    Person p1 = new("Ada", "Lovelace") { MiddleName = "King" };
    Console.WriteLine(p1);
    // Output: Ada King Lovelace

    Person p2 = new("Grace", "Hopper");
    Console.WriteLine(p2);
    // Output: Grace Hopper
}

Anotasi mendorong implementasi ToString. Karena FirstName dan LastName tidak dapat bernilai null, override menggunakannya secara langsung dalam string yang diinterpolasi (sintaks $"..." yang menyematkan ekspresi dalam placeholder {}) tanpa pemeriksaan null. MiddleName dapat bernilai null, sehingga override terlebih dahulu memeriksanya terhadap null dan hanya menyertakannya jika tersedia. Kompilator memberlakukan perbedaan: kode yang melewati nilai mungkin-null di mana yang tidak dapat diubah ke null diharapkan menghasilkan peringatan, dan konstruktor yang membuat anggota yang tidak dapat diubah ke null tidak diinisialisasi juga menghasilkan peringatan.

Analisis kondisi null

Pengkompilasi melacak status null dari setiap ekspresi. Status adalah salah satu dari dua nilai:

  • not-null: ekspresi diketahui bukan null.
  • mungkin-null: ekspresinya mungkin null.

Status null variabel lokal diperbarui saat pengkompilasi menganalisis kode Anda. Dua hal mengubahnya: penugasan dan pemeriksaan null. Setelah penetapan, status null variabel cocok dengan ekspresi di sisi kanan. Jika ekspresi bernilai null atau nullable, variabel dianggap mungkin bernilai null. Jika ekspresi adalah sebuah literal yang bukan null, variabel menjadi bukan null. Setelah pemeriksaan null, status null variabel mencerminkan cabang mana pun yang diambil.

public static void NullStateTracking()
{
    string? message = null;

    // Warning: dereference of a possibly null reference.
    Console.WriteLine(message.Length);

    message = "Hello, World!";

    // No warning: the compiler tracks that message is now not-null.
    Console.WriteLine(message.Length);
}

Dalam contoh sebelumnya, dereferensi pertama memunculkan peringatan karena messagemungkin bernilai null. Setelah penugasan ke literal non-null, kompilator mengetahui bahwa messagetidak null, sehingga dereferensi kedua tersebut aman.

Analisis status null berfungsi pada pemeriksaan if, pencocokan pola (ekspresi seperti is null atau is { } yang menguji struktur suatu nilai), dan alur kontrol yang melakukan perulangan atau mengembalikan lebih awal:

 public sealed class Node(string name)
 {
     public string Name { get; } = name;
     public Node? Parent { get; init; }
 }

 public static void FlowAnalysis(Node start)
 {
     Node? current = start;
     while (current is not null)
     {
         // Inside the loop, the compiler knows current is not-null.
         Console.WriteLine(current.Name);

         current = current.Parent;
     }
}

Analisis tidak menelusuri isi metode. Jika Anda memerlukan metode untuk mengomunikasikan status null kepada pemanggilnya, gunakan atribut analisis nullable pada tanda tangannya.

Mengesampingkan peringatan dengan !

Kadang-kadang Anda tahu lebih dari pengkompilasi. Operator pengabaian null! menyatakan bahwa sebuah ekspresi bukan null, bahkan ketika analisis menyatakan sebaliknya:

public static void NullForgiving()
{
    // "ada" matches a switch arm that returns a non-null string,
    // but the return type is string? so the compiler treats the
    // result as maybe-null.
    string? maybeName = LookUpName("ada");

    // The ! tells the compiler "trust me, this isn't null." We just
    // passed "ada", which the switch maps to "Ada Lovelace".
    int length = maybeName!.Length;
    Console.WriteLine(length); // => 12
}

// Returns string? because the wildcard arm yields null.
private static string? LookUpName(string id) => id switch
{
    "ada" => "Ada Lovelace",
    _ => null,
};

Gunakan ! dengan hemat. Setiap kemunculan adalah tempat pengkompilasi tidak dapat lagi melindungi Anda. Lebih suka menambahkan pemeriksaan null, merestrukturisasi kode, atau membuat anotasi API yang relevan sehingga kompilator mencapai kesimpulan yang tepat sendiri.

Atribut yang menjelaskan kontrak API

Anotasi pada parameter atau jenis pengembalian tidak selalu cukup ekspresif. Metode mungkin menerima argumen yang mungkin null tetapi menjamin hasil non-null. Metode pengujian mungkin hanya mengembalikan true ketika argumennya tidak null. Gunakan atribut analisis nullabilitas untuk menyatakan kontrak ini:

public static bool IsPresent([NotNullWhen(true)] string? value) =>
    !string.IsNullOrEmpty(value);

public static void NullAnalysisAttributes()
{
    string? input = ReadInput();

    if (IsPresent(input))
    {
        // No null-forgiving operator needed: the attribute tells the compiler
        // input is not-null when IsPresent returns true.
        Console.WriteLine(input.Length);
    }
}

private static string? ReadInput() => "hello";

NotNullWhenAttribute memberi tahu pengkompilasi bahwa ketika IsPresent mengembalikan true, argumen tidak null. Di dalam blok if, kompilator menganggap value sebagai bukan null tanpa memerlukan operator null-forgiving. Pada .NET 5, semua API runtime .NET diannotasikan, sehingga analisis menguntungkan kode apa pun yang memanggilnya.

Perangkap yang diketahui

Dua pola dapat membuat referensi non-nullable berisi null tanpa peringatan. Kedua pola adalah batasan analisis statis, bukan bug dalam kode Anda.

Struktur default

Anda dapat membuat struct dengan bidang referensi yang tidak dapat diubah ke null dengan menggunakan default atau new(). Pendekatan ini membiarkan field pada struct tidak diinisialisasi:

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static void DefaultStructPitfall()
{
    Student s = default;            // No warning, but FirstName and LastName are null.
    Console.WriteLine(s.FirstName?.Length ?? -1);
}

Field tersebut berisi null saat runtime, tetapi kompiler tidak memberikan peringatan. Jika Anda harus menggunakan struktur, utamakan anggota wajib, yaitu anggota yang harus diinisialisasi oleh pemanggil melalui penginisialisasi objek, atau konstruktor berparameter yang wajib dipanggil oleh pemanggil.

Array referensi dan struktur

Larik baru dari tipe referensi non-nullable berisi elemen null seluruhnya sampai Anda menetapkan nilai untuk masing-masing elemennya:

public static void ArrayPitfall()
{
    string[] values = new string[3];      // Elements are null at run time.
    Console.WriteLine(values[0]?.Length ?? -1);

    string[] initialized = ["a", "b", "c"]; // Collection expression initializes every slot.
    Console.WriteLine(initialized[0].Length);
}

Jebakan yang sama juga berlaku untuk larik struct: setiap elemen awalnya bernilai nilai default struct tersebut, sehingga setiap field referensi yang tidak dapat bernilai null pada tiap elemen awalnya bernilai null.

Menginisialisasi elemen array sebagai bagian dari pembuatan array. Ekspresi koleksi (sintaks literal [1, 2, 3]) dan new bertipe target (menulis new() ketika pengompilasi dapat menyimpulkan tipenya) membuat inisialisasi lengkap menjadi ringkas.