Sdílet prostřednictvím


Návod: Vyjádřete svůj návrhový záměr jasněji pomocí nulovatelných a nenulovatelných referenčních typů

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

V tomto návodu se naučíte, jak:

  • Začlenit nulovatelné a nenulovatelné referenční typy do návrhů
  • Povolte kontroly nulovatelného typu odkazu v celém kódu.
  • Napište kód, ve kterém kompilátor vynucuje tato rozhodnutí o návrhu.
  • Použijte funkci nulovatelné reference ve vlastních návrzích

Požadavky

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

Začlenění referenčních typů s možnou hodnotou null do návrhů

V tomto kurzu vytvoříte knihovnu, která modeluje spuštění průzkumu. Kód používá nulovatelné odkazové typy i nenulovatelné odkazové typy k reprezentaci skutečných konceptů. Otázky průzkumu nemohou mít nikdy hodnotu null. Respondent nemusí chtít odpovědět na otázku. V tomto případě mohou být odpovědi null.

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

Vytvořte aplikaci a povolte nullable reference type.

Vytvořte novou konzolovou aplikaci v sadě Visual Studio nebo z příkazového řádku pomocí dotnet new console. Pojmenujte aplikaci NullableIntroduction. Jakmile vytvoříte aplikaci, musíte určit, že se celý projekt zkompiluje v povoleném kontextu poznámek s možnou hodnotou null. Otevřete soubor .csproj a do elementu Nullable přidejte prvek PropertyGroup. Nastavte jeho hodnotu na enable. V projektech vytvořených před C# 11 / .NET 7 musíte vyjádřit výslovný souhlas s funkcí odkazových typů s možnou hodnotou null . Jakmile je tato funkce zapnutá, stávající deklarace odkazových proměnných se stanou nenulovými odkazovými typy. I když toto rozhodnutí pomáhá najít problémy, kdy stávající kód nemusí mít správné kontroly null, nemusí přesně odrážet původní záměr návrhu:

<Nullable>enable</Nullable>

Návrh typů pro aplikaci

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

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

Tyto typy používají odkazové typy s možnou hodnotou null i nenulové odkazy k vyjádření požadovaných členů a nepovinných členů. Odkazové typy s možnou hodnotou null jasně komunikují o záměru návrhu:

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

Můžete být tak zvyklí na odkazové typy, které umožňují hodnoty null, že byste mohli přehlédnout jiné příležitosti k deklaraci nenulovatelných instancí:

  • Kolekce otázek by měla být nenulová.
  • Kolekce respondentů by měla být nenulovatelná.

Při psaní kódu vidíte, že typ odkazu bez hodnoty null jako výchozí pro odkazy zabraňuje běžným chybám, které by mohly vést k NullReferenceExceptionchybám. Jednou z lekcí tohoto kurzu je, že jste se rozhodli, které proměnné by mohly nebo nemohly být null. Jazyk neposkytoval syntaxi pro vyjádření těchto rozhodnutí. Teď to funguje.

Aplikace, kterou sestaví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 dokončená velikost průzkumu nedosáhne cílového čísla.
  4. Zapíše důležité statistiky o odpovědích na průzkum.

Sestavení průzkumu s nullovatelnými a nenullovatelnými odkazovými typy

První kód, který napíšete, vytvoří průzkum. Napíšete třídy pro modelování otázky průzkumu a provedení průzkumu. Průzkum má tři typy otázek, odlišují se podle formátu odpovědi: Ano/Ne odpovědi, číselné odpovědi a textové odpovědi. Vytvořte třídu public SurveyQuestion:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Kompilátor interpretuje každou deklaraci proměnné referenčního typu jako nenulový referenční typ pro kód v kontextu povolených anotací s hodnotou null. První upozornění můžete zobrazit přidáním vlastností textu otázky a typu 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 vlastnost, která nesmí být null, nebyla inicializována. Návrh vyžaduje, aby text otázky byl nenulový, takže přidáte konstruktor, který ho inicializuje a také hodnotu QuestionType. 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 odebere upozornění. Argument konstruktoru je také nenulový odkazový typ, takže kompilátor nevystavuje žádná upozornění.

Dále vytvořte třídu public 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 nebo kompilátor vydá upozornění. Druhé přetížení AddQuestion neobsahuje žádné kontroly null, protože kompilátor pomáhá vynucovat nenulový kontrakt: Deklarovali jste, že proměnná má být nenulová. Zatímco kompilátor varuje před potenciálními přiřazeními null, hodnoty null modulu runtime jsou stále možné. U veřejných rozhraní API zvažte, že přidáte ověřování argumentů i pro odkazové typy nepovolující null, protože klientský kód nemusí mít povolené typy odkazů s možnou hodnotou null nebo klientský kód může záměrně předat hodnotu null.

Přepněte do Program.cs v editoru a nahraďte obsah Main 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 se nachází v kontextu povolených poznámek o nullovatelnosti, zobrazí se varování, když předáte null jakékoli metodě, která očekává nenulový referenční typ. Zkuste to přidáním následujícího řádku do Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Vytvářejte respondenty a získejte odpovědi na průzkum.

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

  1. Vytvořte metodu, která generuje objekty respondenta. Tyto objekty představují osoby, které jsou požádány o vyplnění průzkumu.
  2. Sestavte logiku pro simulaci kladení otázek respondentovi a shromažďování odpovědí nebo zaznamenání, že respondent neodpověděl.
  3. Opakujte, dokud neodpoví dostatečný počet respondentů na průzkum.

Potřebujete třídu, která bude reprezentovat odpověď průzkumu, takže ji přidejte. Povolte podporu s možnou hodnotou null. Přidejte vlastnost Id 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í odpovědností této třídy je vygenerovat odpovědi účastníka na otázky v průzkumu. Tato odpovědnost má několik kroků:

  1. Požádejte o účast v průzkumu. Pokud osoba nesouhlasí, vraťte chybějící (nebo nulovou) odpověď.
  2. Položte každou otázku a nahrajte odpověď. Každá odpověď může také chybět (nebo 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ě odpovědí na průzkum je , Dictionary<int, string>?což znamená, že může mít hodnotu null. Pomocí nové funkce jazyka deklarujete záměr návrhu, a to jak kompilátoru, tak každému, kdo kód později přečte. Pokud někdy dojde k dereference surveyResponses bez kontroly null hodnoty jako první, zobrazí se upozornění kompilátoru. V metodě AnswerSurvey se nezobrazí upozornění, protože kompilátor může určit, že proměnná surveyResponses byla nastavena na hodnotu, která není null, v předchozím úseku kódu.

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

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

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

V této části můžete znovu zvolit hodnotu nullable List<SurveyResponse>? , což znamená, že odpověď může mít hodnotu null. To znamená, že průzkum zatím nebyl udělen žádným respondentům. Všimněte si, že respondenti jsou přidáváni, dokud není dosaženo dostatečného souhlasu.

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

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šení mezi nulovatelnými a nenulovatelnými odkazy. Začněte přidáním následujících dvou členů, využívajících výrazové tělo, do třídy SurveyResponse:

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

Vzhledem k tomu, že surveyResponses je typ odkazu s možnou hodnotou null, jsou před zrušením odkazu nutné kontroly null. Metoda Answer vrátí nenulový řetězec, takže musíme pokrýt případ chybějící odpovědi pomocí operátoru null-coalescing.

Dále přidejte tyto tři členy s výrazovým tělem do třídy 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 proměnná respondents může mít hodnotu null, ale návratová 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 jeho návratový podpis vrátí nenulový typ.

Nakonec do dolní části metody Main přidejte následující smyčku:

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 základní rozhraní tak, aby všechny vracely odkazové typy bez hodnoty null. Statická analýza kompilátoru pomáhá zajistit, aby se tyto kontrakty návrhu dodržovaly.

Získání kódu

Kód pro dokončený tutoriál získáte z našeho ukázkového repozitáře ve složce csharp/NullableIntroduction.

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

Další kroky

Naučte se používat referenční typ s možnou hodnotou null při použití entity Framework: