Nullable referentietypen

Tip

Nieuw bij het ontwikkelen van software? Beginnen met de Aan de slag-zelfstudies.

Ervaren in een andere taal? Als u hebt gewerkt met de nullable typen van Kotlin, TypeScript's strictNullChecks, of de optionals van Swift, dan komt dit model u waarschijnlijk bekend voor. C# maakt gebruik van statische analyse- en waarschuwingsdiagnose in plaats van een afzonderlijk type. Lees snel Intentie uitdrukken met annotaties en Analyse van de null-status, en ga vervolgens naar de Zelfstudie: Je ontwerpintentie uitdrukken met nullable- en niet-nullbare referentietypen om deze functie toe te passen.

Nullbare verwijzingstypen zijn een verzameling functies die de kans verkleinen dat uw code System.NullReferenceException genereert. U declareert welke variabelen zijn bedoeld om vast te houden null en welke niet, en de compiler waarschuwt wanneer deze declaraties niet overeenkomen met de manier waarop uw code ze gebruikt. Het runtimegedrag van uw programma is ongewijzigd. Nullable-referentietypen zijn volledig een functionaliteit tijdens het compileren.

Drie bouwstenen werken samen:

  • Variabele-annotaties (string vs. string?) geven aan welke referenties bedoeld zijn om null toe te staan.
  • Bij analyse van null-status wordt bijgehouden of de waarde van een expressie niet null of misschien null is op elk punt in uw code.
  • Kenmerken van API's beschrijven meer genuanceerde contracten, zoals 'dit argument kan zijn null, maar de retourwaarde is alleen null als het argument null is.'

De compiler combineert deze signalen om diagnostische gegevens te produceren. Waarschuwingen bij een niet-nullbare variabele betekenen dat de variabele mogelijk null toegewezen krijgt. Waarschuwingen voor een nullbare variabele betekenen dat de code deze mogelijk kan dereferencen zonder eerst een null-controle uit te voeren. Dereference betekent dat je de waarde gebruikt waarnaar de variabele verwijst. Als u bijvoorbeeld een methode erop wilt aanroepen (variable.Method()), een eigenschap wilt lezen (variable.Property), of erop wilt indexeren (variable[0]). Het volgen van de referentie van een variabele met de waarde null veroorzaakt tijdens runtime een uitzondering. Elk type waarschuwing betekent dat het gedrag van de code niet overeenkomt met het aangegeven ontwerp.

Null-context

Projecten die zijn gemaakt op basis van recente .NET-sjablonen die zijn ingesteld <Nullable>enable</Nullable> in het projectbestand, zodat de richtlijnen in dit artikel van toepassing zijn als geschreven. Als u in een ouder project werkt, opent u het .csproj project en controleert u of deze <PropertyGroup> de volgende regel bevat. Voeg deze toe als deze ontbreekt:

<Nullable>enable</Nullable>

Zie voor meer informatie over het migreren van een grote toepassing het artikel over nullable migratiestrategieën voor meer instellingen en richtlijnen.

Druk intentie uit met annotaties

Elke variabele van het verwijzingstype is standaard niet nullbaar . Voeg ? toe om een nullable referentietype te declareren:

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);
    }
}

De aantekening wijzigt het runtimetype niet. string en string? zijn beide System.String. De ? laat de compiler weten wat uw ontwerpbedoeling is. Deze intentie vormen de waarschuwingen die de compiler produceert:

  • Een niet-nullbare variabele heeft een standaard-nullstatus van not-null. De compiler waarschuwt als u een waarde toewijst die mogelijk is null.
  • Een variabele die null kan zijn, heeft standaard de nullstatusmogelijk null. De compiler waarschuwt als u de variabele dereferereert zonder deze eerst te controleren.

Gebruik de aantekening om vereiste en optionele waarden zichtbaar te maken in het typesysteem. Het volgende Person-type verklaart FirstName en LastName als niet-nullbaar—iedere persoon moet beide hebben—en MiddleName als nullbaar, omdat niet iedereen er één heeft:

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
}

De aantekeningen stimuleren de implementatie van ToString. Omdat FirstName en LastName niet-nullbaar zijn, gebruikt de override deze rechtstreeks in een geïnterpoleerde tekenreeks (de syntaxis $"..." waarmee expressies in {} plaatshouders worden ingesloten), zonder null-controle. MiddleName kan null zijn, dus de overschrijving controleert deze eerst op null en neemt deze alleen op als die aanwezig is. De compiler dwingt het verschil af: code die een misschien-null-waarde doorgeeft waarbij een niet-nullbare waarde wordt verwacht, produceert een waarschuwing en een constructor die een niet-nullbaar lid niet-geïnitialiseerd laat, produceert ook een waarschuwing.

Analyse van de null-toestand

De compiler houdt de null-status van elke expressie bij. De status is een van de twee waarden:

  • not-null: van de expressie is bekend dat deze niet null is.
  • misschien-null: de expressie kan zijn null.

De null-status van een lokale variabele wordt bijgewerkt wanneer de compiler uw code analyseert. Twee dingen veranderen het: toewijzingen en null-controles. Na een toewijzing komt de null-status van de variabele overeen met de expressie aan de rechterkant. Als de expressie null is of de waarde null kan hebben, wordt de variabele mogelijk null. Als de expressie een niet-null-letterlijke waarde is, wordt de variabele niet-null. Na een controle op null komt de null-status van de variabele overeen met de vertakking die wordt gevolgd.

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);
}

In het voorgaande voorbeeld produceert de eerste dereferentie een waarschuwing omdat messagemogelijk null is. Na de toewijzing van een niet-nul-literal weet de compiler dat messageniet null is, dus de tweede dereferentie is veilig.

Null-statusanalyse werkt voor if controles, patroonkoppeling (expressies zoals is null of is { } die de vorm van een waarde testen) en controlestroom die wordt herhaald of vroeg wordt geretourneerd:

 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;
     }
}

De analyse dringt niet door tot in de methode-implementaties. Als u een manier nodig hebt om de nullstatus aan de aanroepers te communiceren, gebruikt u kenmerken voor nullbaarheidsanalyse in de signatuur.

De waarschuwingen negeren met !

Soms weet u meer dan de compiler. De operator ! geeft aan dat een expressie niet null is, zelfs als de analyse anders zegt:

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,
};

Gebruik ! spaarzaam. Elk voorkomen is een punt waarop de compiler je niet langer kan beschermen. Voeg liever een null-controle toe, herstructureren van de code of aantekeningen toevoegen aan de relevante API, zodat de compiler zelf de juiste conclusie bereikt.

Kenmerken die API-contracten beschrijven

Aantekeningen op een parameter of retourtype zijn niet altijd expressief genoeg. Een methode kan een mogelijk null-argument accepteren, maar een niet-null-resultaat garanderen. Een testmethode retourneert mogelijk true alleen als het argument niet null is. Gebruik de nullable-analysekenmerken om deze contracten over te brengen:

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";

De NotNullWhenAttribute vertelt de compiler dat wanneer IsPresenttrue retourneert, het argument niet-null is. Binnen het if-blok behandelt de compiler value als niet-null, zonder dat een null-forgiving-operator nodig is. Vanaf .NET 5 worden alle .NET runtime-API's geannoteerd, dus de analyse profiteert van code die ze aanroept.

Bekende valkuilen

Twee patronen kunnen ertoe leiden dat een niet-nullbare referentie null bevat zonder waarschuwing. Beide patronen zijn beperkingen van de statische analyse, niet fouten in uw code.

Standaardstructuren

U kunt een struct maken met niet-nullbare verwijzingsvelden met behulp van default of new(). Bij deze benadering blijven de velden van de struct niet geïnitialiseerd:

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);
}

De velden bevatten tijdens runtime null, maar de compiler waarschuwt niet. Als u een struct moet gebruiken, geeft u de voorkeur aan vereiste leden. Dit zijn leden die de aanroeper moet initialiseren via een object-initializer of een geparameteriseerde constructor die aanroepers moeten aanroepen.

Matrices van verwijzingen en structs

Een nieuwe array van een niet-nullbare verwijzingstype bevat uitsluitend null-elementen totdat u aan elk element een waarde toewijst:

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);
}

Dezelfde valkuil is van toepassing op matrices van structs: elk element begint als de standaardwaarde van de struct, zodat de niet-nulle referentievelden van elk element beginnen als null.

Initialiseer matrixelementen als onderdeel van het maken van de matrix. Verzamelingsexpressies (de [1, 2, 3] letterlijke syntaxis) en het doeltype new (schrijven new() wanneer de compiler het type kan afleiden) maken volledige initialisatie beknopt.