Självstudie: Uttryck din designavsikt med nullbara och icke-nullbara referenstyper

Tip

Ny på nullbara referenstyper? Läs nullbara referenstyper först. Den här självstudiekursen förutsätter att du förstår skillnaden mellan icke-nullbara och nullbara referenstyper och hur kompilatorn spårar nullstatus.

Kommer du från ett annat språk? Om du har använt Kotlin:s nullable typer, TypeScript:s strictNullChecks eller Swift:s optionals, motsvarar den konceptuella modellen direkt. Övningen här handlar om att uttrycka designavsikter, inte lära sig syntaxen.

I den här självstudien skapar du ett litet bibliotek som modellerar hur man genomför en enkät. Datan har två distinkta mönster som nullbara referenstyper låter dig skilja mellan:

  • En undersökningsfråga måste alltid finnas. Listan med frågor och texten i varje fråga kan aldrig vara null.
  • Ett svar på en fråga kanske saknas. Respondenter kan avböja att svara på vissa eller alla frågor, och modellen bör göra det explicit.

Du deklarerar dessa regler med icke-nullbara och nullbara referenstyper. Kompilatorn varnar sedan när kodens beteende inte matchar designen.

I den här handledningen kommer du att:

  • Skapa programmet.
  • Skapa enkätfrågorna.
  • Skapa en undersökning av frågor.
  • Testa kravet att ett värde inte får vara null.
  • Skapa svarstyper.
  • Skapa respondenter.
  • Generera ett undersökningssvar.
  • Skapa en uppsättning enkätsvar.
  • Granska undersökningsresultaten.

Tre klasser modellerar undersökningen:

  • SurveyQuestion: en fråga. Texten och frågetypen är obligatoriska.
  • SurveyRun: samling frågor plus listan över svarande.
  • SurveyResponse: en svarandes svar, som kanske saknas.

Varje typ använder icke-nullbara referenstyper för obligatoriska värden och nullbara referenstyper för saknade värden.

Förutsättningar

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

Skapa programmet och aktivera null-referenstyper

Skapa ett nytt konsolprogram med namnet NullableIntroduction:

dotnet new console -n NullableIntroduction
cd NullableIntroduction

Skapa enkätfrågorna

Lägg till en ny fil med namnet SurveyQuestion.cs i projektet och ersätt dess innehåll med följande kod. Texten och frågetypen kan inte vara null, så konstruktorn måste initiera båda:

namespace NullableIntroduction;

public enum QuestionType
{
    YesNo,
    Number,
    Text
}

public class SurveyQuestion(QuestionType typeOfQuestion, string text)
{
    public string QuestionText { get; } = text;
    public QuestionType TypeOfQuestion { get; } = typeOfQuestion;
}

Konstruktorparametrarna är icke-nullbara referenstyper, så kompilatorn varnar anroparen om något av argumenten kan vara null.

Skapa en undersökning av frågor

Lägg sedan till en ny fil med namnet SurveyRun.cs i projektet och definiera en SurveyRun klass som innehåller listan med frågor:

namespace NullableIntroduction;

public class SurveyRun
{
    private List<SurveyQuestion> surveyQuestions = [];

    public void AddQuestion(QuestionType type, string question) =>
        AddQuestion(new SurveyQuestion(type, question));

    public void AddQuestion(SurveyQuestion surveyQuestion) =>
        surveyQuestions.Add(surveyQuestion);
}

Fältet surveyQuestions är en icke-nullbar List<SurveyQuestion>. Det använder ett samlingsuttryck för att initiera en tom lista. Båda AddQuestion överlagringarna accepterar icke-nullbara parametrar, så kompilatorn framtvingar att anropare inte skickar null.

I Program.cs, skapar du SurveyRun och lägger till tre frågor:

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?");

Testa kravet som inte är null

Om du vill se hur kompilatorn framtvingar icke-nullbara parametrar kan du prova att lägga till följande rad och återskapa:

surveyRun.AddQuestion(QuestionType.Text, default);

Kompilatorn ger varningen CS8625 eftersom default utvärderas till null för en referenstyp och AddQuestion förväntar sig en icke-nullbar string. Ta bort raden innan du fortsätter.

Skapa svarstyper

Respondenter kan avböja att delta i undersökningen, och även när de deltar kan de hoppa över enskilda frågor. Båda formerna av "saknas" är giltiga resultat och typsystemet bör göra dem synliga. Du uttrycker båda formulären med null.

Lägg till en ny fil med namnet SurveyResponse.cs i projektet och definiera en SurveyResponse klass. Använd en primär konstruktor (parametrar som deklareras på själva typen, tillgängliga i hela brödtexten) för att samla in den alltid nödvändiga Id:

namespace NullableIntroduction;

public class SurveyResponse(int id)
{
    public int Id { get; } = id;
}

Skapa respondenter

Lägg till en statisk fabriksmetod (en static metod som skapar och returnerar en ny instans av typen, ett alternativ till att anropa konstruktorn direkt) som skapar respondenter med ett slumpmässigt ID:

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

Generera ett undersökningssvar

Lägg sedan till metoden som ställer undersökningen till en respondent. Lagra svaren i en nullable-ordlista så att själva typen signalerar att den som svarar kan välja att avstå:

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

Fältet surveyResponses är Dictionary<int, string>?. Om du avreferera fältet utan att först söka efter nullutfärdar kompilatorn en varning. I AnswerSurvey vet kompilatorn att surveyResponses är inte null direkt efter uttrycket new, så loopens innehåll behöver ingen ytterligare kontroll.

Skapa en uppsättning undersökningssvar

Lägg till en metod på SurveyRun som bygger upp en lista med respondenter tills tillräckligt många samtycker till att delta:

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

Fältet respondents är List<SurveyResponse>? – det är null tills undersökningen körs.

Ring PerformSurvey från Main:

surveyRun.PerformSurvey(50);

Granska undersökningsresultaten

För att rapportera resultat, gör några hjälpfunktioner från SurveyResponse och SurveyRun tillgängliga. I SurveyResponse lägger du till uttrycksdefinierade medlemmar (medlemmar som definieras med => och ett enda uttryck i stället för ett { ... }-block) som hanterar den nullbara ordlistan:

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

AnsweredSurvey kontrollerar fältet mot null. Answer använder ?. nullvillkorsoperatorn (som utvärderas till null när den vänstra sidan är null i stället för att utlösa ett undantag) för säker dereferering, och ?? nullkoalescensoperatorn (som ersätter med den högra operanden när den vänstra är null) för att ge ett reservvärde som inte är null. Metodens returtyp är inte nullbar string, så anropare behöver inte null-kontroller.

SurveyRunlägger du till uttrycksbaserade medlemmar som visar listan över deltagare och frågor:

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

AllParticipants returnerar en icke-nullbar sekvens även om respondents den kan vara null. Operatorn ?? ersätter Enumerable.Empty<SurveyResponse>() när fältet inte har fyllts i ännu. Om du tar bort ??-satsen varnar kompilatorn för att metoden kan returnera null trots en icke-nullbar returtyp.

Skriv slutligen rapporten längst ned Maini :

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

Observera att ingen null-kontroll krävs för participant, surveyRun.Questionseller surveyRun.GetQuestion(i). Typerna deklarerar dessa värden som icke-nullbara, så kompilatorn behandlar dem som inte null i hela loopen.

Kör programmet:

dotnet run

Utdata skiljer sig mellan körningarna eftersom respondenterna genereras slumpmässigt, men varje rad redovisar antingen en deltagares svar eller anger att personen avböjde att svara.

Conclusion

Det färdiga exemplet finns i mappen csharp/NullableIntroductionlagringsplatsen dotnet/samples . Experimentera med att ändra typer mellan nullbara och icke-nullbara. Om du tar bort en ? plats där designen tillåter saknade värden genereras kompilatorvarningar som pekar på varje plats där det saknade värdet är viktigt.