Atribut untuk analisis statis null-state yang ditafsirkan oleh compiler C#

Dalam konteks yang diaktifkan nullable, compiler melakukan analisis statis kode untuk menentukan status null dari semua variabel jenis referensi:

  • not-null: Analisis statis menentukan bahwa variabel memiliki nilai non-null.
  • mayben-null: Analisis statis tidak dapat menentukan bahwa variabel diberi nilai non-null.

Status ini memungkinkan compiler untuk memberi peringatan saat Anda dapat mendereferensikan nilai null, melemparkan System.NullReferenceException. Atribut ini menyediakan compiler dengan informasi semantik tentang status null dari argumen, nilai pengembalian, dan anggota objek berdasarkan status argumen dan nilai yang dikembalikan. Compiler memberi peringatan yang lebih akurat saat API Anda telah dianotasi dengan benar dengan informasi semantik ini.

Artikel ini memberi deskripsi singkat tentang setiap atribut jenis referensi yang dapat diubah ke null dan cara menggunakannya.

Mari kita mulai dengan contoh. Bayangkan pustaka Anda memiliki API berikut untuk mengambil string sumber daya. Metode ini awalnya dikompilasi dalam konteks terlupakan nullable:

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

Contoh sebelumnya mengikuti pola Try* yang akrab di .NET. Ada dua parameter referensi untuk API ini: key dan message. API ini memiliki aturan berikut yang berkaitan dengan status null dari parameter ini:

  • Pemanggil tidak boleh meneruskan null sebagai argumen untuk key.
  • Pemanggil dapat meneruskan variabel yang nilainya null sebagai argumen untuk message.
  • Jika metode TryGetMessage mengembalikan true, nilai message tidak null. Jika nilai yang dikembalikan adalah false, nilai message adalah null.

Aturan untuk key dapat diekspresikan dengan tepat: key harus merupakan jenis referensi yang tidak dapat diubah ke null. Parameter message lebih kompleks. Ini memungkinkan variabel yang null sebagai argumen, tetapi menjamin, pada keberhasilan, bahwa argumen out bukan null. Untuk skenario ini, Anda memerlukan kosakata yang lebih kaya untuk menggambarkan harapan. Atribut NotNullWhen, yang dijelaskan di bawah ini menjelaskan status null untuk argumen yang digunakan untuk parameter message.

Catatan

Menambahkan atribut ini memberi compiler informasi lebih lanjut tentang aturan untuk API Anda. Saat kode panggilan dikompilasi dalam konteks yang diaktifkan nullable, compiler akan memperingatkan pemanggil saat mereka melanggar aturan tersebut. Atribut ini tidak mengaktifkan lebih banyak pemeriksaan pada implementasi Anda.

Atribut Kategori Makna
AllowNull Prasyarat Parameter, bidang, atau properti yang tidak dapat diubah ke null mungkin null.
DisallowNull Prasyarat Parameter, bidang, atau properti yang dapat diubah ke null tidak boleh null.
MaybeNull Pascakondisi Parameter, bidang, properti, atau nilai pengembalian yang tidak dapat diubah ke null mungkin null.
NotNull Pascakondisi Parameter, bidang, properti, atau nilai pengembalian yang dapat diubah ke null mungkin null.
MaybeNullWhen Pascakondisi bersyarat Argumen yang tidak dapat diubah ke null mungkin null saat metode mengembalikan nilai bool yang ditentukan.
NotNullWhen Pascakondisi bersyarat Argumen yang tidak dapat diubah ke null mungkin null saat metode mengembalikan nilai bool yang ditentukan.
NotNullIfNotNull Pascakondisi bersyarat Nilai pengembalian, properti, atau argumen tidak null jika argumen untuk parameter yang ditentukan bukan null.
MemberNotNull Metode dan metode pembantu properti Anggota yang tercantum tidak akan null saat metode kembali.
MemberNotNullWhen Metode dan metode pembantu properti Argumen yang tidak dapat diubah ke null mungkin null saat metode mengembalikan nilai bool yang ditentukan.
DoesNotReturn Kode yang tidak dapat dijangkau Metode atau properti tidak pernah kembali. Dengan kata lain, itu selalu melemparkan pengecualian.
DoesNotReturnIf Kode yang tidak dapat dijangkau Metode atau properti ini tidak pernah mengembalikan jika parameter bool terkait memiliki nilai yang ditentukan.

Deskripsi sebelumnya adalah referensi cepat untuk apa yang dilakukan setiap atribut. Bagian berikut menjelaskan perilaku dan arti atribut ini secara lebih menyeluruh.

Prasyarat: AllowNull dan DisallowNull

Pertimbangkan properti baca/tulis yang tidak pernah mengembalikan null karena memiliki nilai default yang wajar. Pemanggil meneruskan null ke aksesor yang ditetapkan saat mengaturnya ke nilai default tersebut. Misalnya, pertimbangkan sistem olahpesan yang meminta nama layar di ruang obrolan. Jika tidak ada yang disediakan, sistem menghasilkan nama acak:

public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

Saat Anda mengompilasi kode sebelumnya dalam konteks samar-samar, semuanya baik-baik saja. Setelah Anda mengaktifkan jenis referensi nullable, properti ScreenName menjadi referensi yang tidak dapat diubah ke null. Itu benar untuk aksesor get: tidak pernah mengembalikan null. Pemanggil tidak perlu memeriksa properti yang dikembalikan untuk null. Tetapi sekarang mengatur properti untuk null menghasilkan peringatan. Untuk mendukung jenis kode ini, Anda menambah System.Diagnostics.CodeAnalysis.AllowNullAttribute atribut ke properti, seperti yang ditunjukkan dalam kode berikut:

[AllowNull]
public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

Anda mungkin perlu menambah direktif using untuk System.Diagnostics.CodeAnalysis menggunakan atribut ini dan atribut lain yang dibahas dalam artikel ini. Atribut diterapkan ke properti, bukan aksesor set. Atribut AllowNull menentukan pra-kondisi, dan hanya berlaku untuk argumen. Aksesor get memiliki nilai pengembalian, tetapi tidak ada parameter. Oleh karena itu, atribut AllowNull hanya berlaku untuk aksesor set.

Contoh sebelumnya menunjukkan apa yang harus dicari saat menambahkan atribut AllowNull pada argumen:

  1. Kontrak umum untuk variabel tersebut adalah bahwa seharusnya tidak null, jadi Anda menginginkan jenis referensi yang tidak dapat diubah ke null.
  2. Ada skenario bagi pemanggil untuk meneruskan null sebagai argumen, meskipun bukan penggunaan yang paling umum.

Paling sering Anda memerlukan atribut ini untuk properti, atau argumen in, out, dan ref. Atribut AllowNull adalah pilihan terbaik saat variabel biasanya non-null, tetapi Anda perlu mengizinkan null sebagai prasyarat.

Berbeda dengan skenario untuk menggunakan DisallowNull: Anda menggunakan atribut ini untuk menentukan bahwa argumen dari jenis referensi yang dapat diubah ke null tidak boleh null. Pertimbangkan properti di mana null adalah nilai default, tetapi klien hanya dapat mengaturnya ke nilai non-null. Pertimbangkan gambar berikut:

public string ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

Kode sebelumnya adalah cara terbaik untuk mengekspresikan desain Anda bahwa ReviewComment bisa null tetapi tidak dapat diatur ke null. Setelah kode ini sadar null, Anda dapat mengekspresikan konsep ini dengan lebih jelas kepada pemanggil menggunakan System.Diagnostics.CodeAnalysis.DisallowNullAttribute:

[DisallowNull]
public string? ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

Dalam konteks nullable, aksesor ReviewCommentget dapat mengembalikan nilai default null. Compiler memperingatkan bahwa compiler harus diperiksa sebelum akses. Selain itu, ini memperingatkan pemanggil bahwa, meskipun bisa jadi null, pemanggil tidak boleh secara eksplisit mengaturnya ke null. Atribut DisallowNull ini juga menentukan pra-kondisi, tidak memengaruhi pengakses get. Anda menggunakan atribut DisallowNull saat mengamati karakteristik ini tentang:

  1. Variabel dapat null dalam skenario inti, sering kali saat pertama kali dibuat.
  2. Variabel tidak boleh diatur secara eksplisit ke null.

Situasi ini umum dalam kode yang awalnya tidak sadar null. Mungkin properti objek diatur dalam dua operasi inisialisasi yang berbeda. Mungkin beberapa properti diatur hanya setelah beberapa pekerjaan asinkron selesai.

Atribut AllowNull dan DisallowNull memungkinkan Anda menentukan bahwa prasyarat pada variabel mungkin tidak cocok dengan anotasi nullable pada variabel tersebut. Ini memberikan detail lebih lanjut tentang karakteristik API Anda. Informasi tambahan ini membantu pemanggil menggunakan API Anda dengan benar. Ingat bahwa Anda menentukan prasyarat menggunakan atribut berikut:

  • AllowNull: Argumen yang tidak dapat diubah ke null mungkin null.
  • DisallowNull: Argumen nullable tidak boleh null.

Pascakondisi: MaybeNull dan NotNull

Misalkan Anda memiliki metode dengan tanda tangan berikut:

public Customer FindCustomer(string lastName, string firstName)

Anda mungkin telah menulis metode seperti ini untuk mengembalikan null saat nama yang dicari tidak ditemukan. null jelas menunjukkan bahwa rekaman tidak ditemukan. Dalam contoh ini, Anda mungkin akan mengubah jenis pengembalian dari Customer ke Customer?. Mendeklarasikan nilai yang dikembalikan sebagai jenis referensi yang dapat diubah ke null menentukan niat API ini dengan jelas:

public Customer? FindCustomer(string lastName, string firstName)

Untuk alasan yang tercakup dalam Generic nullability, teknik tersebut mungkin tidak menghasilkan analisis statis yang cocok dengan API Anda. Anda mungkin memiliki metode generik yang mengikuti pola serupa:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Metode mengembalikan null saat item yang dicari tidak ditemukan. Anda dapat mengklarifikasi bahwa metode mengembalikan null saat item tidak ditemukan dengan menambah anotasi MaybeNull ke pengembalian metode:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Kode sebelumnya memberi tahu pemanggil bahwa nilai yang dikembalikan mungkin benar-benar null. Ini juga menginformasikan compiler bahwa metode dapat mengembalikan null ekspresi meskipun jenisnya tidak dapat diubah ke null. Saat Anda memiliki metode generik yang mengembalikan instans parameter jenisnya, T, Anda dapat mengekspresikan bahwa itu tidak pernah mengembalikan null menggunakan atribut NotNull.

Anda juga dapat menentukan bahwa nilai yang dikembalikan atau argumen tidak null meskipun jenisnya adalah jenis referensi yang dapat diubah ke null. Metode berikut adalah metode pembantu yang melemparkan jika argumen pertamanya adalah null:

public static void ThrowWhenNull(object value, string valueExpression = "")
{
    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

Anda dapat memanggil rutinitas ini sebagai berikut:

public static void LogMessage(string? message)
{
    ThrowWhenNull(message, $"{nameof(message)} must not be null");

    Console.WriteLine(message.Length);
}

Setelah mengaktifkan jenis referensi null, Anda ingin memastikan bahwa kode sebelumnya dikompilasi tanpa peringatan. Saat metode kembali, parameter value dijamin tidak null. Namun, dapat diterima untuk memanggil ThrowWhenNull dengan referensi null. Anda dapat membuat value jenis referensi yang dapat diubah ke null, dan menambahkan pasca-kondisi NotNull ke deklarasi parameter:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
    // other logic elided

Kode sebelumnya mengekspresikan kontrak yang ada dengan jelas: Pemanggil dapat meneruskan variabel dengan nilai null, tetapi argumen dijamin tidak akan pernah null jika metode kembali tanpa melemparkan pengecualian.

Anda menentukan pascakondisi tanpa syarat menggunakan atribut berikut:

  • MaybeNull: Nilai pengembalian yang tidak dapat diubah ke null mungkin null.
  • NotNull: Nilai pengembalian nullable tidak akan pernah null.

Kondisi pasca kondisi: NotNullWhen, MaybeNullWhen, dan NotNullIfNotNull

Anda mungkin terbiasa dengan string metode String.IsNullOrEmpty(String). Metode ini mengembalikan true saat argumen null atau string kosong. Ini adalah bentuk pemeriksaan null: Pemanggil tidak perlu memeriksa null argumen jika metode mengembalikan false. Untuk membuat metode seperti sadar null ini, Anda akan mengatur argumen ke jenis referensi yang dapat diubah ke null, dan menambahkan NotNullWhen atribut :

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

Itu menginformasikan compiler bahwa kode apa pun di mana nilai false yang dikembalikan tidak memerlukan pemeriksaan null. Penambahan atribut menginformasikan analisis statis compiler bahwa IsNullOrEmpty melakukan pemeriksaan null yang dibutuhkan: saat mengembalikan false, argumen bukan null.

string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
    int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

Catatan

Contoh sebelumnya hanya valid di C# 11 dan yang lebih baru. Dimulai dengan C# 11, ekspresi dapat mereferensikan nameof parameter dan mengetik nama parameter saat digunakan dalam atribut yang diterapkan ke metode. Di C# 10 dan yang lebih lama, Anda perlu menggunakan string literal alih-alih nameof ekspresi.

Metode String.IsNullOrEmpty(String) ini akan diannotasi seperti yang ditunjukkan di atas untuk .NET Core 3.0. Anda mungkin memiliki metode serupa di basis kode Anda yang memeriksa status objek untuk nilai null. Compiler tidak akan mengenali metode pemeriksaan null kustom, dan Anda harus menambahkan anotasi sendiri. Saat Anda menambahkan atribut, analisis statis compiler tahu kapan variabel yang diuji telah diperiksa null.

Penggunaan lain untuk atribut ini adalah pola Try*. Pascakondisi untuk argumen ref dan out dikomunikasikan melalui nilai yang dikembalikan. Pertimbangkan metode ini yang ditunjukkan sebelumnya (dalam konteks nonaktif null):

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

Metode sebelumnya mengikuti idiom .NET yang khas: nilai pengembalian menunjukkan apakah message diatur ke nilai yang ditemukan atau, jika tidak ada pesan yang ditemukan, ke nilai default. Jika metode mengembalikan true, nilai message tidak null; jika tidak, metode mengatur message ke null.

Dalam konteks yang diaktifkan nullable, Anda dapat mengomunikasikan idiom tersebut NotNullWhen menggunakan atribut . Saat Anda membuat anotasi parameter untuk jenis referensi yang dapat diubah ke null, buat message menjadi string? dan tambahkan atribut:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message is not null;
}

Dalam contoh sebelumnya, nilai message diketahui tidak null saat TryGetMessage mengembalikan true. Anda harus membuat anotasi metode serupa dalam basis kode Anda dengan cara yang sama: argumen bisa sama dengan null, dan diketahui tidak null saat metode mengembalikan true.

Ada satu atribut akhir yang mungkin juga Anda butuhkan. Terkadang status null dari nilai yang dikembalikan tergantung pada status null dari satu atau beberapa argumen. Metode ini akan mengembalikan nilai non-null setiap kali argumen tertentu bukan null. Untuk membuat anotasi metode ini dengan benar, Anda menggunakan atribut NotNullIfNotNull. Pertimbangkan kode berikut:

string GetTopLevelDomainFromFullUrl(string url)

Jika argumen url tidak null, outputnya bukan null. Setelah referensi nullable diaktifkan, Anda perlu menambahkan lebih banyak anotasi jika API Anda mungkin menerima argumen null. Anda dapat membuat anotasi jenis pengembalian seperti yang ditunjukkan dalam kode berikut:

string? GetTopLevelDomainFromFullUrl(string? url)

Itu juga berfungsi, tetapi akan sering memaksa pemanggil untuk menerapkan pemeriksaan null tambahan. Kontraknya adalah bahwa nilai pengembalian hanya akan null saat argumen url adalah null. Untuk mengekspresikan kontrak tersebut, Anda akan membuat anotasi metode ini seperti yang ditunjukkan dalam kode berikut:

[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)

Contoh sebelumnya menggunakan operator nameof untuk parameter url. Fitur tersebut tersedia di C# 11. Sebelum C# 11, Anda harus mengetikkan nama parameter sebagai string. Nilai yang dikembalikan dan argumen telah diannotasikan dengan ? menunjukkan bahwa salah satunya bisa berupa null. Atribut selanjutnya mengklarifikasi bahwa nilai yang dikembalikan tidak akan null saat argumen url bukan null.

Anda menentukan pascakondisi bersyarat menggunakan atribut ini:

  • MaybeNullWhen: Argumen non-nullable dapat null saat metode mengembalikan nilai bool yang ditentukan.
  • NotNullWhen: Argumen nullable tidak dapat null saat metode mengembalikan nilai bool yang ditentukan.
  • NotNullIfNotNull: Nilai pengembalian bukan null jika argumen untuk parameter yang ditentukan bukan null.

Metode pembantu: MemberNotNull dan MemberNotNullWhen

Atribut ini menentukan niat Anda saat Anda telah memfaktorkan ulang kode umum dari konstruktor ke dalam metode pembantu. Compiler C# menganalisis konstruktor dan penginisialisasi bidang untuk memastikan bahwa semua bidang referensi yang tidak dapat diubah ke null telah diinisialisasi sebelum setiap konstruktor kembali. Namun, compiler C# tidak melacak penetapan bidang melalui semua metode pembantu. Pomengompilasi mengeluarkan peringatan CS8618 saat bidang tidak diinisialisasi langsung di konstruktor, melainkan dalam metode pembantu. Anda menambah MemberNotNullAttribute ke deklarasi metode dan menentukan bidang yang diinisialisasi ke nilai non-null dalam metode. Misalnya, pertimbangkan contoh berikut:

public class Container
{
    private string _uniqueIdentifier; // must be initialized.
    private string? _optionalMessage;

    public Container()
    {
        Helper();
    }

    public Container(string message)
    {
        Helper();
        _optionalMessage = message;
    }

    [MemberNotNull(nameof(_uniqueIdentifier))]
    private void Helper()
    {
        _uniqueIdentifier = DateTime.Now.Ticks.ToString();
    }
}

Anda dapat menentukan beberapa nama bidang sebagai argumen ke konstruktor atribut MemberNotNull.

MemberNotNullWhenAttribute memiliki argumen bool. Anda menggunakan MemberNotNullWhen dalam situasi di mana metode pembantu Anda mengembalikan bool yang menunjukkan apakah metode pembantu Anda menginisialisasi bidang.

Hentikan analisis nullable saat metode yang dipanggil melempar

Beberapa metode, biasanya pembantu pengecualian atau metode utilitas lainnya, selalu keluar dengan melemparkan pengecualian. Atau, pembantu dapat melemparkan pengecualian berdasarkan nilai argumen Boolean.

Dalam kasus pertama, Anda dapat menambahkan atribut DoesNotReturnAttribute ke deklarasi metode. Analisis status null compiler tidak memeriksa kode apa pun dalam metode yang mengikuti panggilan ke metode yang diannotasikan dengan DoesNotReturn. Pertimbangkan metode ini:

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

Compiler tidak mengeluarkan peringatan apa pun setelah panggilan ke FailFast.

Dalam kasus kedua, Anda menambah atribut System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute ke parameter Boolean dari metode. Anda dapat mengubah contoh sebelumnya sebagai berikut:

private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
    if (isNull)
    {
        throw new InvalidOperationException();
    }
}

public void SetFieldState(object? containedField)
{
    FailFastIf(containedField == null);
    // No warning: containedField can't be null here:
    _field = containedField;
}

Saat nilai argumen cocok dengan nilai konstruktor, compiler tidak melakukan analisis DoesNotReturnIfstatus null setelah metode tersebut.

Ringkasan

Menambahkan jenis referensi nullable menyediakan kosakata awal untuk menjelaskan ekspektasi API Anda untuk variabel yang bisa berupa null. Atribut menyediakan kosakata yang lebih kaya untuk menggambarkan status variabel null sebagai prasyarat dan pascakondisi. Atribut ini lebih jelas menggambarkan harapan Anda dan memberikan pengalaman yang lebih baik bagi pengembang menggunakan API Anda.

Saat Anda memperbarui pustaka untuk konteks yang dapat diubah ke null, tambahkan atribut ini untuk memandu pengguna API Anda ke penggunaan yang benar. Atribut ini membantu Anda sepenuhnya menjelaskan status null argumen dan mengembalikan nilai.

  • AllowNull: Bidang, parameter, atau properti yang tidak dapat diubah ke null mungkin null.
  • DisallowNull: Bidang, parameter, atau properti yang dapat diubah ke null tidak boleh null.
  • MaybeNull: Bidang, parameter, properti, atau nilai pengembalian yang tidak dapat diubah ke null mungkin null.
  • NotNull: Parameter, bidang, properti, atau nilai pengembalian yang dapat diubah ke null tidak akan pernah null.
  • MaybeNullWhen: Argumen non-nullable dapat null saat metode mengembalikan nilai bool yang ditentukan.
  • NotNullWhen: Argumen nullable tidak dapat null saat metode mengembalikan nilai bool yang ditentukan.
  • NotNullIfNotNull: Parameter, bidang, properti, atau nilai pengembalian bukan null jika argumen untuk parameter yang ditentukan bukan null.
  • DoesNotReturn: Metode atau properti tidak pernah kembali. Dengan kata lain, itu selalu melemparkan pengecualian.
  • DoesNotReturnIf: Metode atau properti ini tidak pernah kembali jika parameter bool memiliki nilai yang ditentukan.