Självstudie: Uttrycka design avsikten tydligare med nullbara och icke-null-referenstyper

Referenstyper som kan användas som null kompletterar referenstyper på samma sätt som nullbara värdetyper kompletterar värdetyper. Du deklarerar en variabel som en nullbar referenstyp genom att lägga till en ? till typen . Representerar till exempel string? en nullbar string. Du kan använda dessa nya typer för att tydligare uttrycka din design avsikt: vissa variabler måste alltid ha ett värde, andra kanske saknar ett värde.

I den här självstudien får du lära dig att:

  • Införliva null- och icke-null-referenstyper i designen
  • Aktivera kontroller av null-referenstyper i hela koden.
  • Skriv kod där kompilatorn framtvingar dessa designbeslut.
  • Använd den nullbara referensfunktionen i dina egna designer

Krav

Du måste konfigurera datorn för att köra .NET, inklusive C#-kompilatorn. C#-kompilatorn är tillgänglig med Visual Studio 2022 eller .NET SDK.

Den här självstudien förutsätter att du är bekant med C# och .NET, inklusive antingen Visual Studio eller .NET CLI.

Införliva null-referenstyper i designen

I den här självstudien skapar du ett bibliotek som modeller som kör en undersökning. Koden använder både nullbara referenstyper och icke-nullbara referenstyper för att representera de verkliga begreppen. Enkätfrågorna kan aldrig vara null. En svarande kanske föredrar att inte svara på en fråga. Svaren kan vara null i det här fallet.

Koden som du skriver för det här exemplet uttrycker den avsikten, och kompilatorn tillämpar den avsikten.

Skapa programmet och aktivera null-referenstyper

Skapa ett nytt konsolprogram antingen i Visual Studio eller från kommandoraden med .dotnet new console Ge programmet NullableIntroductionnamnet . När du har skapat programmet måste du ange att hela projektet kompileras i en aktiverad ogiltig anteckningskontext. Öppna .csproj-filen och lägg till ett Nullable element i elementet PropertyGroup . Ställ in värdet på enable. Du måste välja funktionen för null-referenstyper i projekt tidigare än C# 11. Det beror på att när funktionen är aktiverad blir befintliga referensvariabeldeklarationer icke-nullbara referenstyper. Även om det beslutet hjälper dig att hitta problem där befintlig kod kanske inte har rätt null-kontroller, kanske det inte korrekt återspeglar din ursprungliga design avsikt:

<Nullable>enable</Nullable>

Före .NET 6 innehåller inte nya projekt elementet Nullable . Från och med .NET 6 innehåller nya projekt elementet <Nullable>enable</Nullable> i projektfilen.

Utforma typerna för programmet

Det här undersökningsprogrammet kräver att du skapar ett antal klasser:

  • En klass som modellerar listan med frågor.
  • En klass som modellerar en lista över personer som har kontaktats för undersökningen.
  • En klass som modellerar svaren från en person som tog undersökningen.

Dessa typer använder både null- och icke-null-referenstyper för att uttrycka vilka medlemmar som krävs och vilka medlemmar som är valfria. Referenstyper som kan användas som null kommunicerar den designen tydligt:

  • Frågorna som ingår i undersökningen kan aldrig vara null: Det är meningslöst att ställa en tom fråga.
  • Respondenterna kan aldrig vara null. Du vill spåra personer som du har kontaktat, även de svarande som avböjt att delta.
  • Alla svar på en fråga kan vara null. Respondenterna kan avböja att svara på några eller alla frågor.

Om du har programmerat i C#, kan du vara så van vid referenstyper som tillåter null värden som du kanske har missat andra möjligheter att deklarera icke-nullbara instanser:

  • Frågesamlingen bör inte vara nullbar.
  • Insamlingen av respondenter bör inte vara nullbar.

När du skriver koden ser du att en referenstyp som inte är nullbar som standard för referenser undviker vanliga misstag som kan leda till NullReferenceExceptions. En lärdom av den här självstudien är att du har fattat beslut om vilka variabler som kan eller inte kan vara null. Språket angav inte syntax för att uttrycka dessa beslut. Nu gör den det.

Appen som du skapar gör följande:

  1. Skapar en undersökning och lägger till frågor till den.
  2. Skapar en pseudo-slumpmässig uppsättning respondenter för undersökningen.
  3. Kontaktar respondenterna tills den slutförda undersökningsstorleken når målnumret.
  4. Skriver ut viktig statistik om enkätsvaren.

Skapa undersökningen med null- och icke-null-referenstyper

Den första koden du skriver skapar undersökningen. Du skriver klasser för att modellera en undersökningsfråga och en undersökningskörning. Din undersökning har tre typer av frågor, som skiljer sig från svarets format: Ja/Nej-svar, nummersvar och textsvar. Skapa en public SurveyQuestion klass:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Kompilatorn tolkar varje deklaration av referenstypvariabeln som en icke-nullbar referenstyp för kod i en aktiverad ogiltig anteckningskontext. Du kan se din första varning genom att lägga till egenskaper för frågetexten och typen av fråga, som du ser i följande kod:

namespace NullableIntroduction
{
    public enum QuestionType
    {
        YesNo,
        Number,
        Text
    }

    public class SurveyQuestion
    {
        public string QuestionText { get; }
        public QuestionType TypeOfQuestion { get; }
    }
}

Eftersom du inte har initierat QuestionTextutfärdar kompilatorn en varning om att en egenskap som inte kan nulliseras inte har initierats. Din design kräver att frågetexten inte är null, så du lägger till en konstruktor för att initiera den QuestionType och även värdet. Den färdiga klassdefinitionen ser ut som följande kod:

namespace NullableIntroduction;

public enum QuestionType
{
    YesNo,
    Number,
    Text
}

public class SurveyQuestion
{
    public string QuestionText { get; }
    public QuestionType TypeOfQuestion { get; }

    public SurveyQuestion(QuestionType typeOfQuestion, string text) =>
        (TypeOfQuestion, QuestionText) = (typeOfQuestion, text);
}

När du lägger till konstruktorn tar du bort varningen. Konstruktorargumentet är också en referenstyp som inte kan nulliseras, så kompilatorn utfärdar inga varningar.

Skapa sedan en public klass med namnet SurveyRun. Den här klassen innehåller en lista över SurveyQuestion objekt och metoder för att lägga till frågor i undersökningen, enligt följande kod:

using System.Collections.Generic;

namespace NullableIntroduction
{
    public class SurveyRun
    {
        private List<SurveyQuestion> surveyQuestions = new List<SurveyQuestion>();

        public void AddQuestion(QuestionType type, string question) =>
            AddQuestion(new SurveyQuestion(type, question));
        public void AddQuestion(SurveyQuestion surveyQuestion) => surveyQuestions.Add(surveyQuestion);
    }
}

Precis som tidigare måste du initiera listobjektet till ett värde som inte är null eller så utfärdar kompilatorn en varning. Det finns inga null-kontroller i den andra överlagringen av AddQuestion eftersom de inte behövs: Du har deklarerat att variabeln inte är nullbar. Dess värde får inte vara null.

Växla till Program.cs i redigeringsprogrammet och ersätt innehållet i Main med följande kodrader:

var surveyRun = new SurveyRun();
surveyRun.AddQuestion(QuestionType.YesNo, "Has your code ever thrown a NullReferenceException?");
surveyRun.AddQuestion(new SurveyQuestion(QuestionType.Number, "How many times (to the nearest 100) has that happened?"));
surveyRun.AddQuestion(QuestionType.Text, "What is your favorite color?");

Eftersom hela projektet är i en aktiverad nullbar anteckningskontext får du varningar när du skickar null till alla metoder som förväntar sig en referenstyp som inte kan ogiltigförklaras. Prova genom att lägga till följande rad i Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Skapa respondenter och få svar på undersökningen

Skriv sedan koden som genererar svar på undersökningen. Den här processen omfattar flera små uppgifter:

  1. Skapa en metod som genererar respondentobjekt. Dessa representerar personer som ombeds fylla i undersökningen.
  2. Skapa logik för att simulera att ställa frågor till en svarande och samla in svar eller notera att en svarande inte svarade.
  3. Upprepa tills tillräckligt många respondenter har besvarat undersökningen.

Du behöver en klass för att representera ett undersökningssvar, så lägg till det nu. Aktivera stöd som kan användas som null. Lägg till en Id egenskap och en konstruktor som initierar den, enligt följande kod:

namespace NullableIntroduction
{
    public class SurveyResponse
    {
        public int Id { get; }

        public SurveyResponse(int id) => Id = id;
    }
}

Lägg sedan till en static metod för att skapa nya deltagare genom att generera ett slumpmässigt ID:

private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());

Huvudansvaret för den här klassen är att generera svar för en deltagare på frågorna i undersökningen. Det här ansvaret har några steg:

  1. Be om deltagande i undersökningen. Om personen inte samtycker returnerar du ett svar som saknas (eller null).
  2. Ställ varje fråga och spela in svaret. Varje svar kan också saknas (eller null).

Lägg till följande kod i klassen SurveyResponse :

private Dictionary<int, string>? surveyResponses;
public bool AnswerSurvey(IEnumerable<SurveyQuestion> questions)
{
    if (ConsentToSurvey())
    {
        surveyResponses = new Dictionary<int, string>();
        int index = 0;
        foreach (var question in questions)
        {
            var answer = GenerateAnswer(question);
            if (answer != null)
            {
                surveyResponses.Add(index, answer);
            }
            index++;
        }
    }
    return surveyResponses != null;
}

private bool ConsentToSurvey() => randomGenerator.Next(0, 2) == 1;

private string? GenerateAnswer(SurveyQuestion question)
{
    switch (question.TypeOfQuestion)
    {
        case QuestionType.YesNo:
            int n = randomGenerator.Next(-1, 2);
            return (n == -1) ? default : (n == 0) ? "No" : "Yes";
        case QuestionType.Number:
            n = randomGenerator.Next(-30, 101);
            return (n < 0) ? default : n.ToString();
        case QuestionType.Text:
        default:
            switch (randomGenerator.Next(0, 5))
            {
                case 0:
                    return default;
                case 1:
                    return "Red";
                case 2:
                    return "Green";
                case 3:
                    return "Blue";
            }
            return "Red. No, Green. Wait.. Blue... AAARGGGGGHHH!";
    }
}

Lagringen för undersökningssvaren är en Dictionary<int, string>?, som anger att den kan vara null. Du använder den nya språkfunktionen för att deklarera din designavsikt, både till kompilatorn och till alla som läser koden senare. Om du någonsin avrefereras surveyResponses utan att först söka null efter värdet får du en kompilatorvarning. Du får ingen varning i AnswerSurvey metoden eftersom kompilatorn kan fastställa att variabeln surveyResponses har angetts till ett värde som inte är null ovan.

Om du använder null för saknade svar är det viktigt att arbeta med referenstyper som kan ha värdet null: målet är inte att ta bort alla null värden från programmet. Målet är i stället att se till att den kod du skriver uttrycker avsikten med din design. Saknade värden är ett nödvändigt begrepp för att uttrycka i din kod. Värdet null är ett tydligt sätt att uttrycka de saknade värdena. Att försöka ta bort alla null värden leder bara till att definiera något annat sätt att uttrycka de saknade värdena utan null.

Därefter måste du skriva PerformSurvey metoden i SurveyRun klassen . Lägg till följande kod i SurveyRun klassen :

private List<SurveyResponse>? respondents;
public void PerformSurvey(int numberOfRespondents)
{
    int respondentsConsenting = 0;
    respondents = new List<SurveyResponse>();
    while (respondentsConsenting < numberOfRespondents)
    {
        var respondent = SurveyResponse.GetRandomId();
        if (respondent.AnswerSurvey(surveyQuestions))
            respondentsConsenting++;
        respondents.Add(respondent);
    }
}

Även här anger ditt val av ett null-värde List<SurveyResponse>? att svaret kan vara null. Det tyder på att undersökningen inte har getts till några respondenter ännu. Observera att respondenterna läggs till tills tillräckligt många har samtyckt.

Det sista steget för att köra undersökningen är att lägga till ett anrop för att utföra undersökningen i slutet av Main metoden:

surveyRun.PerformSurvey(50);

Granska undersökningssvar

Det sista steget är att visa undersökningsresultat. Du lägger till kod i många av de klasser som du har skrivit. Den här koden visar värdet för att särskilja referenstyper som kan ha värdet null och som inte kan ha värdet null. Börja med att lägga till följande två uttryckskroppsmedlemmar i SurveyResponse klassen :

public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";

Eftersom surveyResponses är en referenstyp som kan ha värdet null är null-kontroller nödvändiga innan de refererar till den. Metoden Answer returnerar en icke-nullbar sträng, så vi måste täcka fallet med ett svar som saknas med operatorn null-coalescing.

Lägg sedan till dessa tre uttryckskroppsmedlemmar i SurveyRun klassen :

public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];

Medlemmen AllParticipants måste ta hänsyn till att variabeln respondents kan vara null, men returvärdet får inte vara null. Om du ändrar uttrycket genom att ta bort ?? och den tomma sekvensen som följer varnar kompilatorn dig om att metoden kan returnera null och dess retursignatur returnerar en typ som inte kan ha värdet null.

Lägg slutligen till följande loop längst ned i Main metoden:

foreach (var participant in surveyRun.AllParticipants)
{
    Console.WriteLine($"Participant: {participant.Id}:");
    if (participant.AnsweredSurvey)
    {
        for (int i = 0; i < surveyRun.Questions.Count; i++)
        {
            var answer = participant.Answer(i);
            Console.WriteLine($"\t{surveyRun.GetQuestion(i).QuestionText} : {answer}");
        }
    }
    else
    {
        Console.WriteLine("\tNo responses");
    }
}

Du behöver null inga kontroller i den här koden eftersom du har utformat de underliggande gränssnitten så att alla returnerar referenstyper som inte kan ha värdet null.

Hämta koden

Du kan hämta koden för den färdiga självstudien från vår exempellagringsplats i mappen csharp/NullableIntroduction .

Experimentera genom att ändra typdeklarationerna mellan referenstyper som kan ha värdet null och som inte kan ha värdet null. Se hur det genererar olika varningar för att se till att du inte av misstag avreferleder en null.

Nästa steg

Lär dig hur du använder en referenstyp som kan ha värdet null när du använder Entity Framework: