共用方式為


教學課程:使用可為 Null 和不可為 Null 的參考類型清晰表達您的設計意圖

可為 Null 的參考型別補充參考型別,同樣地,可為 Null 的實值型別補充實值型別。 您可以在型別後附加?,以將變數宣告為可為 Null 的參考型別。 例如,string? 表示可以為空的 string。 您可以使用這些新類型更清楚地表達您的設計意圖:某些變數 必須一律有值,有些 變數可能遺漏值

在本教學課程中,您將了解如何:

  • 將可空和不可空的參考型別納入您的設計
  • 在整個程式代碼中啟用可為 Null 的參考類型檢查。
  • 撰寫程式代碼,讓編譯程式強制執行這些設計決策。
  • 在您自己的設計中使用可空的參考功能

先決條件

本教學課程假設您已熟悉 C# 和 .NET,包括 Visual Studio 或 .NET CLI。

將可為空的參考類型納入您的設計中

在本教學課程中,您將建置一個模型運行問卷調查的程式庫。 程序代碼同時使用可為 Null 的參考型別和不可為 Null 的參考型別來代表真實世界的概念。 問卷問題永遠不能是空值。 受訪者可能不想回答問題。 在此案例中,回應可能是null

您為此範例撰寫的程式代碼會表達該意圖,而編譯程式會強制執行該意圖。

建立應用並啟用可為空的參考類型

在 Visual Studio 中或者從命令提示字元使用 dotnet new console 建立新的控制台應用程式。 將應用程式 NullableIntroduction命名為 。 建立應用程式後,您需要指定整個專案在啟用的 可空批註上下文中進行編譯。 開啟 .csproj 檔案,並在 PropertyGroup 元素中新增一個 Nullable 元素。 將其值設定為 enable。 您必須在早於 C# 11 的專案中加入 可為 Null 的參考型別 功能。 這是因為一旦功能開啟,現有的參考變數宣告就會變成 不可為 Null 的參考型別。 雖然該決策有助於找出現有程式代碼可能沒有適當 Null 檢查的問題,但它可能無法準確地反映您的原始設計意圖:

<Nullable>enable</Nullable>

在 .NET 6 之前,新的專案不會包含 Nullable 元素。 從 .NET 6 開始,新專案會在專案檔中包含 <Nullable>enable</Nullable> 元素。

設計應用程式的類型

此問卷應用程式需要建立一些類別:

  • 模型化問題清單的類別。
  • 用於建模問卷聯繫人清單的類別。
  • 一個類別,該類別會建立調查人員答案的模型。

這些類型會使用 nullable 和非 nullable 的參考型別來表示哪些成員是必要的,哪些是可選的。 可為 Null 的參考型別會清楚傳達該設計意圖:

  • 屬於調查一部分的問題永遠不能是 Null:詢問空白問題並無意義。
  • 受訪者永遠不能是 Null。 您會想要追蹤您連絡的人員,甚至是拒絕參與的受訪者。
  • 對問題的任何回應可能是 Null。 受訪者可能會拒絕回答部分或所有問題。

如果您已使用 C# 進行程式設計,您可能已經習慣於允許 Null 值的參考型別,因此可能會錯過宣告不可為 Null 的實例的其他機會:

  • 問題集合應該不可為空值。
  • 受訪者的集合應該是不可為 Null 的。

當您撰寫程式碼時,您會看到將不可為 Null 的參考類型設為預設值,能避免導致 NullReferenceException 的常見錯誤。 本教程的一個啟示是,您在這個過程中學到如何決定哪些變數可能是 null 或不可能。 語言沒有提供語法來表達這些決定。 現在確實如此。

您將建置的應用程式會執行下列步驟:

  1. 建立問卷,並將問題新增至其中。
  2. 為問卷建立一組虛擬隨機受訪者。
  3. 連絡受訪者,直到完成的問卷數量達到目標數量為止。
  4. 寫出問卷回應的重要統計數據。

使用可為 Null 和不可為 Null 的參考類型來設計問卷

您將撰寫的第一個程式代碼會建立問卷。 您將撰寫課程來建立問卷問題和問卷回合的模型。 您的問卷有三種類型的問題,以答案的格式區分:是/否答案、數位答案和文字答案。 建立類別 public SurveyQuestion

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

編譯程式會將每個參考型別變數宣告解釋為在啟用可為 Null 註釋的上下文中非可為 Null 的參考型別。 您可以新增問題文字的屬性和問題類型來查看您的第一個警告,如下列程式代碼所示:

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

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

因為您尚未初始化 QuestionText,編譯程式會發出警告,指出尚未初始化不可為 Null 的屬性。 您的設計要求問題內容不可為空,因此您新增一個建構函式來初始化它和QuestionType值。 完成的類別定義看起來像下列程式代碼:

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

新增建構函式會移除警告。 建構函式自變數也是不可為 Null 的參考類型,因此編譯程式不會發出任何警告。

接下來,建立名為的publicSurveyRun類別。 這個類別包含物件和方法清單 SurveyQuestion ,可將問題新增至問卷,如下列程式代碼所示:

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

如同之前,您必須將清單物件初始化為非 Null 值,或編譯程式發出警告。 第二個多載 AddQuestion 中沒有 Null 檢查,因為它們並不需要:您已宣告該變數不可為 Null。 其值不可以是 null

切換至編輯器中的 Program.cs ,並以下列幾行程式代碼取代 的內容 Main

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

由於整個專案都是在啟用了可為空值的註解環境中,因此當您傳遞 null 至任何預期不可為空值的參考類型的方法時,您會收到警告。 將下列這一行新增至 Main,然後試試看:

surveyRun.AddQuestion(QuestionType.Text, default);

建立受訪者並取得問卷的解答

接下來,撰寫產生問卷解答的程序代碼。 過程涉及幾項小任務:

  1. 建置產生受訪者物件的方法。 這代表被要求填寫調查問卷的人。
  2. 建置邏輯來模擬向受訪者詢問問題,並收集答案或注意到受訪者未回答的問題。
  3. 重複,直到足夠的受訪者回答調查。

您需要一個類別來代表問卷回應,因此請立即新增。 啟用可為 Null 的支援功能。 Id新增屬性和初始化它的建構函式,如下列程式代碼所示:

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

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

接下來,新增 static 方法以產生隨機標識符來建立新的參與者:

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

此類別的主要責任是產生問卷中問題參與者的回應。 此責任有一些步驟:

  1. 要求參與調查。 如果人員不同意,請傳回遺漏的 (或 null) 回應。
  2. 詢問每個問題並記錄答案。 每個答案也可能遺失(或 Null)。

將下列程式碼新增至您的 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!";
    }
}

問卷答案的儲存是 Dictionary<int, string>?,表示可能是空值。 您正在使用新的語言特性來明確表達您的設計意圖,無論是給編譯器還是給稍後閱讀您的程式碼的人。 如果您在未先檢查 null 值的情況下引用 surveyResponses,就會收到編譯器警告。 在 AnswerSurvey 方法中您不會收到警告,因為編譯器能夠判斷 surveyResponses 變數在上方已設定為非空值。

利用 null 來表示遺漏的答案,突顯了使用可為 Null 參考類型的一個關鍵點:您的目標並不是要從程式中移除所有 null 值。 相反地,您的目標是確保您撰寫的程式代碼表達設計意圖。 遺漏值是在您的程序代碼中表示的必要概念。 此值 null 是表達這些遺漏值的清楚方式。 嘗試移除所有 null 值只會定義一些其他方法來表示沒有的 null遺漏值。

接下來,您必須在SurveyRun類別中撰寫PerformSurvey方法。 在類別中 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);
    }
}

同樣地,您選擇的可為 null 的 List<SurveyResponse>? 表示回應可能是 null。 這表明調查尚未提供給任何受訪者。 請注意,會持續新增受訪者,直到有足夠的人同意為止。

執行問卷的最後一步是新增一個呼叫,在 Main 方法的結尾執行問卷。

surveyRun.PerformSurvey(50);

檢查問卷回應

最後一個步驟是顯示問卷結果。 您會將程式代碼新增至您已撰寫的許多類別。 此程式碼演示區分可空和不可空參考型別的重要性。 首先,將下列兩個運算式主體成員新增至 SurveyResponse 類別:

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

因為 surveyResponses 是可為 Null 的參考型別,所以在取消參考之前,必須先進行 Null 檢查。 Answer方法會傳回不可為 Null 的字串,因此我們必須使用 Null 合併運算子來處理缺少答案的情況。

接下來,將這三個表達式主體成員新增至 SurveyRun 類別:

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

成員 AllParticipants 必須考慮 respondents 變數可能是 Null,但傳回值不能是 Null。 如果您藉由移除 ?? 和 後面的空白序列來變更該表達式,編譯程式會警告您方法可能會傳回 null ,而其傳回簽章會傳回不可為 Null 的類型。

最後,在方法底部 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");
    }
}

因為基礎介面已設計成都會傳回不可為 Null 的參考型別,所以在此程式代碼中不需要任何 null 檢查。

取得程式碼

您可以從 csharp/NullableIntroduction 資料夾中的範例存放庫取得已完成教學課程的程式代碼。

嘗試變更可空性和非可空性參考型別之間的型別宣告以進行實驗。 看看如何生成不同的警告,以確保您不會不小心解引用 null

後續步驟

瞭解如何使用 Entity Framework 時使用可為 Null 的參考類型: