Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Wskazówka
Dopiero zaczynasz programować oprogramowanie? Zacznij od samouczków Wprowadzenie .
Czy masz doświadczenie w pracy w innym języku? Jeśli masz doświadczenie z typami nullable w Kotlinie, strictNullChecks w TypeScripcie lub typami opcjonalnymi w Swifcie, ten model będzie Ci znajomy. Język C# używa analizy statycznej i diagnostyki ostrzegawczej zamiast oddzielnego typu. Przejrzyj Wyrażanie intencji za pomocą adnotacji i analizę stanu null, a następnie przejdź do samouczka: Wyrażanie założeń projektowych za pomocą typów referencyjnych dopuszczających i niedopuszczających wartości null, aby zastosować tę funkcję.
Typy referencyjne dopuszczające wartość null to grupa funkcji, które minimalizują prawdopodobieństwo, że kod zgłasza błąd System.NullReferenceException. Deklarujesz, które zmienne mają przechowywać null, a które nie, a kompilator ostrzega, gdy te deklaracje nie odpowiadają temu, jak są używane w kodzie. Zachowanie środowiska uruchomieniowego programu nie zmienia się. Typy referencyjne dopuszczane do wartości null są całkowicie funkcją czasu kompilacji.
Trzy bloki konstrukcyjne współpracują ze sobą:
-
Adnotacje zmiennych (
stringastring?) wyrażają, które odwołania mają zezwalać nanull. - Analiza stanów wartości null śledzi, czy wartość wyrażenia nie jest wartością null, czy może być wartością null w każdym miejscu w kodzie.
-
Atrybuty w interfejsach API opisują bardziej zniuansowane kontrakty, takie jak "ten argument może być
null, ale zwracana wartość ma wartość null tylko wtedy, gdy argument ma wartość null".
Kompilator łączy te sygnały w celu utworzenia diagnostyki. Ostrzeżenia dotyczące zmiennej niedopuszczającej wartości null oznaczają, że do zmiennej może zostać przypisana wartość null. Ostrzeżenia dotyczące zmiennej mogącej mieć wartość null oznaczają, że kod może wykonać jej dereferencję bez sprawdzenia, czy nie ma wartości null.
Dereference oznacza użycie wartości, do których odwołuje się zmienna. Na przykład, aby wywołać na nim metodę (variable.Method()), odczytać właściwość (variable.Property) lub uzyskać do niego dostęp za pomocą indeksu (variable[0]). Próba dereferencji zmiennej o wartości null powoduje zgłoszenie wyjątku w czasie wykonywania. Dowolny rodzaj ostrzeżenia oznacza, że zachowanie kodu nie jest zgodne z jego deklarowanym projektem.
Kontekst dopuszczany do wartości null
Projekty utworzone na podstawie ostatnich szablonów .NET ustawiają <Nullable>enable</Nullable> w pliku projektu, więc wskazówki zawarte w tym artykule mają zastosowanie zgodnie z zapisem. Jeśli pracujesz w starszym projekcie, otwórz plik .csproj i sprawdź, czy element <PropertyGroup> zawiera następujący wiersz; dodaj go, jeśli go brakuje:
<Nullable>enable</Nullable>
Aby uzyskać więcej informacji na temat migrowania dużej aplikacji, zobacz artykuł dotyczący strategii migracji dopuszczających wartość null , aby uzyskać więcej ustawień i dyrektyw.
Wyrażanie intencji z adnotacjami
Każda zmienna typu referencyjnego jest domyślnie nieprzyjmująca wartości null. Dodaj ?, aby zadeklarować typ referencyjny dopuszczający wartość null:
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);
}
}
Adnotacja nie zmienia typu środowiska uruchomieniowego.
string i string? są zarówno System.String. Element ? informuje kompilator o intencji projektu. Ta intencja kształtuje ostrzeżenia tworzone przez kompilator:
- Zmienna niepusta ma domyślny stan nullnot-null. Kompilator ostrzega, jeśli przypiszesz wartość, która może mieć wartość
null. - Zmienna dopuszczająca wartość null ma domyślny stan nullmoże mieć wartość null. Kompilator ostrzega, jeśli dereferencjonujesz zmienną bez uprzedniego sprawdzenia jej.
Użyj adnotacji, aby ustawić wymagane i opcjonalne wartości widoczne w systemie typów. Poniższy typ Person deklaruje FirstName i LastName jako nienullowalne — każda osoba musi mieć oba — a MiddleName jako nullowalne, ponieważ nie każdy je ma:
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
}
Adnotacje napędzają implementację elementu ToString. Ponieważ FirstName i LastName nie przyjmują wartości null, przesłonięcie używa ich bezpośrednio w ciągu interpolowanym (składni $"...", która osadza wyrażenia w symbolach zastępczych {}) bez sprawdzania pod kątem wartości null.
MiddleName może mieć wartość null, więc przesłonięcie najpierw sprawdza je względem null i uwzględnia je tylko wtedy, gdy jest obecne. Kompilator rozróżnia te przypadki: kod, który przekazuje wartość mogącą mieć wartość null tam, gdzie oczekiwana jest wartość niedopuszczająca null, generuje ostrzeżenie, a konstruktor, który pozostawia niezainicjowaną składową niedopuszczającą null, również generuje ostrzeżenie.
Analiza stanu null
Kompilator śledzi stan null każdego wyrażenia. Stan jest jedną z dwóch wartości:
-
not-null: wyrażenie jest znane jako nie
null. -
może mieć wartość null: wyrażenie może mieć wartość
null.
Stan null zmiennej lokalnej jest aktualizowany w miarę analizowania kodu przez kompilator. Dwie rzeczy to zmieniają: przypisania i sprawdzenia wartości null. Po przypisaniu stan null zmiennej jest zgodny z wyrażeniem po prawej stronie. Jeśli wyrażenie ma wartość null lub może mieć wartość null, zmienna jest traktowana jako mogąca mieć wartość null. Jeśli wyrażenie jest literałem różnym od null, zmienna nie będzie mieć wartości null. Po sprawdzeniu wartości null stan null zmiennej odzwierciedla gałąź, która jest zajęta.
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);
}
W poprzednim przykładzie pierwsza operacja dereferencji generuje ostrzeżenie, ponieważ message ma wartość maybe-null. Po przypisaniu literału różnego od null kompilator wie, że messagenie jest nullem, więc druga dereferencja jest bezpieczna.
Analiza stanu null działa w przypadku sprawdzeń if, dopasowywania wzorców (wyrażeń, takich jak is null lub is { }, które testują strukturę wartości) oraz przepływu sterowania obejmującego pętle lub wcześniejsze zwroty:
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;
}
}
Analiza nie śledzi treści metod. Jeśli potrzebujesz sposobu na przekazanie wywołującym informacji o stanie null, użyj w jej sygnaturze atrybutów analizy nullowalności.
Zastąpij ostrzeżenia za pomocą polecenia !
Czasami wiesz więcej niż kompilator. Operator tłumienia wartości null! deklaruje, że wyrażenie nie jest nullem, nawet jeśli analiza wskazuje inaczej:
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,
};
Używaj ! oszczędnie. Każde wystąpienie to miejsce, w którym kompilator nie może cię już chronić. Preferuj dodawanie sprawdzania wartości null, restrukturyzację kodu lub dodawanie adnotacji do odpowiedniego interfejsu API, aby kompilator osiągnął właściwy wniosek samodzielnie.
Atrybuty opisujące kontrakty interfejsu API
Adnotacje w parametrze lub zwracanym typie nie zawsze są wystarczająco wyraziste. Metoda może akceptować argument, który może mieć wartość null, ale gwarantuje wynik niebędący wartością null. Metoda testowa może zwracać tylko true wtedy, gdy argument nie ma wartości null. Użyj atrybutów analizy dopuszczanej do wartości null , aby przekazać te kontrakty:
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 informuje kompilator, że jeśli IsPresent zwraca true, argument nie ma wartości null. Wewnątrz bloku if kompilator traktuje value jako nienullowalne, bez konieczności użycia operatora wybaczania wartości null. Począwszy od platformy .NET 5 wszystkie interfejsy API środowiska uruchomieniowego .NET są opatrzone adnotacjami, więc analiza przynosi korzyści każdemu kodowi, który je wywołuje.
Znane pułapki
Dwa wzorce mogą sprawić, że referencja, która nie może mieć wartości null, będzie miała wartość null bez ostrzeżenia. Oba wzorce są ograniczeniami analizy statycznej, a nie usterek w kodzie.
Domyślne struktury
Możesz utworzyć strukturę z polami odwołania bez wartości null przy użyciu polecenia default lub new(). Takie podejście pozostawia niezainicjowane pola struktury:
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);
}
Pola zawierają null w czasie wykonywania, ale kompilator nie ostrzega. Jeśli musisz użyć struktury, preferuj wymagane składowe, czyli składowe, które wywołujący musi zainicjować za pomocą inicjalizatora obiektu lub przez wywołanie konstruktora z parametrami.
Tablice odwołań i struktur
Nowa tablica niepustego typu referencyjnego zawiera same elementy null, dopóki nie przypiszesz każdego z nich:
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);
}
Ta sama pułapka ma zastosowanie do tablic struktur: każdy element zaczyna się od wartości domyślnej struktury, więc pola odwołania dla każdego elementu zaczynają się od null.
Inicjuj elementy tablicy w ramach tworzenia tablicy.
Wyrażenia kolekcji (składnia literału [1, 2, 3]) i new z typem docelowym (pisanie new(), gdy kompilator może wywnioskować typ) pozwalają zwięźle zapisać pełną inicjalizację.
Treści powiązane
- Samouczek: wyrażanie intencji projektowej przy użyciu typów referencyjnych dopuszczających wartość null i niedopuszczających wartości null
- Usuwanie ostrzeżeń dotyczących wartości null
- Strategie migracji dopuszczane do wartości null
- Atrybuty statycznej analizy dopuszczające wartość null
- Rozwiązywanie ostrzeżeń dotyczących typów dopuszczających wartość null (informacje o kompilatorze)