Udostępnij za pomocą


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 muszą zawsze mieć wartość , podczas gdy inne mogą nie mieć wartości.

W tym poradniku nauczysz się, jak:

  • 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

  • Najnowszy .NET SDK
  • Edytor programu Visual Studio Code
  • Zestaw deweloperski C#

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 piszesz 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 jest kompilowany w kontekście adnotacji z włączoną wartością null. Otwórz plik csproj i dodaj element Nullable do elementu PropertyGroup. Ustaw jej wartość na wartość enable. Musisz wyrazić zgodę na funkcję typów odwołań dopuszczanych do wartości null w projektach utworzonych przed C# 11 / .NET 7. Po włączeniu tej funkcji istniejące deklaracje zmiennych referencyjnych stają się typami referencyjnymi bez wartości null. Chociaż ta decyzja pomaga znaleźć problemy, w których istniejący kod może nie mieć odpowiednich kontroli wartości null, może to nie odzwierciedlać dokładnie oryginalnej intencji projektu:

<Nullable>enable</Nullable>

Projektowanie typów aplikacji

Ta aplikacja ankiety wymaga utworzenia następujących 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 używają zarówno referencyjnych typów dopuszczających wartość null, jak i niedopuszczających wartości null, aby wyrazić, którzy członkowie są wymagani, oraz którzy są opcjonalni. 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.

Możesz być tak przyzwyczajony do typów referencyjnych, które zezwalają na wartości null, że możesz przegapić inne sposoby deklarowania wystąpień, które nie mogą być null.

  • Kolekcja pytań powinna być niezerowa.
  • 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 mogących prowadzić do NullReferenceExceptiony. Jedną z lekcji z tego samouczka jest to, że podjąłeś decyzje dotyczące tego, które zmienne mogą lub nie mogą być null. Język nie dostarczył składni, aby wyrazić te decyzje. Teraz to robi.

Utworzona 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ę.

Zbuduj ankietę z typami referencyjnymi z możliwością ustawienia na null i bez możliwości ustawienia na null.

Pierwszy kod, który piszesz, tworzy ankietę. Piszesz klasy do modelowania pytania ankiety i realizacji 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 QuestionTextelementu , kompilator generuje ostrzeżenie, że nie zainicjowano właściwości innej niż null. Projekt wymaga, aby tekst pytania nie był wartością null, dlatego należy dodać konstruktor, który go zainicjuje, 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 żadnych kontroli wartości null w drugim przeciążeniu AddQuestion, ponieważ kompilator pomaga wymusić kontrakt nienullowalności: zadeklarowaliśmy zmienną jako nienullowalną. Chociaż kompilator ostrzega przed potencjalnymi przypisaniami wartości null, wartości null w czasie wykonywania nadal mogą wystąpić. W przypadku publicznych interfejsów API rozważ dodanie weryfikacji argumentów nawet dla typów odwołań, które nie dopuszczają wartości null, ponieważ kod klienta może nie mieć włączonych typów odwołań dopuszczanych do wartości null lub może celowo przekazać wartość 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 kontekście adnotacji z włączonym dopuszczaniem wartości null, przy przekazywaniu null do dowolnej metody oczekującej typu referencyjnego, który nie dopuszcza wartości null, pojawiają się 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. Te obiekty reprezentują 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, aż wystarczająca liczba respondentów odpowie na ankietę.

Potrzebujesz klasy do reprezentowania odpowiedzi na ankietę, więc teraz ją dodaj. 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ą odpowiedź (lub odpowiedź null).
  2. Zadaj każde pytanie i zarejestruj odpowiedź. Każda odpowiedź może również brakować (lub mieć wartość 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!";
    }
}

Pamięć na odpowiedzi ankiety to Dictionary<int, string>?, co wskazuje, że może być pusta. 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łuszczasz surveyResponses bez najpierw sprawdzenia wartości null, otrzymasz ostrzeżenie kompilatora. W metodzie AnswerSurvey nie jest wyświetlane ostrzeżenie, ponieważ kompilator może określić surveyResponses , że zmienna została ustawiona na wartość inną niż null w poprzednim kodzie.

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 null wartości prowadzi do zdefiniowania innego sposobu wyrażania brakujących wartości bez nullelementu .

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

W tym przypadku wybór typu List<SurveyResponse>? nullable wskazuje, że odpowiedź może być pusta. Oznacza to, że ankieta nie została jeszcze udzielona żadnym respondentom. Zwróć uwagę, że respondenci są dodawani do momentu uzyskania wystarczającej 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. Dodajesz 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 null sprawdzeń w tym kodzie, ponieważ zaprojektowałeś podstawowe interfejsy tak, aby wszystkie zwracały typy odwołań nie-null. Analiza statyczna kompilatora pomaga upewnić się, że te kontrakty projektowe są przestrzegane.

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ć typów odwołaniowych dopuszczających wartości null podczas korzystania z Entity Framework.