Attribut för statisk analys med null-tillstånd som tolkas av C#-kompilatorn

I en null-aktiverad kontext utför kompilatorn statisk analys av kod för att fastställa null-tillståndet för alla referenstypvariabler:

  • not-null: Statisk analys avgör att en variabel har ett värde som inte är null.
  • maybe-null: Statisk analys kan inte avgöra att en variabel tilldelas ett värde som inte är null.

Dessa tillstånd gör att kompilatorn kan ge varningar när du kan avreferera ett null-värde och utlösa ett System.NullReferenceException. Dessa attribut ger kompilatorn semantisk information om null-tillståndet för argument, returvärden och objektmedlemmar baserat på argumentens och returvärdenas tillstånd. Kompilatorn ger mer exakta varningar när dina API:er har kommenterats korrekt med den här semantiska informationen.

Den här artikeln innehåller en kort beskrivning av var och en av de nullbara referenstypattributen och hur du använder dem.

Vi börjar med ett exempel. Anta att biblioteket har följande API för att hämta en resurssträng. Den här metoden kompilerades ursprungligen i en nullbar omedveten kontext:

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

Föregående exempel följer det välbekanta Try* mönstret i .NET. Det finns två referensparametrar för det här API:et keymessage: och . Det här API:et har följande regler som rör null-tillståndet för dessa parametrar:

  • Anropare bör inte skickas null som argument för key.
  • Anropare kan skicka en variabel vars värde är null som argument för message.
  • TryGetMessage Om metoden returnerar trueär värdet message för inte null. Om returvärdet är false, värdet för message är null.

Regeln för key kan uttryckas kortfattat: key bör vara en referenstyp som inte kan nulleras. Parametern message är mer komplex. Den tillåter en variabel som är null som argument, men garanterar att out argumentet inte nullär . För dessa scenarier behöver du ett rikare ordförråd för att beskriva förväntningarna. Attributet NotNullWhen som beskrivs nedan beskriver null-tillståndet för argumentet som används för parametern message .

Kommentar

Om du lägger till dessa attribut får kompilatorn mer information om reglerna för ditt API. När anropande kod kompileras i en nullaktiverad aktiverad kontext varnar kompilatorn anropare när de bryter mot dessa regler. De här attributen aktiverar inte fler kontroller av implementeringen.

Attribut Kategori Innebörd
AllowNull Förutsättning En parameter, ett fält eller en egenskap som inte är null kan vara null.
Tillåt inteNull Förutsättning En nullbar parameter, ett fält eller en egenskap får aldrig vara null.
MaybeNull Postcondition En parameter, ett fält, en egenskap eller ett returvärde som inte är null kan vara null.
NotNull Postcondition En nullbar parameter, ett fält, en egenskap eller ett returvärde blir aldrig null.
MaybeNullNär Villkorsstyrd postkondition Ett icke-nullbart argument kan vara null när metoden returnerar det angivna bool värdet.
NotNullWhen Villkorsstyrd postkondition Ett null-argument är inte null när metoden returnerar det angivna bool värdet.
NotNullIfNotNull Villkorsstyrd postkondition Ett returvärde, en egenskap eller ett argument är inte null om argumentet för den angivna parametern inte är null.
MemberNotNull Metod- och egenskapshjälpmetoder Medlemmen i listan är inte null när metoden returneras.
MemberNotNullWhen Metod- och egenskapshjälpmetoder Medlemmen i listan blir inte null när metoden returnerar det angivna bool värdet.
DoesNotReturn Oåtkomlig kod En metod eller egenskap returnerar aldrig. Med andra ord utlöser det alltid ett undantag.
DoesNotReturnIf Oåtkomlig kod Den här metoden eller egenskapen returnerar aldrig om den associerade bool parametern har det angivna värdet.

Ovanstående beskrivningar är en snabbreferens till vad varje attribut gör. I följande avsnitt beskrivs beteendet och innebörden av dessa attribut mer noggrant.

Förhandsvillkor: AllowNull och DisallowNull

Överväg en läs-/skrivegenskap som aldrig returneras null eftersom den har ett rimligt standardvärde. Anropare skickar null till den inställda åtkomstorn när de anger det till det standardvärdet. Tänk dig till exempel ett meddelandesystem som ber om ett skärmnamn i ett chattrum. Om inget anges genererar systemet ett slumpmässigt namn:

public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

När du kompilerar föregående kod i en oförståelig kontext är allt bra. När du aktiverar nullbara referenstyper blir egenskapen ScreenName en icke-nullbar referens. Det stämmer för get accessorn: den returnerar nullaldrig . Anropare behöver inte kontrollera den returnerade egenskapen för null. Men nu skapar egenskapen null en varning. För att stödja den här typen av kod lägger du till System.Diagnostics.CodeAnalysis.AllowNullAttribute attributet i egenskapen, som du ser i följande kod:

[AllowNull]
public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

Du kan behöva lägga till ett using direktiv för System.Diagnostics.CodeAnalysis att använda detta och andra attribut som beskrivs i den här artikeln. Attributet tillämpas på egenskapen, inte på set accessorn. Attributet AllowNull anger förhandsvillkor och gäller endast argument. Accessorn get har ett returvärde, men inga parametrar. Därför AllowNull gäller attributet endast för set accessorn.

Föregående exempel visar vad du ska leta efter när du lägger till attributet i AllowNull ett argument:

  1. Det allmänna kontraktet för den variabeln är att den inte ska vara null, så du vill ha en referenstyp som inte kan upphävas.
  2. Det finns scenarier där en anropare kan skickas null som argument, även om de inte är den vanligaste användningen.

Oftast behöver du det här attributet för egenskaper, eller in, outoch ref argument. Attributet AllowNull är det bästa valet när en variabel vanligtvis inte är null, men du måste tillåta null det som en förhandsvillkor.

Jämför det med scenarier för att använda DisallowNull: Du använder det här attributet för att ange att ett argument av en nullbar referenstyp inte ska vara null. Överväg en egenskap där null är standardvärdet, men klienter kan bara ange det till ett värde som inte är null. Ta följande kod som exempel:

public string ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

Föregående kod är det bästa sättet att uttrycka din design att ReviewComment kan vara null, men kan inte anges till null. När den här koden är nullbar kan du uttrycka det här konceptet tydligare för anropare med hjälp av System.Diagnostics.CodeAnalysis.DisallowNullAttribute:

[DisallowNull]
public string? ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

I en null-kontext ReviewCommentget kan accessorn returnera standardvärdet nullför . Kompilatorn varnar för att den måste kontrolleras innan åtkomst. Dessutom varnar den uppringare för att anropare, även om det kan vara null, inte uttryckligen bör ställa in den på null. Attributet DisallowNull anger också ett förhandsvillkor, det påverkar get inte åtkomstgivaren. Du använder attributet DisallowNull när du observerar följande egenskaper om:

  1. Variabeln kan finnas null i kärnscenarier, ofta när den först instansieras.
  2. Variabeln ska inte uttryckligen anges till null.

Dessa situationer är vanliga i kod som ursprungligen var null omedveten. Det kan vara så att objektegenskaper anges i två distinkta initieringsåtgärder. Det kan vara så att vissa egenskaper endast anges när något asynkront arbete har slutförts.

Med attributen AllowNull och DisallowNull kan du ange att förhandsvillkor för variabler kanske inte matchar de nullbara anteckningarna för dessa variabler. Dessa ger mer information om egenskaperna för ditt API. Den här ytterligare informationen hjälper anropare att använda ditt API korrekt. Kom ihåg att du anger förhandsvillkor med hjälp av följande attribut:

  • AllowNull: Ett argument som inte kan null-värdet kan vara null.
  • Tillåt inteNull: Ett null-argument får aldrig vara null.

Postconditions: MaybeNull och NotNull

Anta att du har en metod med följande signatur:

public Customer FindCustomer(string lastName, string firstName)

Du har förmodligen skrivit en metod som den här för att returnera null när namnet som söktes inte hittades. Det null visar tydligt att posten inte hittades. I det här exemplet skulle du förmodligen ändra returtypen från Customer till Customer?. Om du deklarerar returvärdet som en nullbar referenstyp anges avsikten med det här API:et tydligt:

public Customer? FindCustomer(string lastName, string firstName)

Av skäl som omfattas av generics nullability kan tekniken inte generera den statiska analys som matchar ditt API. Du kan ha en allmän metod som följer ett liknande mönster:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Metoden returnerar null när det sökta objektet inte hittas. Du kan klargöra att metoden returnerar null när ett objekt inte hittas genom att lägga till anteckningen MaybeNull i metodens retur:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Föregående kod informerar anropare om att returvärdet faktiskt kan vara null. Den informerar också kompilatorn om att metoden kan returnera ett null uttryck även om typen inte är nullbar. När du har en allmän metod som returnerar en instans av dess typparameter Tkan du uttrycka att den aldrig returneras null med hjälp NotNull av attributet.

Du kan också ange att ett returvärde eller ett argument inte är null trots att typen är en nullbar referenstyp. Följande metod är en hjälpmetod som genererar om det första argumentet är null:

public static void ThrowWhenNull(object value, string valueExpression = "")
{
    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

Du kan anropa den här rutinen på följande sätt:

public static void LogMessage(string? message)
{
    ThrowWhenNull(message, $"{nameof(message)} must not be null");

    Console.WriteLine(message.Length);
}

När du har aktiverat null-referenstyper vill du se till att koden ovan kompileras utan varningar. När metoden returneras är parametern value garanterat inte null. Det är dock acceptabelt att anropa ThrowWhenNull med en null-referens. Du kan ange value en null-referenstyp och lägga till eftervillkoret NotNull i parameterdeklarationen:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
    // other logic elided

Föregående kod uttrycker det befintliga kontraktet tydligt: Anropare kan skicka en variabel med null värdet, men argumentet är garanterat aldrig null om metoden returnerar utan att utlösa ett undantag.

Du anger ovillkorliga postkonditioner med hjälp av följande attribut:

  • MaybeNull: Ett icke-nullbart returvärde kan vara null.
  • NotNull: Ett null-returvärde blir aldrig null.

Villkorliga villkor: NotNullWhen, MaybeNullWhenoch NotNullIfNotNull

Du är förmodligen bekant med string metoden String.IsNullOrEmpty(String). Den här metoden returnerar true när argumentet är null eller en tom sträng. Det är en form av null-check: Anropare behöver inte null-kontrollera argumentet om metoden returnerar false. Om du vill göra en metod som denna nullbar medveten, anger du argumentet till en nullbar referenstyp och lägger till NotNullWhen attributet:

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

Det informerar kompilatorn om att all kod där returvärdet är false inte behöver null-kontroller. Tillägget av attributet informerar kompilatorns statiska analys som IsNullOrEmpty utför den nödvändiga null-kontrollen: när det returnerar falseär argumentet inte null.

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

Kommentar

Föregående exempel är endast giltigt i C# 11 och senare. Från och med C# 11 nameof kan uttrycket referera till parameter- och typparameternamn när det används i ett attribut som tillämpas på en metod. I C# 10 och tidigare måste du använda en strängliteral i stället för nameof uttrycket.

Metoden String.IsNullOrEmpty(String) kommenteras enligt ovan för .NET Core 3.0. Du kan ha liknande metoder i kodbasen som kontrollerar objektens status för null-värden. Kompilatorn känner inte igen anpassade null-kontrollmetoder och du måste lägga till anteckningarna själv. När du lägger till attributet vet kompilatorns statiska analys när den testade variabeln har markerats som null.

En annan användning för dessa attribut är Try* mönstret. Postkonditionerna för ref och out argumenten förmedlas via returvärdet. Tänk på den här metoden som visades tidigare (i en nullbar inaktiverad kontext):

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

Föregående metod följer ett typiskt .NET-formspråk: returvärdet anger om message värdet har angetts till det hittade värdet eller, om inget meddelande hittas, till standardvärdet. Om metoden returnerar trueär värdet message för inte null, annars anges message metoden till null.

I en null-aktiverad kontext kan du kommunicera detta formspråk med hjälp av attributet NotNullWhen . När du kommenterar parametrar för null-referenstyper gör message du ett string? och lägger till ett attribut:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message is not null;
}

I föregående exempel är värdet message för inte null när TryGetMessage returnerar true. Du bör kommentera liknande metoder i kodbasen på samma sätt: argumenten kan vara lika med nulloch är kända för att inte vara null när metoden returnerar true.

Det finns ett sista attribut som du också kan behöva. Ibland beror null-tillståndet för ett returvärde på null-tillståndet för ett eller flera argument. Dessa metoder returnerar ett värde som inte är null när vissa argument inte nullär . Om du vill kommentera dessa metoder korrekt använder du attributet NotNullIfNotNull . Tänk på följande metod:

string GetTopLevelDomainFromFullUrl(string url)

url Om argumentet inte är null är utdata inte null. När null-referenser har aktiverats måste du lägga till fler anteckningar om ditt API kan acceptera ett null-argument. Du kan kommentera returtypen enligt följande kod:

string? GetTopLevelDomainFromFullUrl(string? url)

Det fungerar också, men tvingar ofta uppringare att implementera extra null kontroller. Kontraktet är att returvärdet bara skulle vara null när argumentet url är null. Om du vill uttrycka kontraktet kommenterar du den här metoden enligt följande kod:

[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)

I föregående exempel används operatorn nameof för parametern url. Den funktionen är tillgänglig i C# 11. Innan C# 11 måste du ange namnet på parametern som en sträng. Returvärdet och argumentet har båda kommenterats med ? indikerar att antingen kan vara null. Attributet förtydligar vidare att returvärdet inte är null när url argumentet inte nullär .

Du anger villkorsstyrda postkonditioner med hjälp av följande attribut:

  • MaybeNullWhen: Ett icke-nullbart argument kan vara null när metoden returnerar det angivna bool värdet.
  • NotNullWhen: Ett nullbart argument är inte null när metoden returnerar det angivna bool värdet.
  • NotNullIfNotNull: Ett returvärde är inte null om argumentet för den angivna parametern inte är null.

Hjälpmetoder: MemberNotNull och MemberNotNullWhen

Dessa attribut anger din avsikt när du har omstrukturerat vanlig kod från konstruktorer till hjälpmetoder. C#-kompilatorn analyserar konstruktorer och fältinitierare för att se till att alla referensfält som inte kan nulliseras har initierats innan varje konstruktor returnerar. C#-kompilatorn spårar dock inte fälttilldelningar via alla hjälpmetoder. Kompilatorn utfärdar en varning CS8618 när fält inte initieras direkt i konstruktorn, utan i stället i en hjälpmetod. Du lägger till i MemberNotNullAttribute en metoddeklaration och anger de fält som initieras till ett värde som inte är null i metoden. Tänk till exempel på följande exempel:

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

Du kan ange flera fältnamn som argument för MemberNotNull attributkonstruktorn.

Har MemberNotNullWhenAttribute ett bool argument. Du använder MemberNotNullWhen i situationer där din hjälpmetod returnerar ett bool som anger om din hjälpmetod initierade fält.

Stoppa nullbar analys när metoden anropas kastar

Vissa metoder, vanligtvis undantagshjälpare eller andra verktygsmetoder, avslutar alltid genom att utlösa ett undantag. En hjälp kan också utlösa ett undantag baserat på värdet för ett booleskt argument.

I det första fallet kan du lägga till DoesNotReturnAttribute attributet i metoddeklarationen. Kompilatorns null-tillståndsanalys kontrollerar inte någon kod i en metod som följer ett anrop till en metod som kommenterats med DoesNotReturn. Tänk på den här metoden:

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

Kompilatorn utfärdar inga varningar efter anropet till FailFast.

I det andra fallet lägger du till System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute attributet till en boolesk parameter för metoden. Du kan ändra föregående exempel på följande sätt:

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

När värdet för argumentet matchar konstruktorns DoesNotReturnIf värde utför kompilatorn ingen null-tillståndsanalys efter den metoden.

Sammanfattning

Om du lägger till nullbara referenstyper får du en inledande vokabulär för att beskriva dina API:ers förväntningar på variabler som kan vara null. Attributen ger ett mer omfattande ordförråd för att beskriva nulltillståndet för variabler som förhandsvillkor och postkonditioner. Dessa attribut beskriver dina förväntningar tydligare och ger en bättre upplevelse för utvecklare som använder dina API:er.

När du uppdaterar bibliotek för en nullbar kontext lägger du till dessa attribut för att vägleda användare av dina API:er till rätt användning. De här attributen hjälper dig att fullständigt beskriva null-tillståndet för argument och returvärden.

  • AllowNull: Ett fält, parameter eller egenskap som inte kan null-värdet kan vara null.
  • Tillåt inteNull: Ett nullbart fält, en parameter eller en egenskap får aldrig vara null.
  • MaybeNull: Ett fält, parameter, egenskap eller returvärde som inte kan null-värdet kan vara null.
  • NotNull: Ett nullbart fält, en parameter, en egenskap eller ett returvärde blir aldrig null.
  • MaybeNullWhen: Ett icke-nullbart argument kan vara null när metoden returnerar det angivna bool värdet.
  • NotNullWhen: Ett nullbart argument är inte null när metoden returnerar det angivna bool värdet.
  • NotNullIfNotNull: Ett parameter-, egenskaps- eller returvärde är inte null om argumentet för den angivna parametern inte är null.
  • DoesNotReturn: En metod eller egenskap returnerar aldrig. Med andra ord utlöser det alltid ett undantag.
  • DoesNotReturnIf: Den här metoden eller egenskapen returnerar aldrig om den associerade bool parametern har det angivna värdet.