Nullaértékű hivatkozási típusok

Jótanács

Új szoftverfejlesztés? Kezdje az első lépéseket ismertető oktatóanyagokkal.

Tapasztalt egy másik nyelven? Ha Kotlin null értékű típusaival, TypeScript strictNullChecksvagy Swift választható típusaival dolgozott, a modell ismerős. A C# külön típus helyett statikus elemzést és figyelmeztetési diagnosztikát használ. Fussa át a A szándék kifejezése annotációkkal és a Nullállapot-elemzés című témaköröket, majd ugorjon a Oktatóanyag: A tervezési szándék kifejezése nullázható és nem nullázható referenciatípusokkal részhez, hogy alkalmazza ezt a funkciót.

A null értékű hivatkozástípusok olyan funkciók egy csoportja, amelyek minimalizálják a kód által eredményezett esélyeket System.NullReferenceException. Megadja, hogy mely változók tárolhatnak null értéket, és melyek nem, a fordító pedig figyelmeztet, ha ezek a deklarációk nem egyeznek azzal, ahogyan a kód használja őket. A program futásideje nem változik. A null értékűre engedélyezett referenciatípusok kizárólag fordítási időben érvényes funkciók.

Három építőelem működik együtt:

  • Változóannotációk (string vs. string?) azt jelzik, hogy mely hivatkozások teszik lehetővé a(z) null-t.
  • A nullállapot-elemzés nyomon követi, hogy egy kifejezés értéke nem null vagytalán null a kód minden egyes pontján.
  • Az API-k attribútumai részletesebb szerződéseket írnak le, például "ez az argumentum lehetnull, de a visszatérési érték csak akkor null, ha az argumentum null".

A fordító ezeket a jeleket kombinálva diagnosztikai üzeneteket állít elő. A nem null értékű változókra vonatkozó figyelmeztetések azt jelentik, hogy a változó kaphat null. A null értékű változókra vonatkozó figyelmeztetések azt jelentik, hogy a kód null ellenőrzés nélkül elhalaszthatja azt. A dereferálás a változó által hivatkozott érték használatát jelenti. Például egy metódus meghívásához (variable.Method()), olvasson be egy tulajdonságot (variable.Property), vagy indexelje bele (variable[0]). Egy null értékű változó dereferálása futásidőben kivételt vált ki. Bármelyik figyelmeztetés azt jelenti, hogy a kód viselkedése nem egyezik a megadott kialakításával.

Null értékkel rendelkezhető környezet

A legújabb .NET-sablonokból létrehozott projektek beállítják a <Nullable>enable</Nullable> értéket a projektfájlban, így az ebben a cikkben szereplő útmutatás változtatás nélkül alkalmazható. Ha egy régebbi projektben dolgozik, nyissa meg a .csproj következő sort, és ellenőrizze, hogy az <PropertyGroup> tartalmazza-e a következő sort; adja hozzá, ha hiányzik:

<Nullable>enable</Nullable>

A nagy méretű alkalmazások migrálásával kapcsolatos további információkért tekintse meg a null értékű migrálási stratégiákról szóló cikket, amely további beállításokat és irányelveket tartalmaz.

Szándék kifejezése széljegyzetekkel

Alapértelmezés szerint minden referenciatípus-változó nem null értékű . Fűzze hozzá a(z) ? elemet egy null értéket is felvevő referenciatípus deklarálásához:

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

A széljegyzet nem változtatja meg a futtatókörnyezet típusát. string és string? mindkettő System.String. A ? tájékoztatja a fordítót a tervezési szándékáról. Ez a cél határozza meg a fordító által kiadott figyelmeztetéseket:

  • A nem null értékű változók alapértelmezett null-állapotanem null. A fordító figyelmeztet, ha olyan értéket rendel hozzá, amely lehet null.
  • A null értékű változók alapértelmezett null-állapotatalán-null. A fordító figyelmeztet, ha a változót anélkül dereferálja, hogy előbb ellenőrizné.

A jegyzettel láthatóvá teheti a kötelező és nem kötelező értékeket a típusrendszerben. A következő Person típus a(z) FirstName és LastName elemet nem nullázhatóként deklarálja — mindenkinek rendelkeznie kell mindkettővel —, a(z) MiddleName elemet pedig nullázhatóként, mivel nem mindenkinek van ilyen.

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
}

Az annotációk vezérlik a(z) ToString megvalósítását. Mivel a FirstName és a LastName nem vehet fel null értéket, a felülírás közvetlenül egy interpolált karakterláncban használja őket, nullérték-ellenőrzés nélkül (azaz a $"..." szintaxissal, amely kifejezéseket ágyaz be a {} helyőrzőkbe). A MiddleName lehet null, ezért a felülírás először a null alapján ellenőrzi, és csak akkor foglalja bele, ha jelen van. A fordító kikényszeríti ezt a különbséget: az a kód, amely null értékű is lehető értéket ad át ott, ahol nem nullázható érték van elvárva, figyelmeztetést vált ki, és az a konstruktor is figyelmeztetést vált ki, amely egy nem nullázható tagot inicializálatlanul hagy.

Nullállapot-elemzés

A fordító minden kifejezés null állapotát követi nyomon. Az állapot két érték egyike:

  • not-null: ismert, hogy a kifejezés nem null.
  • maybe-null: a kifejezés lehet null.

A helyi változó null állapota a kód elemzésekor frissül. Két dolog módosítja: hozzárendelések és nullellenőrzések. A hozzárendelés után a változó null állapota megegyezik a jobb oldali kifejezéssel. Ha a kifejezés null vagy null értékű, akkor a változó talán-null értékű lesz. Ha a kifejezés nem null literál, a változó nem null értékű lesz. A null értékű ellenőrzés után a változó nullállapota a levett ágat tükrözi.

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

Az előző példában az első hareferencia figyelmeztetést eredményez, mert messagelehet, hogy null. A nem null értékű literálhoz való hozzárendelés után a fordító tudja, hogy messagenem null, így a második hareferencia biztonságos.

A nullállapot elemzése működik a if ellenőrzéseken, a mintailesztésen (olyan kifejezéseken, mint a is null vagy a is { }, amelyek egy érték szerkezetét vizsgálják), valamint az olyan vezérlési folyamatokon át is, amelyek ciklusba lépnek vagy korán visszatérnek:

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

Az elemzés nem terjed ki a metódusok törzsére. Ha egy metódusra van szüksége a null állapot hívókkal való kommunikálásához, használjon null értékű elemzési attribútumokat az aláírásán.

A figyelmeztetések felülbírálása a következővel: !

Néha többet tud, mint a fordító. A null-megbocsátó operátor! azt állítja, hogy egy kifejezés nem null, még akkor is, ha az elemzés másként szól:

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

A ! elemet csak mértékkel használja. Minden előfordulás olyan hely, ahol a fordító már nem tudja megvédeni Önt. Előnyben részesítse a null értékű ellenőrzés hozzáadását, a kód szerkezetátalakítását vagy a megfelelő API megjegyzését, hogy a fordító önmagában is megfelelő következtetést érjen el.

API-szerződéseket leíró attribútumok

Egy paraméter vagy visszatérési típus megjegyzései nem mindig elég kifejezőek. A metódusok elfogadhatnak egy lehetséges null argumentumot, de nem null eredményt garantálnak. A tesztmetódusok csak akkor térhetnek vissza true , ha argumentuma nem null. A nullázhatósági elemzési attribútumokkal adhatja meg ezeket a szerződéseket:

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

A NotNullWhenAttribute jelzi a fordítónak, hogy amikor a(z) IsPresenttrue értéket ad vissza, az argumentum nem null. A(z) if blokkon belül a fordító a value elemet nem null értékűként kezeli, anélkül hogy nullérték-megengedő operátorra lenne szükség. A .NET 5-től kezdve a .NET-futtatókörnyezet összes API-ja annotációkkal van ellátva, így az elemzés minden olyan kód számára előnyös, amely meghívja őket.

Ismert buktatók

Két minta esetén előfordulhat, hogy egy nem null értékű referencia figyelmeztetés nélkül null értéket tartalmaz. Mindkét minta a statikus elemzés korlátai, nem pedig a kód hibái.

Alapértelmezett szerkezetek

Létrehozhat egy nem null értékű referenciamezőkkel rendelkező struktúrát a(z) default vagy a(z) new() használatával. Ez a megközelítés inicializálatlanul hagyja a struktúra mezőit:

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

A mezők futáskor null értéket tartalmaznak, de a fordító nem figyelmeztet. Ha szerkezetet kell használnia, előnyben kell részesítenie a szükséges tagokat, amelyek tagok, amelyeket a hívónak inicializálnia kell egy objektum-inicializálón keresztül, vagy egy paraméteres konstruktort, amelyet a hívóknak meg kell hívniuk.

Hivatkozások és szerkezetek tömbjei

A nem null értékű hivatkozástípus új tömbje az összes null elemet tartalmazza, amíg mindegyikhez nem rendel hozzá elemeket:

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

Ugyanez a buktató a szerkezetek tömbjeire is vonatkozik: minden elem a szerkezet alapértelmezett értékeként kezdődik, így minden elem nem null értékű referenciamezői a következőképpen kezdődnek null: .

A tömbelemek inicializálása a tömb létrehozása során. Gyűjteménykifejezések (a [1, 2, 3] literálszintaxis) és a céltípusos new (new() írása, amikor a fordító ki tudja következtetni a típust) tömörré teszi a teljes inicializálást.