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 messageAPI: 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 dla keyelementu .
  • Obiekt wywołujący może przekazać zmienną, której wartość jest null argumentem .message
  • TryGetMessage Jeśli metoda zwraca truewartość , wartość parametru message nie ma wartości null. Jeśli wartość zwracana jest false, wartością message 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 nulljest . 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 nullelementu . 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 :

  1. Ogólny kontrakt dla tej zmiennej polega na tym, że nie powinien mieć nullwartości , więc chcesz, aby typ odwołania nie dopuszczał wartości null.
  2. 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, outi 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ę ReviewCommentget dostępu może zwrócić wartość nulldomyś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:

  1. Zmienna może być null w podstawowych scenariuszach, często po pierwszym utworzeniu wystąpienia.
  2. 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, MaybeNullWheni NotNullIfNotNull

Prawdopodobnie znasz metodę stringString.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 falsewartość . 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 falsewartość , argument nie nulljest .

string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
    int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

Uwaga

Powyższy przykład jest prawidłowy tylko w języku C# 11 lub nowszym. Począwszy od języka C# 11, nameof wyrażenie może odwoływać się do parametrów i nazw parametrów typu w przypadku użycia w atrybucie zastosowanym do metody. W języku C# 10 i starszych należy użyć literału ciągu zamiast nameof wyrażenia.

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 truewartość , 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ć messagestring? 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 truewartość .

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 nullsą . 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 nullsą . 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 nullma 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 MemberNotNullWhenAttributebool 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ć nullwartość . 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ść.