Atrybuty analizy statycznej stanu null interpretowane przez kompilator języka C#
W kontekście z możliwością dopuszczania wartości null kompilator wykonuje statyczną analizę kodu w celu określenia stanu null wszystkich zmiennych typu odwołania:
- not-null: Analiza statyczna określa, że zmienna ma wartość inną niż null.
- może-null: analiza statyczna nie może określić, że zmienna ma przypisaną wartość inną niż null.
Te stany umożliwiają kompilatorowi podanie ostrzeżeń, gdy można wyłuszczyć wartość null, zgłaszając System.NullReferenceExceptionwartość . Te atrybuty zapewniają kompilatorowi informacje semantyczne o stanie null argumentów, zwracanych wartościach i elementach członkowskich obiektów na podstawie stanu argumentów i zwracanych wartości. Kompilator udostępnia dokładniejsze ostrzeżenia, gdy interfejsy API zostały prawidłowo oznaczone tą semantyczną informacją.
Ten artykuł zawiera krótki opis każdego atrybutu typu odwołania dopuszczanego do wartości null i sposobu ich używania.
Zacznijmy od przykładu. Wyobraź sobie, że twoja biblioteka ma następujący interfejs API, aby pobrać ciąg zasobu. Ta metoda została pierwotnie skompilowana w kontekście bezpłciowym:
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
Powyższy przykład jest zgodny ze znanym Try*
wzorcem na platformie .NET. Istnieją dwa parametry referencyjne dla tego interfejsu message
API: i key
. Ten interfejs API ma następujące reguły dotyczące stanu null tych parametrów:
- Osoby wywołujące nie powinny przekazywać
null
argumentu dlakey
elementu . - Obiekt wywołujący może przekazać zmienną, której wartość jest
null
argumentem .message
TryGetMessage
Jeśli metoda zwracatrue
wartość , wartość parametrumessage
nie ma wartości null. Jeśli zwracana wartość tofalse
, wartość parametrumessage
ma wartość null.
Reguła dla key
elementu może być wyrażona zwięźle: key
powinna być typem referencyjnym, który nie może mieć wartości null. Parametr message
jest bardziej złożony. Umożliwia zmienną, która jest null
argumentem, ale gwarantuje powodzenie, że out
argument nie null
jest . W przypadku tych scenariuszy potrzebujesz bogatszego słownictwa, aby opisać oczekiwania. Atrybut NotNullWhen
opisany poniżej opisuje stan null argumentu używanego dla parametru message
.
Uwaga
Dodanie tych atrybutów zapewnia kompilatorowi więcej informacji o regułach interfejsu API. Podczas wywoływania kodu jest kompilowany w kontekście obsługującym wartość null, kompilator ostrzega osoby wywołujące, gdy naruszają te reguły. Te atrybuty nie umożliwiają większej liczby kontroli implementacji.
Atrybut | Kategoria | Znaczenie |
---|---|---|
AllowNull | Warunek wstępny | Parametr bez wartości null, pole lub właściwość może mieć wartość null. |
Nie zezwalajNull | Warunek wstępny | Parametr, pole lub właściwość dopuszczana do wartości null nigdy nie powinna mieć wartości null. |
MożeNull | Warunek końcowy | Parametr bez wartości null, pole, właściwość lub wartość zwracana może mieć wartość null. |
NotNull | Warunek końcowy | Parametr dopuszczający wartość null, pole, właściwość lub wartość zwracana nigdy nie będzie mieć wartości null. |
MożeNullWhen | Warunkowe pośmiertne | Argument bez wartości null może mieć wartość null, gdy metoda zwraca określoną bool wartość. |
NotNullWhen | Warunkowe pośmiertne | Argument dopuszczany do wartości null nie będzie mieć wartości null, gdy metoda zwraca określoną bool wartość. |
NotNullIfNotNull | Warunkowe pośmiertne | Wartość zwracana, właściwość lub argument nie ma wartości null, jeśli argument dla określonego parametru nie ma wartości null. |
MemberNotNull | Metody i metody pomocnicze właściwości | Wymieniony element członkowski nie będzie mieć wartości null, gdy metoda zwróci wartość . |
MemberNotNullWhen | Metody i metody pomocnicze właściwości | Wymieniony element członkowski nie będzie mieć wartości null, gdy metoda zwróci określoną bool wartość. |
DoesNotReturn | Kod niemożliwy do osiągnięcia | Metoda lub właściwość nigdy nie zwraca. Innymi słowy, zawsze zgłasza wyjątek. |
DoesNotReturnIf | Kod niemożliwy do osiągnięcia | Ta metoda lub właściwość nigdy nie zwraca, jeśli skojarzony bool parametr ma określoną wartość. |
Powyższe opisy to krótkie odwołanie do tego, co robi każdy atrybut. W poniższych sekcjach opisano zachowanie i znaczenie tych atrybutów dokładniej.
Warunki wstępne: AllowNull
i DisallowNull
Rozważ właściwość odczytu/zapisu, która nigdy nie zwraca null
, ponieważ ma rozsądną wartość domyślną. Wywołujące przechodzą null
do zestawu dostępu podczas ustawiania jej na wartość domyślną. Rozważmy na przykład system obsługi komunikatów, który prosi o nazwę ekranu w pokoju rozmów. Jeśli żadna z nich nie zostanie podana, system generuje losową nazwę:
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;
Po skompilowaniu poprzedniego kodu w kontekście bezpłciowym wszystko jest w porządku. Po włączeniu typów ScreenName
odwołań dopuszczanych wartości null właściwość staje się odwołaniem nienależącym do wartości null. Jest to poprawne dla get
metody dostępu: nigdy nie zwraca wartości null
. Osoby wywołujące nie muszą sprawdzać zwróconej właściwości dla null
elementu . Ale teraz ustawienie właściwości w celu null
wygenerowania ostrzeżenia. Aby obsługiwać ten typ kodu, należy dodać System.Diagnostics.CodeAnalysis.AllowNullAttribute atrybut do właściwości, jak pokazano w poniższym kodzie:
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
Może być konieczne dodanie using
dyrektywy, System.Diagnostics.CodeAnalysis aby użyć tej i innych atrybutów omówionych w tym artykule. Atrybut jest stosowany do właściwości, a nie set
metody dostępu. Atrybut AllowNull
określa warunki wstępne i ma zastosowanie tylko do argumentów. Akcesorium get
ma wartość zwracaną, ale nie ma parametrów. W związku z AllowNull
tym atrybut ma zastosowanie tylko do set
metody dostępu.
W poprzednim przykładzie pokazano, czego należy szukać podczas dodawania atrybutu do argumentu AllowNull
:
- Ogólny kontrakt dla tej zmiennej polega na tym, że nie powinien mieć
null
wartości , więc chcesz, aby typ odwołania nie dopuszczał wartości null. - Istnieją scenariusze przekazywania przez obiekt
null
wywołujący jako argument, choć nie są one najbardziej typowym użyciem.
Najczęściej ten atrybut jest potrzebny dla właściwości lub in
, out
i ref
argumentów. Atrybut AllowNull
jest najlepszym wyborem, gdy zmienna jest zwykle niepusta, ale musisz zezwolić null
na warunek wstępny.
Z kolei w scenariuszach użycia polecenia DisallowNull
: ten atrybut służy do określenia, że argument typu odwołania dopuszczalnego do wartości null nie powinien mieć wartości null
. Rozważ właściwość, w której null
jest wartość domyślna, ale klienci mogą ustawić ją tylko na wartość inną niż null. Spójrzmy na poniższy kod:
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
Powyższy kod jest najlepszym sposobem wyrażenia projektu, że ReviewComment
może to być null
, ale nie można go ustawić na null
. Gdy ten kod jest zrozumiały dla wartości null, można wyraźniej wyrazić tę koncepcję w celu wywołania przy użyciu elementu System.Diagnostics.CodeAnalysis.DisallowNullAttribute:
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
W kontekście dopuszczania wartości null metodę ReviewComment
get
dostępu może zwrócić wartość null
domyślną . Kompilator ostrzega, że musi zostać sprawdzony przed uzyskaniem dostępu. Ponadto ostrzega rozmówców, że mimo że może to być null
, osoby wywołujące nie powinny jawnie ustawić go na null
. Atrybut DisallowNull
określa również warunek wstępny, nie ma wpływu na akcesoriumget
. Atrybut jest DisallowNull
używany podczas obserwowania następujących cech:
- Zmienna może być
null
w podstawowych scenariuszach, często po pierwszym utworzeniu wystąpienia. - Zmienna nie powinna być jawnie ustawiona na
null
.
Takie sytuacje są powszechne w kodzie, który pierwotnie miał wartość null nieświadomy. Może się zdarzyć, że właściwości obiektu są ustawiane w dwóch odrębnych operacjach inicjowania. Może się zdarzyć, że niektóre właściwości są ustawione dopiero po zakończeniu pracy asynchronicznej.
Atrybuty AllowNull
i DisallowNull
umożliwiają określenie, że warunki wstępne zmiennych mogą nie być zgodne z adnotacjami dopuszczanymi do wartości null w tych zmiennych. Zawierają one więcej szczegółowych informacji na temat cech interfejsu API. Te dodatkowe informacje pomagają obiektom wywołującym prawidłowo używać interfejsu API. Pamiętaj, że należy określić warunki wstępne przy użyciu następujących atrybutów:
- AllowNull: Argument bez wartości null może mieć wartość null.
- Nie zezwalajNull: argument dopuszczalny do wartości null nigdy nie powinien mieć wartości null.
Postconditions: MaybeNull
i NotNull
Załóżmy, że masz metodę z następującym podpisem:
public Customer FindCustomer(string lastName, string firstName)
Prawdopodobnie napisano metodę podobną do tej, która ma być zwracana null
, gdy szukana nazwa nie została znaleziona. Wyraźnie null
wskazuje, że rekord nie został znaleziony. W tym przykładzie prawdopodobnie zmienisz typ zwracany z Customer
na Customer?
. Deklarowanie wartości zwracanej jako typu odwołania dopuszczającego wartość null określa intencję tego interfejsu API wyraźnie:
public Customer? FindCustomer(string lastName, string firstName)
Ze względów opisanych w sekcji Wartości null generics ta technika może nie generować analizy statycznej zgodnej z interfejsem API. Może istnieć metoda ogólna, która jest zgodna z podobnym wzorcem:
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
Metoda zwraca wartość null
, gdy poszukiwany element nie zostanie znaleziony. Można wyjaśnić, że metoda zwraca null
, gdy element nie zostanie znaleziony, dodając adnotację MaybeNull
do metody zwracanej:
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
Powyższy kod informuje obiekty wywołujące, że wartość zwracana może być rzeczywiście równa null. Informuje również kompilator, że metoda może zwrócić null
wyrażenie, mimo że typ jest niepusty. Jeśli masz metodę ogólną zwracającą wystąpienie parametru typu , można wyrazić, T
że nigdy nie zwraca null
przy użyciu atrybutu NotNull
.
Można również określić, że wartość zwracana lub argument nie ma wartości null, mimo że typ jest typem odwołania dopuszczanym do wartości null. Poniższa metoda to metoda pomocnika, która zgłasza, jeśli jej pierwszy argument to null
:
public static void ThrowWhenNull(object value, string valueExpression = "")
{
if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}
Tę procedurę można wywołać w następujący sposób:
public static void LogMessage(string? message)
{
ThrowWhenNull(message, $"{nameof(message)} must not be null");
Console.WriteLine(message.Length);
}
Po włączeniu typów odwołań o wartości null należy upewnić się, że powyższy kod kompiluje się bez ostrzeżeń. Gdy metoda zwróci wartość, parametr ma gwarancję, value
że nie ma wartości null. Jednak dopuszczalne jest wywołanie ThrowWhenNull
przy użyciu odwołania o wartości null. Możesz utworzyć value
typ odwołania dopuszczalnego do wartości null i dodać NotNull
warunek post do deklaracji parametru:
public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided
Powyższy kod wyraźnie wyraża istniejący kontrakt: obiekt wywołujący może przekazać zmienną z null
wartością, ale argument ma gwarancję, że nigdy nie będzie mieć wartości null, jeśli metoda zwróci wyjątek bez zgłaszania wyjątku.
Należy określić bezwarunkowe postconditions przy użyciu następujących atrybutów:
- MożeNull: Wartość zwracana bez wartości null może mieć wartość null.
- NotNull: zwracana wartość dopuszczana do wartości null nigdy nie będzie równa null.
Warunki końcowe warunkowe: NotNullWhen
, MaybeNullWhen
i NotNullIfNotNull
Prawdopodobnie znasz metodę string
String.IsNullOrEmpty(String). Ta metoda zwraca wartość true
, gdy argument ma wartość null lub pusty ciąg. Jest to forma sprawdzania wartości null: Osoby wywołujące nie muszą sprawdzać argumentu o wartości null, jeśli metoda zwraca false
wartość . Aby określić metodę podobną do tej dopuszczanej wartości null, należy ustawić argument na typ odwołania dopuszczalny do wartości null i dodać NotNullWhen
atrybut:
bool IsNullOrEmpty([NotNullWhen(false)] string? value)
Informuje kompilator, że każdy kod, w którym zwracana wartość nie wymaga false
sprawdzania wartości null. Dodanie atrybutu informuje statyczną analizę kompilatora, która IsNullOrEmpty
wykonuje niezbędne sprawdzanie wartości null: gdy zwraca false
wartość , argument nie null
jest .
string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.
Metoda String.IsNullOrEmpty(String) zostanie oznaczona adnotacją, jak pokazano powyżej dla platformy .NET Core 3.0. W bazie kodu mogą istnieć podobne metody, które sprawdzają stan obiektów pod kątem wartości null. Kompilator nie rozpoznaje niestandardowych metod sprawdzania wartości null i musisz samodzielnie dodać adnotacje. Po dodaniu atrybutu analiza statyczna kompilatora wie, kiedy testowana zmienna ma wartość null.
Innym zastosowaniem Try*
tych atrybutów jest wzorzec. Postconditions dla ref
argumentów i out
są przekazywane za pośrednictwem wartości zwracanej. Rozważmy tę metodę pokazaną wcześniej (w kontekście wyłączonym z możliwością wartości null):
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
Poprzednia metoda jest zgodna z typowym idiomem platformy .NET: wartość zwracana wskazuje, czy message
została ustawiona na znalezioną wartość lub, jeśli nie znaleziono komunikatu, na wartość domyślną. Jeśli metoda zwraca true
wartość , wartość parametru message
nie ma wartości null; w przeciwnym razie metoda ustawia wartość message
null.
W kontekście obsługującym wartość null można przekazać ten idiom przy użyciu atrybutu NotNullWhen
. W przypadku dodawania adnotacji parametrów dla typów odwołań dopuszczanych do wartości null należy utworzyć message
string?
atrybut i dodać go:
bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}
W poprzednim przykładzie wartość message
jest znana jako nie null, gdy TryGetMessage
zwraca wartość true
. Należy dodać adnotacje do podobnych metod w bazie kodu w taki sam sposób: argumenty mogą być równe null
, i są znane, że nie mają wartości null, gdy metoda zwraca true
wartość .
Może być również potrzebny jeden ostatni atrybut. Czasami stan null wartości zwracanej zależy od stanu null co najmniej jednego argumentu. Te metody będą zwracać wartość inną niż null, gdy niektóre argumenty nie null
są . Aby poprawnie dodać adnotacje do tych metod, należy użyć atrybutu NotNullIfNotNull
. Rozważmy następującą metodę:
string GetTopLevelDomainFromFullUrl(string url)
url
Jeśli argument nie ma wartości null, dane wyjściowe nie null
są . Po włączeniu odwołań dopuszczanych do wartości null należy dodać więcej adnotacji, jeśli interfejs API może zaakceptować argument o wartości null. Możesz dodać adnotację do typu zwracanego, jak pokazano w poniższym kodzie:
string? GetTopLevelDomainFromFullUrl(string? url)
To również działa, ale często zmusza rozmówców do wdrożenia dodatkowych null
kontroli. Kontrakt polega na tym, że wartość zwracana byłaby null
tylko wtedy, gdy argument url
to null
. Aby wyrazić ten kontrakt, należy dodać adnotację do tej metody, jak pokazano w poniższym kodzie:
[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)
W poprzednim przykładzie użyto nameof
operatora dla parametru url
. Ta funkcja jest dostępna w języku C# 11. Przed użyciem języka C# 11 należy wpisać nazwę parametru jako ciąg. Wartość zwracana i argument zostały oznaczone adnotacją wskazującą ?
, że może to być null
. Atrybut dodatkowo wyjaśnia, że zwracana wartość nie będzie mieć wartości null, gdy url
argument nie null
ma wartości .
Warunkowe pokondycjach określa się przy użyciu następujących atrybutów:
- MożeNullWhen: Argument niezwiązany z wartością null może mieć wartość null, gdy metoda zwraca określoną
bool
wartość. - NotNullWhen: argument dopuszczalny do wartości null nie będzie mieć wartości null, gdy metoda zwraca określoną
bool
wartość. - NotNullIfNotNull: wartość zwracana nie ma wartości null, jeśli argument dla określonego parametru nie ma wartości null.
Metody pomocnika: MemberNotNull
i MemberNotNullWhen
Te atrybuty określają intencję podczas refaktoryzacji wspólnego kodu z konstruktorów do metod pomocników. Kompilator języka C# analizuje konstruktory i inicjatory pól, aby upewnić się, że wszystkie pola odwołania niezwiązane z wartościami null zostały zainicjowane przed zwróceniem każdego konstruktora. Jednak kompilator języka C# nie śledzi przypisań pól za pomocą wszystkich metod pomocnika. Kompilator zgłasza ostrzeżenie CS8618
, gdy pola nie są inicjowane bezpośrednio w konstruktorze, ale raczej w metodzie pomocniczej. Należy dodać element MemberNotNullAttribute do deklaracji metody i określić pola, które są inicjowane do wartości innej niż null w metodzie . Rozważmy na przykład następujący przykład:
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();
}
}
Można określić wiele nazw pól jako argumentów konstruktora atrybutu MemberNotNull
.
Argument ma MemberNotNullWhenAttribute bool
argument. MemberNotNullWhen
W sytuacjach, w których metoda pomocnika zwraca wartość wskazującąbool
, czy metoda pomocnika zainicjowała pola.
Zatrzymaj analizę dopuszczaną do wartości null, gdy wywoływana metoda zgłasza
Niektóre metody, zazwyczaj pomocnicy wyjątków lub inne metody narzędziowe, zawsze zamykają się, zgłaszając wyjątek. Lub pomocnik może zgłosić wyjątek na podstawie wartości argumentu logicznego.
W pierwszym przypadku można dodać DoesNotReturnAttribute atrybut do deklaracji metody. Analiza stanu null kompilatora nie sprawdza żadnego kodu w metodzie, która jest zgodna z wywołaniem metody z adnotacją .DoesNotReturn
Rozważmy tę metodę:
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}
public void SetState(object containedField)
{
if (containedField is null)
{
FailFast();
}
// containedField can't be null:
_field = containedField;
}
Kompilator nie generuje żadnych ostrzeżeń po wywołaniu metody FailFast
.
W drugim przypadku należy dodać System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute atrybut do parametru logicznego metody . Poprzedni przykład można zmodyfikować w następujący sposób:
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;
}
Gdy wartość argumentu jest zgodna z wartością DoesNotReturnIf
konstruktora, kompilator nie wykonuje żadnej analizy stanu null po tej metodzie.
Podsumowanie
Dodanie typów odwołań dopuszczających wartość null zapewnia początkowe słownictwo opisujące oczekiwania interfejsów API dotyczące zmiennych, które mogą mieć null
wartość . Atrybuty zapewniają bogatsze słownictwo opisujące stan null zmiennych jako warunki wstępne i terminy końcowe. Te atrybuty lepiej opisują oczekiwania i zapewniają lepsze środowisko dla deweloperów korzystających z interfejsów API.
Podczas aktualizowania bibliotek dla kontekstu dopuszczanego do wartości null dodaj te atrybuty, aby kierować użytkowników interfejsów API do poprawnego użycia. Te atrybuty pomagają w pełni opisać stan null argumentów i zwracane wartości.
- AllowNull: pole bez wartości null, parametr lub właściwość może mieć wartość null.
- Nie zezwalajNull: pole dopuszczane do wartości null, parametr lub właściwość nigdy nie powinny mieć wartości null.
- MożeNull: pole bez wartości null, parametr, właściwość lub wartość zwracana może mieć wartość null.
- NotNull: pole dopuszczane do wartości null, parametr, właściwość lub wartość zwracana nigdy nie będą mieć wartości null.
- MożeNullWhen: Argument niezwiązany z wartością null może mieć wartość null, gdy metoda zwraca określoną
bool
wartość. - NotNullWhen: argument dopuszczalny do wartości null nie będzie mieć wartości null, gdy metoda zwraca określoną
bool
wartość. - NotNullIfNotNull: parametr, właściwość lub wartość zwracana nie ma wartości null, jeśli argument dla określonego parametru nie ma wartości null.
- DoesNotReturn: metoda lub właściwość nigdy nie zwraca. Innymi słowy, zawsze zgłasza wyjątek.
- DoesNotReturnIf: ta metoda lub właściwość nigdy nie zwraca, jeśli skojarzony
bool
parametr ma określoną wartość.