Udostępnij za pośrednictwem


Samouczek: Bardziej wyraźne wyrażenie intencji projektowej przy użyciu typów odwołań nullowalnych i nienullowalnych

Typy referencyjne dopuszczające wartości null uzupełniają typy referencyjne w taki sam sposób, jak typy wartości dopuszczające wartość null uzupełniają typy wartości. Deklarujesz zmienną jako typ odwołania dopuszczający wartości null, dołączając ? do typu. Na przykład string? reprezentuje string, który może być wartścią null. Możesz użyć tych nowych typów, aby wyraźniej wyrazić intencję projektowania: niektóre zmienne zawsze muszą mieć wartość, inne mogą nie mieć wartości.

Z tego samouczka dowiesz się, jak wykonywać następujące działania:

  • Włącz typy odwołań dopuszczalne do wartości null i niedopuszczalne do wartości null do swoich projektów
  • Włącz kontrole typów referencyjnych dopuszczających wartość null w całym kodzie.
  • Napisz kod, w którym kompilator wymusza te decyzje projektowe.
  • Użyj funkcji referencji dopuszczających wartości null we własnych projektach

Warunki wstępne

Musisz skonfigurować maszynę do uruchamiania platformy .NET, w tym kompilatora języka C#. Kompilator języka C# jest dostępny w programie Visual Studio 2022lub .NET SDK.

W tym samouczku założono, że znasz języki C# i .NET, w tym program Visual Studio lub interfejs wiersza polecenia platformy .NET.

Włączanie typów referencyjnych dopuszczających wartości NULL do projektów

W tym samouczku zbudujesz bibliotekę, która modeluje przeprowadzanie ankiety. Kod używa zarówno typów referencyjnych dopuszczających null, jak i typów referencyjnych niedopuszczających null, aby reprezentować koncepcje świata rzeczywistego. Pytania dotyczące ankiety nigdy nie mogą mieć wartości null. Respondent może nie odpowiedzieć na pytanie. Odpowiedzi mogą być null w tym przypadku.

Kod, który napiszesz dla tego przykładu, wyraża tę intencję, a kompilator wymusza tę intencję.

Utwórz aplikację i włącz typy odwołań dopuszczających wartość null

Utwórz nową aplikację konsolową w programie Visual Studio lub z poziomu wiersza polecenia przy użyciu dotnet new console. Nadaj aplikacji nazwę NullableIntroduction. Po utworzeniu aplikacji należy określić, że cały projekt kompiluje się w włączonym kontekstu adnotacji dopuszczających wartość null. Otwórz plik csproj i dodaj element Nullable do elementu PropertyGroup. Ustaw jej wartość na wartość enable. Należy włączyć funkcję typów odwołań dopuszczających null w projektach starszych niż C# 11. Dzieje się tak dlatego, że po włączeniu funkcji istniejące deklaracje zmiennych referencyjnych stają się niepustych typów odwołań. Chociaż ta decyzja pomoże znaleźć problemy, w których istniejący kod może nie mieć odpowiednich kontroli wartości null, może nie odzwierciedlać dokładnie oryginalnej intencji projektu:

<Nullable>enable</Nullable>

Przed platformą .NET 6 nowe projekty nie zawierają elementu Nullable. Począwszy od platformy .NET 6, nowe projekty zawierają element <Nullable>enable</Nullable> w pliku projektu.

Projektowanie typów aplikacji

Ta aplikacja ankiety wymaga utworzenia wielu klas:

  • Klasa, która modeluje listę pytań.
  • Klasa, która modeluje listę osób, z którymi skontaktowano się w ramach ankiety.
  • Klasa, która modeluje odpowiedzi od osoby, która przeprowadziła ankietę.

Te typy będą używać zarówno nullowalnych, jak i niedopuszczających wartości null typów odwołań, aby wskazać, które elementy członkowskie są wymagane, a które są opcjonalne. Typy referencyjne dopuszczające wartość null wyraźnie przekazują intencję projektową.

  • Pytania, które są częścią ankiety, nigdy nie mogą mieć wartości null: Nie ma sensu zadawać pustego pytania.
  • Respondenci nigdy nie mogą mieć wartości null. Chcesz śledzić osoby, z którymi się skontaktowałeś, nawet respondentów, którzy odmówili udziału.
  • Każda odpowiedź na pytanie może mieć wartość null. Respondenci mogą odrzucić odpowiedzi na niektóre lub wszystkie pytania.

Jeśli programujesz w języku C#, możesz być tak przyzwyczajony do typów referencyjnych, które zezwalają na null wartości, które mogły przegapić inne możliwości deklarowania wystąpień bez wartości null:

  • Kolekcja pytań powinna być niepusta.
  • Kolekcja respondentów nie powinna przechowywać wartości null.

Podczas pisania kodu zobaczysz, że typ odwołania nieakceptujący wartości null jako domyślny dla odwołań pozwala uniknąć typowych błędów, które mogą prowadzić do NullReferenceExceptions. Jedną z lekcji z tego samouczka jest to, że podjąłeś decyzje dotyczące tego, które zmienne mogą być lub nie mogą być null. Język nie dostarczył składni, aby wyrazić te decyzje. Teraz to robi.

Skompilowa aplikacja wykonuje następujące czynności:

  1. Tworzy ankietę i dodaje do niej pytania.
  2. Tworzy pseudolosowy zestaw respondentów dla ankiety.
  3. Kontaktuja się z respondentami, dopóki liczba wypełnionych ankiet nie osiągnie docelowej liczby.
  4. Zapisuje ważne statystyki dotyczące odpowiedzi na ankietę.

Tworzenie ankiety z typami referencyjnymi dopuszczanymi do wartości null i niepustymi

Pierwszy kod, który napiszesz, tworzy ankietę. Napiszesz klasy do modelowania pytania ankiety i przebiegu ankiety. Ankieta zawiera trzy typy pytań, wyróżniające się formatem odpowiedzi: Tak/Nie odpowiedzi, odpowiedzi liczbowe i odpowiedzi tekstowe. Utwórz klasę public SurveyQuestion:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Kompilator interpretuje każdą deklarację zmiennej typu odwołania jako nienullowalny typ referencyjny dla kodu w kontekście włączonej adnotacji wartości nullowalnej. Pierwsze ostrzeżenie można wyświetlić, dodając właściwości tekstu pytania i typu pytania, jak pokazano w poniższym kodzie:

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

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

Ponieważ nie zainicjowano QuestionText, kompilator generuje ostrzeżenie, że nie zainicjowano właściwości innej niż null. Projekt wymaga, aby tekst pytania był inny niż null, dlatego należy dodać konstruktor, aby go zainicjować, a także wartość QuestionType. Gotowa definicja klasy wygląda podobnie do następującego kodu:

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

Dodanie konstruktora usuwa ostrzeżenie. Argument konstruktora jest również typem referencyjnym bez wartości null, więc kompilator nie generuje żadnych ostrzeżeń.

Następnie utwórz klasę public o nazwie SurveyRun. Ta klasa zawiera listę obiektów SurveyQuestion i metod dodawania pytań do ankiety, jak pokazano w poniższym kodzie:

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

Tak jak wcześniej, należy zainicjować obiekt listy do wartości innej niż null lub kompilator wystawia ostrzeżenie. Nie ma kontroli sprawdzania wartości null w drugim przeciążeniu AddQuestion, ponieważ są zbędne: zadeklarowałeś, że zmienna ma być niezerowa. Jej wartość nie może być null.

Przejdź do Program.cs w edytorze i zastąp zawartość Main następującymi wierszami kodu:

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

Ponieważ cały projekt znajduje się w włączonym kontekście adnotacji dopuszczającej wartość null, podczas przekazywania null do dowolnej metody oczekiwanej typu odwołania niepustego otrzymasz ostrzeżenia. Wypróbuj go, dodając następujący wiersz do Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Tworzenie respondentów i uzyskiwanie odpowiedzi na ankietę

Następnie napisz kod, który generuje odpowiedzi na ankietę. Ten proces obejmuje kilka małych zadań:

  1. Utwórz metodę, która generuje obiekty respondentów. Reprezentują one osoby poproszone o wypełnienie ankiety.
  2. Utwórz logikę, aby symulować zadawanie pytań respondentowi i zbieranie odpowiedzi lub zauważanie, że respondent nie odpowiedział.
  3. Powtarzaj, dopóki wystarczająca liczba respondentów nie odpowiedziała na ankietę.

Do reprezentowania odpowiedzi na ankietę potrzebna będzie klasa, więc dodaj ją teraz. Włącz obsługę dopuszczania wartości null. Dodaj właściwość Id i konstruktor, który go inicjuje, jak pokazano w poniższym kodzie:

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

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

Następnie dodaj metodę static, aby utworzyć nowych uczestników, generując losowy identyfikator:

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

Główną obowiązkiem tej klasy jest wygenerowanie odpowiedzi dla uczestnika na pytania w ankiecie. Ta odpowiedzialność obejmuje kilka kroków:

  1. Poproś o udział w ankiecie. Jeśli osoba nie wyrazi zgody, zwróć brakującą (lub null) odpowiedź.
  2. Zadaj każde pytanie i zarejestruj odpowiedź. W każdej odpowiedzi może brakować wartości (lub być null).

Dodaj następujący kod do klasy SurveyResponse:

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

Miejsce przechowywania odpowiedzi na ankietę jest Dictionary<int, string>?, co oznacza, że może być nullem. Używasz nowej funkcji języka do deklarowania intencji projektu zarówno kompilatora, jak i do każdego, kto czyta kod później. Jeśli kiedykolwiek wyłuszczyć surveyResponses bez sprawdzania wartości null najpierw, otrzymasz ostrzeżenie kompilatora. W metodzie AnswerSurvey nie jest wyświetlane ostrzeżenie, ponieważ kompilator może określić, że zmienna surveyResponses została ustawiona na wartość inną niż null powyżej.

Użycie null w przypadku braku odpowiedzi wyróżnia kluczowy punkt pracy z typami referencyjnymi dopuszczającymi wartość null: Twoim celem nie jest usunięcie wszystkich wartości null z programu. Zamiast tego twoim celem jest upewnienie się, że kod, który piszesz, wyraża intencję projektu. Brakujące wartości są niezbędne do wyrażenia w kodzie. Wartość null to jasny sposób wyrażania brakujących wartości. Próba usunięcia wszystkich wartości null prowadzi tylko do zdefiniowania innych sposobów wyrażania brakujących wartości bez null.

Następnie należy napisać metodę PerformSurvey w klasie SurveyRun. Dodaj następujący kod w klasie 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);
    }
}

Tutaj ponownie wybór typu List<SurveyResponse>? jako wartości dopuszczającej null wskazuje, że odpowiedź może mieć wartość null. Oznacza to, że ankieta nie została jeszcze udzielona żadnym respondentom. Zwróć uwagę, że respondenci są dodawani, dopóki wystarczająca liczba nie wyrazi zgody.

Ostatnim krokiem do uruchomienia ankiety jest dodanie wywołania w celu przeprowadzenia ankiety na końcu metody Main:

surveyRun.PerformSurvey(50);

Badanie odpowiedzi na ankietę

Ostatnim krokiem jest wyświetlenie wyników ankiety. Dodasz kod do wielu napisanych klas. Ten kod demonstruje wartość rozróżniania typów referencyjnych dopuszczających wartość null i niedopuszczających wartości null. Zacznij od dodania następujących dwóch składowych wyrażeń do klasy SurveyResponse:

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

Ponieważ surveyResponses jest typem odwołania dopuszczającym wartość null, kontrole wartości null są niezbędne przed odwołaniem się do niego. Metoda Answer zwraca ciąg, który nie może być null, dlatego musimy uwzględnić przypadek brakującej odpowiedzi przy użyciu operatora null-coalescing.

Następnie dodaj te trzy elementy wyrażeniowe do klasy SurveyRun:

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

Element członkowski AllParticipants musi uwzględniać, że zmienna respondents może mieć wartość null, ale wartość zwracana nie może być równa null. Jeśli zmienisz to wyrażenie, usuwając ?? i następującą pustą sekwencję, kompilator ostrzega, że metoda może zwrócić null, a jej sygnatura zwracania określa typ niepusty.

Na koniec dodaj następującą pętlę w dolnej części metody 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");
    }
}

Nie potrzebujesz żadnych kontroli null w tym kodzie, ponieważ zaprojektowano podstawowe interfejsy tak, aby wszystkie zwracały typy referencyjne nieprzyjmujące wartości null.

Pobieranie kodu

Kod skończonego samouczka można pobrać z naszego repozytorium przykładów w folderze csharp/NullableIntroduction.

Poeksperymentuj, zmieniając deklaracje typów między typami referencyjnymi nullable i non-nullable. Zobacz, jak generuje różne ostrzeżenia, aby upewnić się, że przypadkowo nie odwołujesz się do null.

Następne kroki

Dowiedz się, jak używać typu odwołania dopuszczanego do wartości null podczas korzystania z programu Entity Framework: