Návod: Vyjádřete záměr svého návrhu pomocí nulovatelných a nenulovatelných odkazových typů

Tip

Začínáte s referenčními typy s možnou hodnotou null? Nejprve si přečtěte odkazové typy s možnou hodnotou null. V tomto kurzu se předpokládá, že rozumíte rozdílu mezi nenulovými a nullable referenčními typy a tomu, jak kompilátor sleduje stav hodnoty null.

Pochází z jiného jazyka? Pokud jste se setkali s nullable typy v Kotlinu, strictNullChecks v TypeScriptu nebo volitelnými typy ve Swiftu, konceptuální model odpovídá přímo. Toto cvičení se týká vyjádření záměru návrhu, nikoli učení syntaxe.

V tomto kurzu vytvoříte malou knihovnu, která modeluje spuštění průzkumu. V datech jsou dva odlišné vzorce, které vám nulovatelné referenční typy umožňují rozlišit:

  • Otázka průzkumu musí být vždy přítomna. Seznam otázek a text každé otázky nikdy nemohou být null.
  • Odpověď na otázku může chybět. Respondenti můžou odmítnout odpovědi na některé nebo všechny otázky a model by měl tento model explicitně vyjádřit.

Tato pravidla deklarujete pomocí nenulových a nulovatelných referenčních typů. Kompilátor pak upozorní vždy, když chování kódu neodpovídá návrhu.

V tomto kurzu se naučíte:

  • Vytvořte aplikaci.
  • Sestavte otázky průzkumu.
  • Vytvořte průzkum otázek.
  • Otestujte požadavek na hodnotu not-null.
  • Sestavte typy odpovědí.
  • Vytvářejte respondenty.
  • Vygenerujte jednu odpověď průzkumu.
  • Vytvořte sadu odpovědí průzkumu.
  • Prozkoumejte výsledky průzkumu.

Průzkum modeluje tři třídy:

  • SurveyQuestion: jedna otázka. Text a typ otázky jsou povinné.
  • SurveyRun: kolekce otázek a seznam respondentů.
  • SurveyResponse: odpovědi jednoho respondenta, které by mohly chybět.

Každý typ používá nenulovatelné odkazové typy pro požadované hodnoty a nulovatelné odkazové typy pro chybějící hodnoty.

Předpoklady

V tomto kurzu se předpokládá, že znáte jazyk C# a Visual Studio nebo rozhraní příkazového řádku .NET.

Vytvořte aplikaci a povolte nullable reference type.

Vytvořte novou konzolovou aplikaci s názvem NullableIntroduction:

dotnet new console -n NullableIntroduction
cd NullableIntroduction

Sestavení otázek průzkumu

Přidejte do projektu nový soubor s názvem SurveyQuestion.cs a nahraďte jeho obsah následujícím kódem. Text ani typ otázky nemohou mít hodnotu null, takže konstruktor musí inicializovat oba:

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

Parametry konstruktoru jsou nenulové odkazové typy, takže kompilátor varuje volajícího, pokud může být nullněkterý z argumentů .

Vytvoření průzkumu otázek

Dále přidejte do projektu nový soubor s názvem SurveyRun.cs a definujte SurveyRun třídu, která bude obsahovat seznam otázek:

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

Pole surveyQuestions je nenulovatelný typ List<SurveyQuestion>. K inicializaci prázdného seznamu používá výraz kolekce . Obě přetížení AddQuestion přijímají nenulové parametry, takže kompilátor zajišťuje, aby volající nepředávali null.

V Program.cs vytvořte SurveyRun a přidejte tři otázky:

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

Otestujte požadavek na nenulovou hodnotu

Pokud chcete zjistit, jak kompilátor vynucuje parametry bez hodnoty null, zkuste přidat následující řádek a znovu sestavit:

surveyRun.AddQuestion(QuestionType.Text, default);

Kompilátor vygeneruje upozornění CS8625, protože default se pro referenční typ vyhodnotí na null a AddQuestion očekává nenulovou hodnotu string. Před pokračováním odstraňte řádek.

Typy odpovědí sestavení

Respondenti můžou odmítnout průzkum, a dokonce i když se účastní, můžou jednotlivé otázky přeskočit. Obě formy chybějících výsledků jsou platné výsledky a systém typů by je měl zviditelnit. Oba formuláře vyjadřujete pomocí výrazu null.

Přidejte do projektu nový soubor s názvem SurveyResponse.cs a definujte SurveyResponse třídu. Pomocí primárního konstruktoru (parametry deklarované pro samotný typ, dostupné v celém těle) zachyťte vždy požadované Idhodnoty:

namespace NullableIntroduction;

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

Vytváření respondentů

Přidejte statickou tovární metodu (metodu static, která vytvoří a vrátí novou instanci daného typu; jde o alternativu k přímému volání konstruktoru), která vytváří respondenty s náhodným ID:

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

Vygenerování jedné odpovědi průzkumu

Dále přidejte metodu, která položí dotazník respondentovi. Uložte odpovědi do nullable slovníku, aby samotný typ komunikoval, že respondent může odmítnout odpovědět:

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

Pole surveyResponses je Dictionary<int, string>?. Pokud dereferencujete pole, aniž byste nejprve zkontrolovali hodnotu null, kompilátor zobrazí upozornění. Uvnitř AnswerSurveykompilátor sleduje, že surveyResponsesnení null hned za výrazem new , takže tělo smyčky nepotřebuje žádnou další kontrolu.

Vytvoření sady odpovědí na průzkum

Přidejte metodu SurveyRun , která sestaví seznam respondentů, dokud nebude dostatek souhlasu k účasti:

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

Pole respondents je List<SurveyResponse>? – je null, dokud se průzkum nespustí.

Zavolejte PerformSurvey z Main:

surveyRun.PerformSurvey(50);

Prozkoumání výsledků průzkumu

Pro hlášení výsledků zpřístupněte několik pomocných funkcí z SurveyResponse a SurveyRun. Do SurveyResponse přidejte členy s tělem tvořeným výrazem (členy definované pomocí => a jediného výrazu namísto bloku { ... }), které zpracovávají slovník, který může být null:

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

AnsweredSurvey zkontroluje pole proti null. Answer používá ?. operátor podmíněného přístupu při hodnotě null (který se vyhodnotí jako null, když má levá strana hodnotu null, místo aby vyvolal výjimku) k bezpečnému dereferencování a ?? operátor slučování s hodnotou null (který dosadí pravý operand, když má levý hodnotu null), aby poskytl nenulovou náhradní hodnotu. Návratový typ metody je nenulovatelný string, takže volající nemusí provádět kontrolu na hodnotu null.

Přidejte SurveyRunčleny s výrazy, které zpřístupňují seznam účastníků a otázek:

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

AllParticipants vrátí nenulovou sekvenci, i když respondents může být null. Operátor ?? nahradí Enumerable.Empty<SurveyResponse>() , když se pole ještě nenaplní. Pokud klauzuli odeberete ?? , kompilátor varuje, že metoda může vrátit null i přes nenulový návratový typ.

Nakonec dole v Main napište zprávu:

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

Všimněte si, že není nutná žádná kontrola hodnoty null pro participant, surveyRun.Questionsnebo surveyRun.GetQuestion(i). Typy deklarují tyto hodnoty jako nenulové, takže kompilátor je považuje za nenulovou v celé smyčce.

Spusťte aplikaci:

dotnet run

Výstup se při každém spuštění liší, protože respondenti jsou generováni náhodně, ale každý řádek buď uvádí odpovědi účastníka, nebo informaci o tom, že odmítl odpovědět.

Závěr

Hotová ukázka je ve složce csharp/NullableIntroduction úložiště dotnet/samples . Experimentujte s přepínáním typů mezi typy připouštějícími hodnotu null a typy nepřipouštějícími hodnotu null. Odebrání ? tam, kde návrh připouští chybějící hodnoty, vyvolá varování kompilátoru, která upozorňují na každé místo, kde na chybějící hodnotě záleží.