Kenmerken voor statische analyse met null-status, geïnterpreteerd door de C#-compiler

In een context met null-functionaliteit voert de compiler statische analyse van code uit om de null-status van alle referentietypevariabelen te bepalen:

  • not-null: Statische analyse bepaalt dat een variabele een niet-null-waarde heeft.
  • misschien-null: statische analyse kan niet bepalen of aan een variabele een niet-null-waarde is toegewezen.

Met deze statussen kan de compiler waarschuwingen geven wanneer u een null-waarde kunt deductie uitvoeren, waardoor een System.NullReferenceException. Deze kenmerken bieden de compiler semantische informatie over de null-status van argumenten, retourwaarden en objectleden op basis van de status van argumenten en retourwaarden. De compiler biedt nauwkeurigere waarschuwingen wanneer uw API's correct zijn geannoteerd met deze semantische informatie.

Dit artikel bevat een korte beschrijving van elk van de kenmerken van het null-verwijzingstype en hoe u deze kunt gebruiken.

Laten we beginnen met een voorbeeld. Stel dat uw bibliotheek de volgende API heeft om een resourcereeks op te halen. Deze methode is oorspronkelijk gecompileerd in een niet-bruikbare context:

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

Het voorgaande voorbeeld volgt het vertrouwde Try* patroon in .NET. Er zijn twee referentieparameters voor deze API: de key en de message. Deze API heeft de volgende regels met betrekking tot de null-status van deze parameters:

  • Bellers mogen niet doorgeven null als het argument voor key.
  • Bellers kunnen een variabele doorgeven waarvan de waarde het argument is null voor message.
  • Als de TryGetMessage methode retourneert true, is de waarde message van niet null. Als de retourwaarde false, de waarde is van message null.

De regel voor key kan beknopt worden uitgedrukt: key moet een niet-null-referentietype zijn. De message parameter is complexer. Hiermee is een variabele toegestaan die het argument is null , maar garandeert, bij succes, dat het out argument niet nullis. Voor deze scenario's hebt u een uitgebreidere woordenlijst nodig om de verwachtingen te beschrijven. Het NotNullWhen kenmerk, dat hieronder wordt beschreven, beschrijft de null-status voor het argument dat wordt gebruikt voor de message parameter.

Notitie

Als u deze kenmerken toevoegt, krijgt de compiler meer informatie over de regels voor uw API. Wanneer het aanroepen van code wordt gecompileerd in een context met null-functionaliteit, waarschuwt de compiler bellers wanneer ze deze regels schenden. Met deze kenmerken worden geen controles meer ingeschakeld voor uw implementatie.

Kenmerk Categorie Betekenis
AllowNull Voorwaarde Een niet-nullbare parameter, veld of eigenschap kan null zijn.
DisallowNull Voorwaarde Een parameter, veld of eigenschap die null kan worden gebruikt, mag nooit null zijn.
MisschienNull Postcondition Een niet-nullbare parameter, veld, eigenschap of retourwaarde kan null zijn.
NotNull Postcondition Een parameter, veld, eigenschap of retourwaarde is nooit null.
MisschienNullWhen Voorwaardelijke nacondition Een niet-null-argument kan null zijn wanneer de methode de opgegeven bool waarde retourneert.
NotNullWhen Voorwaardelijke nacondition Een null-argument is niet null wanneer de methode de opgegeven bool waarde retourneert.
NotNullIfNotNull Voorwaardelijke nacondition Een retourwaarde, eigenschap of argument is niet null als het argument voor de opgegeven parameter niet null is.
MemberNotNull Helpermethoden voor methoden voor methoden en eigenschappen Het vermelde lid is niet null wanneer de methode wordt geretourneerd.
MemberNotNullWhen Helpermethoden voor methoden voor methoden en eigenschappen Het vermelde lid is niet null wanneer de methode de opgegeven bool waarde retourneert.
DoesNotReturn Onbereikbare code Een methode of eigenschap retourneert nooit. Met andere woorden, het genereert altijd een uitzondering.
DoesNotReturnIf Onbereikbare code Deze methode of eigenschap retourneert nooit als de bijbehorende bool parameter de opgegeven waarde heeft.

De voorgaande beschrijvingen zijn een beknopt overzicht van wat elk kenmerk doet. In de volgende secties worden het gedrag en de betekenis van deze kenmerken grondiger beschreven.

Voorwaarden: AllowNull en DisallowNull

Overweeg een lees-/schrijfeigenschap die nooit wordt geretourneerd null omdat deze een redelijke standaardwaarde heeft. Bellers geven null aan de ingestelde toegangsserver door wanneer ze deze op die standaardwaarde instellen. Denk bijvoorbeeld aan een berichtensysteem dat vraagt om een schermnaam in een chatruimte. Als er geen is opgegeven, genereert het systeem een willekeurige naam:

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

Wanneer u de voorgaande code compileert in een lege context, is alles prima. Zodra u null-verwijzingstypen hebt ingeschakeld, wordt de ScreenName eigenschap een niet-null-verwijzing. Dat is juist voor de get accessor: het retourneert nullnooit. Bellers hoeven de geretourneerde eigenschap niet te controleren op null. Maar nu de eigenschap zo instellen dat null er een waarschuwing wordt gegenereerd. Ter ondersteuning van dit type code voegt u het System.Diagnostics.CodeAnalysis.AllowNullAttribute kenmerk toe aan de eigenschap, zoals wordt weergegeven in de volgende code:

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

Mogelijk moet u een using instructie toevoegen voor System.Diagnostics.CodeAnalysis het gebruik van deze en andere kenmerken die in dit artikel worden besproken. Het kenmerk wordt toegepast op de eigenschap, niet op de set accessor. Het AllowNull kenmerk geeft vooraf voorwaarden op en is alleen van toepassing op argumenten. De get accessor heeft een retourwaarde, maar geen parameters. Daarom is het AllowNull kenmerk alleen van toepassing op de set accessor.

In het voorgaande voorbeeld ziet u wat u moet zoeken bij het toevoegen van het AllowNull kenmerk voor een argument:

  1. Het algemene contract voor die variabele is dat deze niet mag zijn null, dus u wilt een niet-null-verwijzingstype.
  2. Er zijn scenario's voor een aanroeper die als argument moet worden doorgegeven null , hoewel dit niet het meest voorkomende gebruik is.

Meestal hebt u dit kenmerk nodig voor eigenschappen, of in, outen ref argumenten. Het AllowNull kenmerk is de beste keuze wanneer een variabele doorgaans niet null is, maar u moet toestaan null als voorwaarde.

Contrasteer dat met scenario's voor het gebruik DisallowNull: U gebruikt dit kenmerk om op te geven dat een argument van een null-verwijzingstype niet mag zijn null. Overweeg een eigenschap waarbij null de standaardwaarde is, maar clients kunnen deze alleen instellen op een niet-null-waarde. Kijk eens naar de volgende code:

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

De voorgaande code is de beste manier om uw ontwerp uit te drukken dat het ReviewComment kan zijn null, maar kan niet worden ingesteld op null. Zodra deze code nullable is, kunt u dit concept duidelijker uitdrukken voor bellers met behulp van het System.Diagnostics.CodeAnalysis.DisallowNullAttributevolgende:

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

In een null-context kan de ReviewCommentget accessor de standaardwaarde van null. De compiler waarschuwt dat deze vóór toegang moet worden gecontroleerd. Bovendien waarschuwt het bellers dat bellers nulldeze niet expliciet moeten instellen nullop . Het DisallowNull kenmerk geeft ook een voorvoorwaarde op, dit heeft geen invloed op de get toegangsbeheerprogramma. U gebruikt het DisallowNull kenmerk wanneer u deze kenmerken bekijkt over:

  1. De variabele kan zich in kernscenario's bevinden null , vaak wanneer de eerste instantie werd geïnstantieerd.
  2. De variabele mag niet expliciet worden ingesteld op null.

Deze situaties zijn gebruikelijk in code die oorspronkelijk null-vergetelheid was. Het kan zijn dat objecteigenschappen zijn ingesteld in twee afzonderlijke initialisatiebewerkingen. Het kan zijn dat sommige eigenschappen pas worden ingesteld nadat een asynchroon werk is voltooid.

Met de AllowNull kenmerken DisallowNull kunt u opgeven dat voorwaarden voor variabelen mogelijk niet overeenkomen met de null-aantekeningen voor deze variabelen. Deze bieden meer informatie over de kenmerken van uw API. Met deze aanvullende informatie kunnen bellers uw API correct gebruiken. Onthoud dat u voorwaarden opgeeft met behulp van de volgende kenmerken:

  • AllowNull: Een argument dat niet null kan zijn, kan null zijn.
  • DisallowNull: Een null-argument mag nooit null zijn.

Postconditions: MaybeNull en NotNull

Stel dat u een methode hebt met de volgende handtekening:

public Customer FindCustomer(string lastName, string firstName)

U hebt waarschijnlijk een methode zoals deze geschreven om te retourneren null wanneer de gezochte naam niet is gevonden. Het null geeft duidelijk aan dat de record niet is gevonden. In dit voorbeeld zou u waarschijnlijk het retourtype wijzigen van Customer in Customer?. Als u de retourwaarde declareert als een null-referentietype, wordt de intentie van deze API duidelijk aangegeven:

public Customer? FindCustomer(string lastName, string firstName)

Om redenen die worden behandeld onder Algemene null-baarheid , kan de statische analyse niet worden geproduceerd die overeenkomt met uw API. Mogelijk hebt u een algemene methode die een vergelijkbaar patroon volgt:

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

De methode retourneert null wanneer het gezochte item niet wordt gevonden. U kunt verduidelijken dat de methode wordt geretourneerd null wanneer een item niet wordt gevonden door de MaybeNull aantekening toe te voegen aan de methode return:

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

De voorgaande code informeert bellers dat de retourwaarde daadwerkelijk null kan zijn. Ook wordt de compiler geïnformeerd dat de methode een null expressie kan retourneren, ook al is het type niet nullable. Wanneer u een algemene methode hebt die een exemplaar van de parameter van het type retourneert, Tkunt u uitdrukken dat deze nooit wordt geretourneerd null met behulp van het NotNull kenmerk.

U kunt ook opgeven dat een retourwaarde of een argument niet null is, ook al is het type een null-verwijzingstype. De volgende methode is een helpermethode die wordt veroorzaakt als het eerste argument is null:

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

U kunt deze routine als volgt noemen:

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

    Console.WriteLine(message.Length);
}

Nadat u null-verwijzingstypen hebt ingeschakeld, wilt u ervoor zorgen dat de voorgaande code zonder waarschuwingen wordt gecompileerd. Wanneer de methode wordt geretourneerd, is de value parameter gegarandeerd niet null. Het is echter acceptabel om aan te roepen ThrowWhenNull met een null-verwijzing. U kunt een null-verwijzingstype maken value en de NotNull postvoorwaarde toevoegen aan de parameterdeclaratie:

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

Met de voorgaande code wordt het bestaande contract duidelijk weergegeven: bellers kunnen een variabele doorgeven met de null waarde, maar het argument is gegarandeerd nooit null als de methode retourneert zonder een uitzondering te genereren.

U geeft onvoorwaardelijke naconditioners op met behulp van de volgende kenmerken:

  • MisschienNull: Een niet-nullable retourwaarde kan null zijn.
  • NotNull: Een nullable retourwaarde is nooit null.

Voorwaardelijke voorwaarden na: NotNullWhen, MaybeNullWhenen NotNullIfNotNull

Waarschijnlijk bent u bekend met de string methode String.IsNullOrEmpty(String). Deze methode retourneert true wanneer het argument null of een lege tekenreeks is. Dit is een vorm van null-controle: bellers hoeven het argument niet te controleren als de methode retourneert false. Als u een methode zoals deze nullable aware wilt maken, stelt u het argument in op een null-verwijzingstype en voegt u het NotNullWhen kenmerk toe:

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

Hiermee wordt de compiler geïnformeerd dat code waarvoor de retourwaarde geen null-controles nodig heeft false . De toevoeging van het kenmerk informeert de statische analyse van de compiler die IsNullOrEmpty de benodigde null-controle uitvoert: wanneer deze wordt geretourneerd false, is het argument niet null.

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

Notitie

Het voorgaande voorbeeld is alleen geldig in C# 11 en hoger. Vanaf C# 11 kan de nameof expressie verwijzen naar parameter- en typeparameternamen wanneer deze worden gebruikt in een kenmerk dat is toegepast op een methode. In C# 10 en eerder moet u een letterlijke tekenreeks gebruiken in plaats van de nameof expressie.

De String.IsNullOrEmpty(String) methode wordt geannoteerd zoals hierboven wordt weergegeven voor .NET Core 3.0. Mogelijk hebt u vergelijkbare methoden in uw codebasis die de status van objecten voor null-waarden controleren. De compiler herkent geen aangepaste null-controlemethoden en u moet zelf de aantekeningen toevoegen. Wanneer u het kenmerk toevoegt, weet de statische analyse van de compiler wanneer de geteste variabele null is gecontroleerd.

Een ander gebruik voor deze kenmerken is het Try* patroon. De naconditionen voor ref en out argumenten worden doorgegeven via de retourwaarde. Houd rekening met deze methode die eerder wordt weergegeven (in een uitgeschakelde context met null):

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

De voorgaande methode volgt een typische .NET-idiom: de retourwaarde geeft aan of message deze is ingesteld op de gevonden waarde of, als er geen bericht wordt gevonden, op de standaardwaarde. Als de methode retourneert true, is de waarde message niet null; anders wordt de methode ingesteld message op null.

In een context met null-functionaliteit kunt u dat idioom communiceren met behulp van het NotNullWhen kenmerk. Wanneer u aantekeningen maakt voor parameters voor null-verwijzingstypen, maakt message u een string? kenmerk en voegt u een kenmerk toe:

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

In het voorgaande voorbeeld is de waarde van message bekend dat deze niet null is als TryGetMessage resultaat true. U moet op dezelfde manier aantekeningen toevoegen aan vergelijkbare methoden in uw codebasis: de argumenten kunnen gelijk nullzijn aan en zijn bekend dat ze niet null zijn wanneer de methode wordt geretourneerd true.

Er is één laatste kenmerk dat u mogelijk ook nodig hebt. Soms is de null-status van een retourwaarde afhankelijk van de null-status van een of meer argumenten. Deze methoden retourneren een niet-null-waarde wanneer bepaalde argumenten dat niet nullzijn. Als u deze methoden correct wilt aantekeningen maken, gebruikt u het NotNullIfNotNull kenmerk. Houd rekening met de volgende methode:

string GetTopLevelDomainFromFullUrl(string url)

Als het url argument niet null is, is de uitvoer niet null. Zodra null-verwijzingen zijn ingeschakeld, moet u meer aantekeningen toevoegen als uw API een null-argument kan accepteren. U kunt aantekeningen maken op het retourtype, zoals wordt weergegeven in de volgende code:

string? GetTopLevelDomainFromFullUrl(string? url)

Dat werkt ook, maar dwingt bellers vaak om extra null controles te implementeren. Het contract is dat de retourwaarde alleen zou zijn null wanneer het argument url is null. Als u dit contract wilt uitdrukken, maakt u aantekeningen op deze methode, zoals wordt weergegeven in de volgende code:

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

In het vorige voorbeeld wordt de nameof operator voor de parameter urlgebruikt. Deze functie is beschikbaar in C# 11. Voor C# 11 moet u de naam van de parameter als tekenreeks typen. De retourwaarde en het argument zijn beide geannoteerd met de ? aanwijzing dat een van beide kan zijn null. Het kenmerk verduidelijkt verder dat de retourwaarde niet null is wanneer het url argument niet nullis.

U geeft voorwaardelijke postconditions op met behulp van deze kenmerken:

  • MisschienNullWhen: Een niet-null-argument kan null zijn wanneer de methode de opgegeven bool waarde retourneert.
  • NotNullWhen: Een nullable argument is niet null wanneer de methode de opgegeven bool waarde retourneert.
  • NotNullIfNotNull: een retourwaarde is niet null als het argument voor de opgegeven parameter niet null is.

Helpermethoden: MemberNotNull en MemberNotNullWhen

Deze kenmerken geven uw intentie op wanneer u algemene code van constructors hebt geherstructureerd in helpermethoden. De C#-compiler analyseert constructors en veld initialisatieprogramma's om ervoor te zorgen dat alle niet-nullbare referentievelden zijn geïnitialiseerd voordat elke constructor wordt geretourneerd. De C#-compiler houdt echter geen veldtoewijzingen bij via alle helpermethoden. De compiler geeft een waarschuwing CS8618 wanneer velden niet rechtstreeks in de constructor worden geïnitialiseerd, maar in een helpermethode. U voegt de MemberNotNullAttribute declaratie toe aan een methode en geeft de velden op die zijn geïnitialiseerd op een niet-null-waarde in de methode. Bekijk bijvoorbeeld het volgende voorbeeld:

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

U kunt meerdere veldnamen opgeven als argumenten voor de MemberNotNull kenmerkconstructor.

Het MemberNotNullWhenAttribute heeft een bool argument. U gebruikt MemberNotNullWhen in situaties waarin de helpermethode een bool indicatie retourneert of uw helpermethode geïnitialiseerde velden retourneert.

Null-analyse stoppen wanneer methode wordt aangeroepen

Sommige methoden, meestal uitzonderingshelpers of andere hulpprogrammamethoden, sluiten altijd af door een uitzondering te genereren. Een helper kan ook een uitzondering genereren op basis van de waarde van een Boole-argument.

In het eerste geval kunt u het DoesNotReturnAttribute kenmerk toevoegen aan de methodedeclaratie. De null-statusanalyse van de compiler controleert geen code in een methode die volgt op een aanroep van een methode met DoesNotReturnaantekeningen. Houd rekening met deze methode:

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

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

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

De compiler geeft geen waarschuwingen uit na de aanroep naar FailFast.

In het tweede geval voegt u het System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute kenmerk toe aan een Booleaanse parameter van de methode. U kunt het vorige voorbeeld als volgt wijzigen:

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

Wanneer de waarde van het argument overeenkomt met de waarde van de DoesNotReturnIf constructor, voert de compiler na die methode geen null-statusanalyse uit.

Samenvatting

Het toevoegen van null-referentietypen biedt een initiële woordenlijst om de verwachtingen van uw API's te beschrijven voor variabelen die kunnen zijn null. De kenmerken bieden een uitgebreidere woordenlijst om de null-status van variabelen te beschrijven als voorwaarden en postconditions. Deze kenmerken beschrijven uw verwachtingen duidelijker en bieden een betere ervaring voor de ontwikkelaars die uw API's gebruiken.

Wanneer u bibliotheken voor een nullable context bijwerkt, voegt u deze kenmerken toe om gebruikers van uw API's naar het juiste gebruik te leiden. Met deze kenmerken kunt u de null-status van argumenten en retourwaarden volledig beschrijven.

  • AllowNull: een niet-null-veld, parameter of eigenschap kan null zijn.
  • DisallowNull: Een nullable veld, parameter of eigenschap mag nooit null zijn.
  • MisschienNull: Een veld, parameter, eigenschap of retourwaarde kan null zijn.
  • NotNull: een null-veld, parameter, eigenschap of retourwaarde is nooit null.
  • MisschienNullWhen: Een niet-null-argument kan null zijn wanneer de methode de opgegeven bool waarde retourneert.
  • NotNullWhen: Een nullable argument is niet null wanneer de methode de opgegeven bool waarde retourneert.
  • NotNullIfNotNull: een parameter, eigenschap of retourwaarde is niet null als het argument voor de opgegeven parameter niet null is.
  • DoesNotReturn: een methode of eigenschap retourneert nooit. Met andere woorden, het genereert altijd een uitzondering.
  • DoesNotReturnIf: Deze methode of eigenschap retourneert nooit als de bijbehorende bool parameter de opgegeven waarde heeft.