Kurz: Srozumitelnější vyjádření záměru návrhu pomocí referenčních typů s možnou a nenulovou hodnotou

Odkazové typy s možnou hodnotou null doplňují odkazové typy stejným způsobem, jako typy hodnot s možnou hodnotou null doplňují typy hodnot. Proměnnou deklarujete jako odkazový typ s možnou hodnotou null připojením ? typu . string? Například představuje hodnotu nullable string. Tyto nové typy můžete použít k jasnějšímu vyjádření záměru návrhu: některé proměnné musí mít vždy hodnotu, jiné můžou hodnotu chybět.

V tomto kurzu se naučíte:

  • Začlenit do návrhů odkazové typy s možnou a nenulovou hodnotou
  • Povolte kontroly typu odkazu s možnou hodnotou null v celém kódu.
  • Napište kód, ve kterém kompilátor vynucuje tato rozhodnutí o návrhu.
  • Použití funkce odkazu s možnou hodnotou null ve vlastních návrzích

Požadavky

Budete muset nastavit počítač tak, aby spouštět .NET, včetně kompilátoru jazyka C#. Kompilátor jazyka C# je k dispozici v sadě Visual Studio 2022 nebo v sadě .NET SDK.

Tento kurz předpokládá, že znáte jazyk C# a .NET, včetně sady Visual Studio nebo .NET CLI.

Začlenit do návrhů odkazové typy s možnou hodnotou null

V tomto kurzu vytvoříte knihovnu, která modeluje spouštění průzkumu. Kód k reprezentaci reálných konceptů používá odkazové typy s možnou hodnotou null i odkazové typy s možnou hodnotou null. Otázky průzkumu nesmí mít nikdy hodnotu null. Respondent by mohl chtít na otázku neodpovídat. Odpovědi můžou být null v tomto případě.

Kód, který napíšete pro tuto ukázku, vyjadřuje tento záměr a kompilátor tento záměr vynutí.

Vytvořte aplikaci a povolte odkazové typy s možnou hodnotou null.

Vytvořte novou konzolovou aplikaci v sadě Visual Studio nebo z příkazového řádku pomocí dotnet new console. Pojmenujte aplikaci NullableIntroduction. Po vytvoření aplikace budete muset určit, že se celý projekt zkompiluje v povoleném kontextu poznámek s možnou hodnotou null. Otevřete soubor .csproj a přidejte do elementu NullablePropertyGroup element. Nastavte její hodnotu na enable. V projektech starších než C# 11 musíte vyjádřit výslovný souhlas s funkcí typů odkazů s možnou hodnotou null . Je to proto, že jakmile je funkce zapnutá, z existujících deklarací referenčních proměnných se stanou odkazové typy bez hodnoty null. I když toto rozhodnutí pomůže najít problémy, kdy stávající kód nemusí mít správnou kontrolu hodnot null, nemusí přesně odrážet váš původní záměr návrhu:

<Nullable>enable</Nullable>

Před .NET 6 nové projekty neobsahují Nullable element . Počínaje .NET 6 nové projekty obsahují <Nullable>enable</Nullable> element v souboru projektu.

Návrh typů pro aplikaci

Tato aplikace průzkumu vyžaduje vytvoření řady tříd:

  • Třída, která modeluje seznam otázek.
  • Třída, která modeluje seznam lidí kontaktovaných pro průzkum.
  • Třída, která modeluje odpovědi od osoby, která se průzkumu zúčastnila.

Tyto typy budou používat odkazové typy s možnou i nenulovou hodnotou k vyjádření toho, které členy jsou povinné a které členy jsou volitelné. Odkazové typy s možnou hodnotou null sdělují záměr návrhu jasně:

  • Otázky, které jsou součástí průzkumu, nesmí mít nikdy hodnotu null: Nemá smysl klást prázdnou otázku.
  • Respondenti nemohou mít nikdy hodnotu null. Budete chtít sledovat lidi, které jste kontaktovali, dokonce i respondenty, kteří odmítli účast.
  • Jakákoli odpověď na otázku může být null. Respondenti můžou odmítnout odpovědět na některé nebo všechny otázky.

Pokud jste programovali v jazyce C#, možná jste tak zvyklí na odkazové typy, které povolují null hodnoty, že jste možná propásli další příležitosti k deklaraci instancí, které nemají hodnotu null:

  • Kolekce otázek by neměla mít hodnotu null.
  • Kolekce respondentů by neměla mít hodnotu null.

Při psaní kódu uvidíte, že odkaz typu, který nemůže mít hodnotu null jako výchozí pro odkazy, se vyhne běžným chybám, které by mohly vést k NullReferenceExceptions. Jedním z poučení z tohoto kurzu je, že jste se rozhodli, které proměnné by mohly nebo nemohly být null. Jazyk neposkytl syntaxi k vyjádření těchto rozhodnutí. Teď už ano.

Aplikace, kterou vytvoříte, provede následující kroky:

  1. Vytvoří průzkum a přidá do něj otázky.
  2. Vytvoří pseudonáhodnou sadu respondentů pro průzkum.
  3. Kontaktuje respondenty, dokud velikost dokončeného průzkumu nedosáhne cílového čísla.
  4. Zapisuje důležité statistiky o odpovědích na průzkum.

Sestavení průzkumu s odkazovými typy s možnou a nenulovou hodnotou null

První kód, který napíšete, vytvoří průzkum. Napíšete třídy, které modelují otázku průzkumu a spuštění průzkumu. Průzkum obsahuje tři typy otázek, které se liší formátem odpovědi: odpovědi Ano/Ne, číselné odpovědi a textové odpovědi. Vytvořte public SurveyQuestion třídu:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Kompilátor interpretuje každou deklaraci proměnné referenčního typu jako odkazový typ, který nesmí mít hodnotu null pro kód v povoleném kontextu poznámky s možnou hodnotou null. První upozornění můžete zobrazit přidáním vlastností pro text otázky a typ otázky, jak je znázorněno v následujícím kódu:

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

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

Protože jste neinicializovali QuestionText, kompilátor zobrazí upozornění, že nebyla inicializována vlastnost s možnou hodnotou null. Návrh vyžaduje, aby text otázky byl jiný než null, takže přidáte konstruktor, který ho inicializuje, a QuestionType také hodnotu. Hotová definice třídy vypadá jako následující kód:

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

Přidání konstruktoru upozornění odebere. Argument konstruktoru je také odkaz s možnou hodnotou null, takže kompilátor nevystavuje žádná upozornění.

Dále vytvořte public třídu s názvem SurveyRun. Tato třída obsahuje seznam SurveyQuestion objektů a metod pro přidání otázek do průzkumu, jak je znázorněno v následujícím kódu:

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

Stejně jako předtím musíte inicializovat objekt seznamu na hodnotu, která není null, jinak kompilátor vydá upozornění. V druhém přetížení AddQuestion nejsou žádné kontroly hodnot null, protože nejsou potřeba: Deklarovali jste tuto proměnnou jako nenulovou. Jeho hodnota nemůže být null.

V editoru přepněte na Program.cs a nahraďte obsah Main souboru následujícími řádky kódu:

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

Vzhledem k tomu, že celý projekt je v povoleném kontextu poznámek s možnou hodnotou null, zobrazí se upozornění, když předáte null libovolné metodě, která očekává odkazový typ, který nemůže mít hodnotu null. Zkuste to přidáním následujícího řádku do Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Vytvoření respondentů a získání odpovědí na průzkum

Dále napište kód, který vygeneruje odpovědi na průzkum. Tento proces zahrnuje několik malých úloh:

  1. Vytvořte metodu, která generuje objekty respondenta. Jedná se o lidi, kteří jsou požádáni o vyplnění průzkumu.
  2. Vytvořte logiku, která simuluje kladení otázek respondentovi a shromažďování odpovědí nebo povedení, že respondent neodpověděl.
  3. Tento postup opakujte, dokud na průzkum neodpoví dostatečný počet respondentů.

Budete potřebovat třídu, která bude představovat odpověď na průzkum, takže ji teď přidejte. Povolte podporu s možnou hodnotou null. Id Přidejte vlastnost a konstruktor, který ji inicializuje, jak je znázorněno v následujícím kódu:

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

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

Dále přidejte metodu static pro vytvoření nových účastníků vygenerováním náhodného ID:

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

Hlavní zodpovědností této třídy je vygenerovat odpovědi pro účastníka na otázky v průzkumu. Tato odpovědnost zahrnuje několik kroků:

  1. Požádejte o účast v průzkumu. Pokud tato osoba neodsouhlasí, vraťte chybějící odpověď (nebo odpověď s hodnotou null).
  2. Položte každou otázku a zaznamenejte odpověď. Každá odpověď může také chybět (nebo mít hodnotu null).

Do třídy SurveyResponse přidejte následující kód:

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

Úložiště pro odpovědi průzkumu je , Dictionary<int, string>?což znamená, že může mít hodnotu null. Funkci nového jazyka používáte k deklaraci záměru návrhu, a to jak kompilátoru, tak i komukoli, kdo si později kód přečte. Pokud někdy použijete dereference surveyResponses bez toho, abyste nejprve zkontrolovali null hodnotu, zobrazí se upozornění kompilátoru. V metodě se nezobrazí upozornění AnswerSurvey , protože kompilátor může zjistit surveyResponses , že proměnná byla nastavena na hodnotu, která není null výše.

Použití null chybějících odpovědí zvýrazňuje klíčový bod pro práci s typy odkazů s možnou hodnotou null: vaším cílem není odebrat z programu všechny null hodnoty. Vaším cílem je spíše zajistit, aby kód, který napíšete, vyjadřoval záměr vašeho návrhu. Chybějící hodnoty jsou nezbytným konceptem, který je potřeba vyjádřit v kódu. Hodnota null představuje jasný způsob, jak tyto chybějící hodnoty vyjádřit. Pokus o odebrání všech null hodnot vede pouze k definování jiného způsobu, jak tyto chybějící hodnoty vyjádřit bez null.

Dále musíte napsat metodu PerformSurvey ve SurveyRun třídě . Do třídy přidejte následující kód SurveyRun :

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

I tady platí, že volba parametru nullable List<SurveyResponse>? značí, že odpověď může být null. To znamená, že průzkum ještě nebyl zadán žádnému respondentovi. Všimněte si, že respondenti se přidávají, dokud nebudou mít dostatek souhlasu.

Posledním krokem ke spuštění průzkumu je přidání volání pro provedení průzkumu na konci Main metody:

surveyRun.PerformSurvey(50);

Prozkoumání odpovědí na průzkum

Posledním krokem je zobrazení výsledků průzkumu. Do mnoha tříd, které jste napsali, přidáte kód. Tento kód ukazuje hodnotu rozlišování typů odkazů s možnou a nenulovou hodnotou. Začněte tím, že do třídy přidáte následující dva členy s výrazem SurveyResponse :

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

Vzhledem k tomu surveyResponses , že odkaz může být typu s možnou hodnotou null, je nutné před zrušením odkazu provést kontrolu hodnoty null. Metoda Answer vrací řetězec, který nemůže mít hodnotu null, takže případ chybějící odpovědi musíme pokrýt pomocí operátoru nulového sjednocení.

Dále do třídy přidejte tyto tři členy s výrazem SurveyRun :

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

Člen AllParticipants musí vzít v úvahu, že respondents proměnná může mít hodnotu null, ale vrácená hodnota nemůže být null. Pokud tento výraz změníte odebráním ?? a prázdné sekvence, která následuje, kompilátor vás upozorní, že metoda může vrátit null a její návratový podpis vrátí typ, který nelze null.

Nakonec do dolní části metody přidejte následující smyčku 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");
    }
}

V tomto kódu nepotřebujete žádné null kontroly, protože jste navrhli podkladová rozhraní tak, aby všechna vracela odkazové typy s možnou hodnotou null.

Získání kódu

Kód pro dokončený kurz můžete získat z našeho úložiště ukázek ve složce csharp/NullableIntroduction .

Experimentujte změnou deklarací typu mezi odkazovými typy s možnou a nenulovou hodnotou. Podívejte se, jak se generují různá upozornění, abyste se ujistili, že omylem nepřesunete odkaz na null.

Další kroky

Naučte se používat odkazový typ s možnou hodnotou null při použití Entity Frameworku: