Zelfstudie: Druk uw ontwerpbedoeling uit met nullbare en niet-nullbare referentietypen

Tip

Nog niet bekend met nullable-verwijzingstypen? Lees eerst Nullable reference types. In deze zelfstudie wordt ervan uitgegaan dat u het verschil begrijpt tussen niet-nullable en nullable referentietypen en hoe de compiler null-status bijhoudt.

Komt u uit een andere taal? Als u de nullable typen van Kotlin, TypeScript's strictNullChecks of de optionals van Swift hebt gebruikt, komt het conceptuele model direct overeen. De oefening hier gaat over het uitdrukken van ontwerpintenties, niet over het leren van de syntaxis.

In deze tutorial bouwt u een kleine bibliotheek die het afnemen van een enquête modelleert. De gegevens hebben twee verschillende patronen waarmee u onderscheid kunt maken tussen nullable-verwijzingstypen:

  • Een enquêtevraag moet altijd aanwezig zijn. De lijst met vragen en de tekst van elke vraag kan nooit zijn null.
  • Er ontbreekt mogelijk een antwoord op een vraag . Respondenten kunnen weigeren om enkele of alle vragen te beantwoorden en het model moet dat expliciet maken.

Je declareert die regels met niet-nullbare en nullbare referentietypen. De compiler waarschuwt vervolgens wanneer het gedrag van de code niet overeenkomt met het ontwerp.

In deze handleiding leert u:

  • Maak de toepassing.
  • Stel de enquêtevragen op.
  • Bouw een enquête met vragen.
  • Test de niet-nullvereiste.
  • Antwoordtypen bouwen.
  • Maak respondenten.
  • Eén enquêteantwoord genereren.
  • Maak een set enquêteantwoorden.
  • Bekijk de enquêteresultaten.

In drie klassen wordt de enquête gemodelleerd:

  • SurveyQuestion: één vraag. De tekst en het vraagtype zijn vereist.
  • SurveyRun: de verzameling vragen plus de lijst met respondenten.
  • SurveyResponse: de antwoorden van één respondent, die mogelijk ontbreekt.

Voor elk type worden niet-null-referentietypen gebruikt voor vereiste waarden en null-referentietypen voor ontbrekende waarden.

Prerequisites

In deze zelfstudie wordt ervan uitgegaan dat u bekend bent met C# en Visual Studio of de .NET CLI.

De toepassing maken en nullable referentietypen inschakelen

Maak een nieuwe consoletoepassing met de naam NullableIntroduction:

dotnet new console -n NullableIntroduction
cd NullableIntroduction

De enquêtevragen maken

Voeg een nieuw bestand toe met de naam SurveyQuestion.cs aan het project en vervang de inhoud door de volgende code. De tekst en het vraagtype kunnen niet null worden, dus de constructor moet beide initialiseren:

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

De parameters van de constructor zijn niet-nullbare referentietypen, dus de compiler waarschuwt de aanroepende code als een van beide argumenten mogelijk null is.

Een enquête met vragen maken

Voeg vervolgens een nieuw bestand toe met de naam SurveyRun.cs aan het project en definieer een SurveyRun klasse voor het opslaan van de lijst met vragen:

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

Het surveyQuestions-veld is een niet-nullbare List<SurveyQuestion>. Er wordt een verzamelingsexpressie gebruikt om een lege lijst te initialiseren. Beide AddQuestion overloads accepteren niet-nullbare parameters, dus de compiler dwingt af dat aanroepers geen null doorgeven.

Maak in Program.cs een SurveyRun en voeg drie vragen toe:

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

De not-nullvereiste testen

Als u wilt zien hoe de compiler niet-nullbare parameters afdwingt, voegt u de volgende regel toe en compileert u opnieuw:

surveyRun.AddQuestion(QuestionType.Text, default);

De compiler geeft waarschuwing CS8625 omdat default wordt geëvalueerd als null bij een referentietype en AddQuestion een niet-nullbare string verwacht. Verwijder de regel alvorens verder te gaan.

Antwoordtypen bouwen

Respondenten kunnen weigeren de enquête te nemen en zelfs wanneer ze deelnemen, kunnen ze afzonderlijke vragen overslaan. Beide vormen van 'ontbrekend' zijn geldige resultaten en het typesysteem moet ze zichtbaar maken. U drukt beide formulieren uit met null.

Voeg een nieuw bestand toe met de naam SurveyResponse.cs aan het project en definieer een SurveyResponse klasse. Gebruik een primaire constructor (parameters die zijn gedeclareerd voor het type zelf, beschikbaar in de hoofdtekst) om de altijd vereiste Idwaarden vast te leggen:

namespace NullableIntroduction;

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

Respondenten maken

Voeg een statische factory-methode toe (een static methode die een nieuw exemplaar van het type maakt en retourneert, een alternatief voor het rechtstreeks aanroepen van de constructor) waarmee respondenten met een willekeurige id worden gemaakt:

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

Eén enquêteantwoord genereren

Voeg vervolgens de methode toe waarmee de enquête wordt gevraagd aan een respondent. Sla de antwoorden op in een nullable woordenlijst, zodat het type zelf communiceert dat de respondent kan weigeren:

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

Het surveyResponses veld is Dictionary<int, string>?. Als u het veld dereferereert zonder eerst op null te controleren, geeft de compiler een waarschuwing. Binnen AnswerSurvey houdt de compiler bij dat surveyResponses direct na de expressie newniet null is, zodat de hoofdtekst van de lus geen extra controle vereist.

Een set enquêteantwoorden maken

Voeg een methode SurveyRun toe waarmee een lijst met respondenten wordt opgebouwd totdat er voldoende toestemming is om deel te nemen:

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

Het respondents-veld is List<SurveyResponse>? - het is null totdat de enquête wordt uitgevoerd.

Opbellen PerformSurvey van Main:

surveyRun.PerformSurvey(50);

De enquêteresultaten bekijken

Om resultaten te rapporteren, stelt u enkele hulpfuncties beschikbaar vanuit SurveyResponse en SurveyRun. Voeg in SurveyResponseleden met expressietekst toe (leden die zijn gedefinieerd met => en één expressie in plaats van een { ... }-blok) die de nullable dictionary verwerken:

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

AnsweredSurvey controleert het veld op null. Answer gebruikt de ?. null-conditionele operator (die resulteert in null wanneer de linkerkant null is, in plaats van een uitzondering te genereren) om veilig te derefereren, en de ?? null-samenvoegingsoperator (die de rechteroperand gebruikt wanneer de linkerkant null is) om een alternatief te bieden dat niet null is. Het retourtype van de methode is niet null-compatibel string, dus bellers hebben geen null-controles nodig.

Voeg aan SurveyRun leden met expressietekst toe die de lijst met deelnemers en vragen blootleggen:

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

AllParticipants retourneert een niet-nulleerbare reeks, ook al respondents kan dat wel zijn null. De ?? operator vervangt Enumerable.Empty<SurveyResponse>() wanneer het veld nog niet is ingevuld. Als u de ?? component verwijdert, waarschuwt de compiler dat de methode mogelijk retourneert null ondanks een niet-nullable retourtype.

Schrijf ten slotte het rapport onderaan Main:

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

U ziet dat er geen null-controle nodig is voor participant, surveyRun.Questionsof surveyRun.GetQuestion(i). De typen declareren deze waarden als niet-nullbaar, dus de compiler behandelt ze als niet-null in de lus.

Voer de toepassing uit:

dotnet run

De uitvoer verschilt bij elke uitvoering omdat deelnemers willekeurig worden gegenereerd, maar elke regel geeft ofwel de antwoorden van een deelnemer weer, ofwel aan dat die niet wilde antwoorden.

Conclusion

Het voltooide voorbeeld bevindt zich in de map csharp/NullableIntroduction van de opslagplaats dotnet/samples . Experimenteer door te wisselen tussen nullbare en niet-nullbare typen. Als u een ? verwijdert in een ontwerp waarin ontbrekende waarden zijn toegestaan, ontstaan compilerwaarschuwingen die wijzen op elke plek waar de ontbrekende waarden van belang zijn.