Referenstyper som kan ogiltigförklaras

Tip

Är du nybörjare på att utveckla programvara? Börja med Kom igång-självstudierna.

Har du erfarenhet av ett annat språk? Om du har arbetat med Kotlins nullbara typer, TypeScripts strictNullChecks eller Swifts optionals, är modellen välbekant. C# använder statisk analys och varningsdiagnostik i stället för en separat typ. Läs snabbt igenom Uttryck avsikten med annoteringar och Nulltillståndsanalys och gå sedan vidare till Självstudie: Uttryck designavsikten med nullbara och icke-nullbara referenstyper för att använda funktionen.

Nullbara referenstyper är en uppsättning funktioner som minimerar risken för att din kod genererar System.NullReferenceException. Du deklarerar vilka variabler som är avsedda att innehålla null och vilka som inte är det, och kompilatorn varnar när dessa deklarationer inte matchar hur koden använder dem. Körningsbeteendet för ditt program är oförändrat. Nullbara referenstyper är enbart en funktion vid kompilering.

Tre byggstenar fungerar tillsammans:

  • Variabelkommentarer (string jämfört med string?) uttrycker vilka referenser som är avsedda att tillåta null.
  • Null-tillståndsanalys spårar om värdet för ett uttryck inte är null eller kanske null vid varje punkt i koden.
  • Attribut på API:er beskriver mer nyanserade kontrakt, till exempel "det här argumentet kan vara null, men returvärdet är endast null när argumentet är null".

Kompilatorn kombinerar dessa signaler för att producera diagnostik. Varningar om en icke-nullbar variabel betyder att variabeln kan tilldelas null. Varningar för en nullbar variabel innebär att koden kan avreferera den utan en null-kontroll. Dereference innebär att använda värdet som variabeln refererar till. Om du till exempel vill anropa en metod på den (variable.Method()), läsa en egenskap (variable.Property) eller indexera den (variable[0]). Att avreferera en variabel som har värdet null utlöser ett undantag vid körning. Båda typerna av varningar innebär att kodens beteende inte matchar den angivna designen.

Nullbar kontext

Projekt som skapats från de senaste .NET mallarna anger <Nullable>enable</Nullable> i projektfilen, så vägledningen i den här artikeln gäller som skrivet. Om du arbetar i ett äldre projekt öppnar .csproj du och kontrollerar att <PropertyGroup> innehåller följande rad. Lägg till den om den saknas:

<Nullable>enable</Nullable>

Mer information om hur du migrerar ett stort program finns i artikeln om nullbara migreringsstrategier för fler inställningar och direktiv.

Uttryck avsikt med anteckningar

Varje referenstypvariabel är inte nullbar som standard. Lägg till för att deklarera en ?:

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

Anteckningen ändrar inte körningstypen. string och string? är båda System.String. Informerar ? kompilatorn om din design avsikt. Avsikten formar varningarna som kompilatorn skapar:

  • En variabel som inte kan ha värdet null har som standard null-tillståndetinte-null. Kompilatorn varnar om du tilldelar ett värde som kan vara null.
  • En nullbar variabel har null-standardtillståndetkanske-null. Kompilatorn varnar om du avreferera variabeln utan att först kontrollera den.

Använd kommentaren för att göra obligatoriska och valfria värden synliga i typsystemet. Följande Person typ deklarerar FirstName och LastName som icke-nullbar – varje person måste ha både och MiddleName som nullbar, eftersom inte alla har någon:

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
}

Anteckningarna driver implementeringen av ToString. Eftersom FirstName och LastName är icke-nullbara använder override-metoden dem direkt i en interpolerad sträng (syntaxen $"..." som infogar uttryck i {}-platshållare) utan någon null-kontroll. MiddleName är nullbar, så override-metoden kontrollerar den först mot null och inkluderar den endast om den finns. Kompilatorn framtvingar skillnaden: kod som skickar ett kanske null-värde där ett icke-nullbart värde förväntas ger en varning, och en konstruktor som lämnar en icke-nullbar medlem onitialiserad skapar också en varning.

Null-tillståndsanalys

Kompilatorn spårar null-tillståndet för varje uttryck. Tillståndet är ett av två värden:

  • not-null: uttrycket är känt för att inte vara null.
  • maybe-null: uttrycket kan vara null.

En lokal variabels null-tillstånd uppdateras när kompilatorn analyserar koden. Två saker ändrar det: tilldelningar och null-kontroller. Efter en tilldelning matchar variabelns null-tillstånd uttrycket till höger. Om uttrycket är null eller kan vara null blir variabeln möjligen null. Om uttrycket är en literal som inte är null blir variabeln inte null. Efter en null-kontroll återspeglar variabelns null-tillstånd den gren som tas.

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

I det föregående exemplet ger den första avrefereringen upphov till en varning eftersom messagekan vara null. Efter tilldelningen till en literal som inte är null vet message kompilatorn att den inte är null, så den andra dereferencen är säker.

Null-tillståndsanalys fungerar mellan if kontroller, mönstermatchning (uttryck som is null eller is { } som testar formen på ett värde) och kontrollflöde som loopar eller returnerar tidigt:

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

Analysen spårar inte in i metodernas implementeringar. Om du behöver att en metod kommunicerar nullstatus till sina anropare, använder du attribut för nullbarhetsanalys i dess signatur.

Åsidosätt varningarna med !

Ibland vet du mer än kompilatorn. Operatorn ! deklarerar att ett uttryck inte är null, även om analysen säger något annat:

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

Använd ! sparsamt. Varje förekomst är en plats som kompilatorn inte längre kan skydda dig på. Föredrar att lägga till en null-kontroll, omstrukturera koden eller kommentera det relevanta API:et så att kompilatorn når rätt slutsats på egen hand.

Attribut som beskriver API-kontrakt

Anteckningar för en parameter eller returtyp är inte alltid tillräckligt uttrycksfulla. En metod kan acceptera ett eventuellt null-argument men garantera ett resultat som inte är null. En testmetod kan bara returneras true när argumentet inte är null. Använd attributen för nullbar analys för att förmedla följande kontrakt:

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 Anger för kompilatorn att när IsPresent returnerar trueär argumentet inte null. Inuti if-blocket behandlar kompilatorn value som icke-null utan att någon null-undertryckningsoperator krävs. Från och med .NET 5 kommenteras alla .NET runtime-API:er, så analysen gynnar all kod som anropar dem.

Kända fallgropar

Två mönster kan leda till att en icke-nullbar referens har värdet null utan att ge någon varning. Båda mönstren är begränsningar i den statiska analysen, inte buggar i koden.

Standardstrukturer

Du kan skapa en struct med icke-nullbara referensfält genom att använda default eller new(). Den här metoden lämnar struct-fälten onitialiserade:

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

Fälten innehåller null under körning, men kompilatorn varnar inte. Om du måste använda en struct bör du föredra obligatoriska medlemmar, det vill säga medlemmar som anroparen måste initiera via en objektinitierare, eller en konstruktor med parametrar som anroparen måste anropa.

Arrayer av referenser och strukturer

En ny matris med en icke-nullbar referenstyp innehåller alla null element tills du tilldelar var och en:

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

Samma fallgropar gäller för matriser med structs: varje element börjar som structens standardvärde, så att varje elements referensfält som inte kan nollföras börjar som null.

Initiera matriselement som en del av att skapa matrisen. Samlingsuttryck (literalsyntaxen [1, 2, 3]) och måltypsbestämda new (att skriva new() när kompilatorn kan härleda typen) gör fullständig initiering kortfattad.