Samouczek: wyraźniej wyrażanie intencji projektowej przy użyciu typów odwołań dopuszczanych do wartości null i nienależących do wartości null

Typy odwołań dopuszczane do wartości null uzupełniają typy odwołań tak samo, jak typy wartości dopuszczanych do wartości null. Zadeklarujesz zmienną, która ma być typem odwołania dopuszczającym wartość null , dołączając element ? do typu. Na przykład string? reprezentuje wartość null.string Możesz użyć tych nowych typów, aby wyraźniej wyrazić intencję projektowania: niektóre zmienne muszą zawsze mieć wartość, inne mogą brakować wartości.

Z tego samouczka dowiesz się, jak wykonywać następujące czynności:

  • Dołączanie typów odwołań dopuszczanych do wartości null i nienależących do wartości null do projektów
  • Włącz sprawdzanie typu odwołania dopuszczanego do wartości null w całym kodzie.
  • Napisz kod, w którym kompilator wymusza te decyzje projektowe.
  • Korzystanie z funkcji odwołania dopuszczanego do wartości null we własnych projektach

Wymagania 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 2022 lub zestawie SDK platformy .NET.

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

Dołączanie typów odwołań dopuszczanych do wartości null do projektów

W tym samouczku utworzysz bibliotekę, która modeluje uruchamianie ankiety. Kod używa zarówno typów referencyjnych dopuszczanych wartości null, jak i typów odwołań nienależących do wartości null do reprezentowania pojęć rzeczywistych. 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ę.

Tworzenie aplikacji i włączanie typów odwołań dopuszczanych do wartości null

Utwórz nową aplikację konsolową w programie Visual Studio lub z poziomu wiersza polecenia przy użyciu polecenia dotnet new console. Nadaj aplikacji NullableIntroductionnazwę . Po utworzeniu aplikacji należy określić, że cały projekt jest kompilowany w kontekście adnotacji z włączoną możliwością wartości null. Otwórz plik csproj i dodaj Nullable element do PropertyGroup elementu. Ustaw dla niej wartość enable. W projektach starszych niż C# 11 należy wyrazić zgodę na funkcję typów odwołań dopuszczanych do wartości null . Dzieje się tak, ponieważ po włączeniu funkcji istniejące deklaracje zmiennych referencyjnych stają się niepustymi typami referencyjnymi. 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ą Nullable .NET 6 nowe projekty nie zawierają elementu. Począwszy od platformy .NET 6, nowe projekty zawierają <Nullable>enable</Nullable> element 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 skontaktowanych z ankietą.
  • Klasa, która modeluje odpowiedzi od osoby, która przeprowadziła ankietę.

Te typy będą korzystać zarówno z typów odwołań dopuszczających wartość null, jak i nienależących do wartości null, aby wyrazić, które elementy członkowskie są wymagane i które elementy członkowskie są opcjonalne. Typy referencyjne dopuszczające wartość null komunikują się wyraźnie z intencją projektowania:

  • 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 skontaktowano się, 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ń nienależących do wartości null:

  • Kolekcja pytań powinna być niepusta.
  • Kolekcja respondentów powinna być niepusta.

Podczas pisania kodu zobaczysz, że typ odwołania niezwiązany z wartością null jako domyślny dla odwołań pozwala uniknąć typowych błędów, które mogą prowadzić do NullReferenceExceptionbłędów. Jedną z lekcji z tego samouczka jest to, że podjęto decyzje dotyczące zmiennych, które mogą lub nie mogą być null. Język nie dostarczył składni w celu wyrażenia tych decyzji. Teraz to robi.

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

  1. Tworzy ankietę i dodaje do niej pytania.
  2. Tworzy pseudo-losowy zestaw respondentów dla ankiety.
  3. Kontaktuje się z respondentami, dopóki ukończony rozmiar ankiety nie osiągnie numeru celu.
  4. Zapisuje ważne statystyki dotyczące odpowiedzi na ankietę.

Tworzenie ankiety z typami referencyjnymi dopuszczanymi do wartości null i nienależących do wartości null

Pierwszy kod, który napiszesz, tworzy ankietę. Napiszesz klasy, aby modelować pytanie ankiety i uruchomić ankietę. Ankieta zawiera trzy typy pytań, wyróżniające się formatem odpowiedzi: Tak/Brak odpowiedzi, odpowiedzi numerów i odpowiedzi tekstowych. Utwórz klasę public SurveyQuestion :

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Kompilator interpretuje każdą deklarację zmiennej typu odwołania jako typ odwołania bez wartości null dla kodu w włączonym kontekście adnotacji dopuszczający wartość null. Pierwsze ostrzeżenie można wyświetlić, dodając właściwości tekstu pytania i typ 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 QuestionTextzainicjowano programu , kompilator wystawia ostrzeżenie o tym, że nie zainicjowano właściwości bez wartości null. Projekt wymaga, aby tekst pytania był inny niż null, dlatego należy dodać konstruktor, aby zainicjować go i QuestionType wartość. Zakończona definicja klasy wygląda jak następujący kod:

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 spowoduje usunięcie ostrzeżenia. Argument konstruktora jest również niepustym typem odwołania, więc kompilator nie wystawia żadnych ostrzeżeń.

Następnie utwórz klasę public o nazwie SurveyRun. Ta klasa zawiera listę SurveyQuestion obiektów 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 żadnych kontroli null w drugim przeciążeniu, AddQuestion ponieważ nie są potrzebne: Zadeklarowaliśmy, że zmienna ma być niepusta. Jego wartość nie może być null.

Przejdź do pliku 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ącym wartość null, podczas przekazywania null do dowolnej metody oczekiwanego typu odwołania niezwiązanego z wartością null otrzymasz ostrzeżenia. Spróbuj wykonać tę próbę, dodając następujący wiersz do Mainelementu :

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. Powtórz, dopóki wystarczająca liczba respondentów nie odpowiedziała na ankietę.

Potrzebna będzie klasa reprezentująca odpowiedź na ankietę, więc dodaj ją teraz. Włącz obsługę dopuszczania wartości null. Dodaj właściwość i konstruktor, który go inicjuje Id , 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 dana osoba nie wyrazi zgody, zwróć brakującą odpowiedź (lub null).
  2. Zadaj każde pytanie i zarejestruj odpowiedź. Każda odpowiedź może również brakować (lub 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!";
    }
}

Magazyn odpowiedzi na ankietę jest wartością wskazującą Dictionary<int, string>?, że może to być wartość null. 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 nie surveyResponses sprawdzisz null wartości jako pierwszej, otrzymasz ostrzeżenie kompilatora. Nie otrzymujesz ostrzeżenia w metodzie AnswerSurvey , ponieważ kompilator może określić, że surveyResponses zmienna została ustawiona na wartość inną niż null powyżej.

Użycie null funkcji dla brakujących odpowiedzi wyróżnia kluczowy punkt pracy z typami referencyjnymi dopuszczającymi wartość null: Twoim celem nie jest usunięcie wszystkich null wartości 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 jest wyraźnym sposobem wyrażenia brakujących wartości. Próba usunięcia wszystkich null wartości prowadzi tylko do zdefiniowania innego sposobu wyrażania brakujących wartości bez nullelementu .

Następnie należy napisać metodę PerformSurvey w SurveyRun klasie . Dodaj następujący kod w SurveyRun klasie :

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

W tym przypadku wybór wartości List<SurveyResponse>? 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 nie wyrazisz na to zgody.

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

surveyRun.PerformSurvey(50);

Badanie odpowiedzi na ankietę

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

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, przed odwołaniem do niego konieczne są kontrole wartości null. Metoda Answer zwraca ciąg niepusty, więc musimy pokryć przypadek brakującej odpowiedzi przy użyciu operatora łączenia wartości null.

Następnie dodaj te trzy składowe SurveyRun wyrażeń do klasy:

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

Element AllParticipants członkowski musi wziąć pod uwagę, że zmienna respondents może mieć wartość null, ale zwracana wartość nie może być równa null. Jeśli zmienisz to wyrażenie, usuwając ?? pustą sekwencję i , która następuje poniżej, kompilator wyświetli ostrzeżenie, że metoda może zwrócić null i zwracany podpis zwraca typ niepusty.

Na koniec dodaj następującą pętlę w dolnej części Main metody:

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 null kontroli w tym kodzie, ponieważ zostały zaprojektowane podstawowe interfejsy, aby wszystkie zwracały typy referencyjne, które nie dopuszczają wartości null.

Uzyskiwanie kodu

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

Eksperymentuj, zmieniając deklaracje typów między typami referencyjnymi dopuszczanymi do wartości null i niepustymi. Zobacz, jak generuje różne ostrzeżenia, aby upewnić się, że przypadkowo nie wyłuszczasz wartości .null

Następne kroki

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