可為 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 檔案,並在 Nullable 元素中新增一個 PropertyGroup 元素。 將其值設定為 enable。 你必須選擇在 C# 11 / .NET 7 之前建立的專案中,啟用 可空的參考類型 功能。 一旦啟用此功能,現有的參考變數宣告就會變成 不可空的參考類型。 雖然這個決定有助於找出現有程式碼可能沒有適當空檢查的問題,但可能無法準確反映你原本的設計意圖:
<Nullable>enable</Nullable>
設計應用程式的類型
此調查申請需要建立以下類別:
- 模型化問題清單的類別。
- 用於建模問卷聯繫人清單的類別。
- 一個類別,該類別會建立調查人員答案的模型。
這些類型同時使用可空與不可空的參考型別,來表達哪些成員是必需的,哪些是可選的。 可為 Null 的參考型別會清楚傳達該設計意圖:
- 屬於調查一部分的問題永遠不能是 Null:詢問空白問題並無意義。
- 受訪者永遠不能是 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,編譯器會發出警告,表示某個不可空屬性未被初始化。 您的設計要求問題內容不可為空,因此您新增一個建構函式來初始化它和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 值。 針對公用 API,建議考慮為非可空參考型別加入參數驗證,因為客戶端程式碼可能未啟用可空參考型別,或者可能故意傳遞空值。
切換至編輯器中的 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) 回應。
- 詢問每個問題並記錄答案。 每個答案也可能缺失(或為零)。
將下列程式碼新增至您的 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的值。
接下來,您必須在PerformSurvey類別中撰寫SurveyRun方法。 在類別中 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);
}
}
在這裡,你選擇的 nullable 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 檢查。 編譯程式的靜態分析有助於確保遵循這些設計合約。
取得程式碼
您可以從 csharp/NullableIntroduction 資料夾中的範例存放庫取得已完成教學課程的程式代碼。
嘗試變更可空性和非可空性參考型別之間的型別宣告以進行實驗。 看看如何生成不同的警告,以確保您不會不小心解引用 null。
後續步驟
瞭解如何使用 Entity Framework 時使用可為 Null 的參考類型: