可為 Null 的參考型別補充參考型別,同樣地,可為 Null 的實值型別補充實值型別。 您可以在型別後附加?
,以將變數宣告為可為 Null 的參考型別。 例如,string?
表示可以為空的 string
。 您可以使用這些新類型更清楚地表達您的設計意圖:某些變數 必須一律有值,有些 變數可能遺漏值。
在本教學課程中,您將了解如何:
- 將可空和不可空的參考型別納入您的設計
- 在整個程式代碼中啟用可為 Null 的參考類型檢查。
- 撰寫程式代碼,讓編譯程式強制執行這些設計決策。
- 在您自己的設計中使用可空的參考功能
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
本教學課程假設您已熟悉 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
或不可能。 語言沒有提供語法來表達這些決定。 現在確實如此。
您將建置的應用程式會執行下列步驟:
- 建立問卷,並將問題新增至其中。
- 為問卷建立一組虛擬隨機受訪者。
- 連絡受訪者,直到完成的問卷數量達到目標數量為止。
- 寫出問卷回應的重要統計數據。
使用可為 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 的參考類型,因此編譯程式不會發出任何警告。
接下來,建立名為的public
SurveyRun
類別。 這個類別包含物件和方法清單 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);
建立受訪者並取得問卷的解答
接下來,撰寫產生問卷解答的程序代碼。 過程涉及幾項小任務:
- 建置產生受訪者物件的方法。 這代表被要求填寫調查問卷的人。
- 建置邏輯來模擬向受訪者詢問問題,並收集答案或注意到受訪者未回答的問題。
- 重複,直到足夠的受訪者回答調查。
您需要一個類別來代表問卷回應,因此請立即新增。 啟用可為 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());
此類別的主要責任是產生問卷中問題參與者的回應。 此責任有一些步驟:
- 要求參與調查。 如果人員不同意,請傳回遺漏的 (或 null) 回應。
- 詢問每個問題並記錄答案。 每個答案也可能遺失(或 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 的參考類型: