1. 前言
想像一下,程式碼就像一齣舞台劇,變數就是演員。每次變數進入新場景(code block),它可能是「活著的」(有值)或「幽靈」(null)。但問題來了:觀眾——也就是編譯器——不是現場看戲,而是看劇本。他只看到演員什麼時候被分配角色(賦值),什麼時候消失(變成 null)。但他沒辦法看到現實(程式執行時),只能根據劇本猜!
資料流靜態分析,就是編譯器在程式還沒跑之前,先檢查你的 code,看看會不會有人演出「NullReferenceException 大崩潰」的戲碼。
這個流程的重點
從 C# 8.0 開始,有了 Nullable Reference Types(NRT)模式,編譯器會分析變數在某些執行路徑下會不會變成 null。如果有可能,他馬上就會提醒你。他不會讓你「用一個突然變成幽靈的演員的名字」。
2. 怎麼看懂編譯器警告
主要的警告類型
開啟 NRT 模式後,編譯器會特別盯著 reference 型別的變數(像 string、User、陣列、物件)。最常見的警告有這幾種:
- Possible null assignment — 你想把可能是 null 的值賦給一個不能是 null 的變數。
- Dereference of a possibly null reference — 你要用物件的成員,但編譯器懷疑它可能是 null。
- Possible null return — 方法可能回傳 null,但簽名規定一定要有值。
- Nullable object must have a value — 你在沒檢查的情況下用 .Value 取 nullable 型別的值。
- Unassigned non-nullable property — 你沒給一個不能是 null 的欄位/屬性賦值。
通常在 IDE(像 Rider 或 Visual Studio)裡,這些警告會被波浪線標記,滑鼠移過去會有說明。
典型警告範例
#nullable enable
string? possibleNull = GetString();
// 哎呀,編譯器在抱怨:"Dereference of a possibly null reference"
int length = possibleNull.Length; // 警告!
編譯器不確定 possibleNull 會不會是 null,所以你用 .Length 可能會爆掉。
如果方法長這樣:
string? GetString() { ... }
然後你寫:
string nonNullable = GetString();
你就會收到警告:可能把 null 傳給 non-nullable 變數,這樣不安全。
3. 編譯器怎麼分析你的 code
範例 1:直接賦值
string? name = null;
Console.WriteLine(name.Length); // 警告:可能存取到 null
這很明顯:name 是 null,你要用它的屬性就很危險。
範例 2:檢查 null
string? name = GetUserName();
if (name != null)
{
Console.WriteLine(name.Length); // 沒問題!
}
else
{
Console.WriteLine("名字沒填!");
}
編譯器知道在 if (name != null) 裡面,變數是安全的。這就是 flow analysis——根據條件分析值的流向。
4. 編譯器沒辦法「看進去方法裡面」
他只看簽名——如果寫說方法回傳 string?,他就相信可能會是 null,就算裡面永遠回傳字串也一樣。
範例 3:從方法回傳值
string? GetMaybeName(bool useName)
{
if (useName)
return "Code Jedi";
else
return null;
}
void PrintLength()
{
string? name = GetMaybeName(false);
Console.WriteLine(name.Length); // 警告!
}
編譯器看到 GetMaybeName 可能 回傳 null,所以要你小心一點。
5. 常見情境與警告
把 nullable 賦給 non-nullable 變數
string? maybeUser = GetUser();
string alwaysUser = maybeUser; // warning: possible null assignment
要消除警告可以這樣:
string alwaysUser = maybeUser ?? "訪客";
這樣 always 會有字串——要嘛來自方法,要嘛就是「訪客」。
忘了檢查 .HasValue 的 nullable value 型別
int? age = GetAge();
int realAge = age.Value; // warning: 可能存取到 null
你應該這樣寫:
if (age.HasValue)
Console.WriteLine(age.Value);
或是這樣:
int realAge = age ?? -1;
自動屬性沒初始化
class User
{
public string Name { get; set; } // warning: 沒有初始化!
}
解法:
public string Name { get; set; } = "沒名字";
或
public string? Name { get; set; }
6. 複雜一點的 flow 分析
在迴圈裡分析
string? text = null;
while (text == null)
{
text = Console.ReadLine();
}
Console.WriteLine(text.Length); // ok:編譯器懂了!
不同路徑的賦值
string? name;
if (Random.Shared.Next() % 2 == 0)
name = "Alice";
else
name = null;
Console.WriteLine(name.Length); // warning: possible null dereference
編譯器會分析所有分支。只要有一條路是 null,他就會警告你。
7. 怎麼「安撫」編譯器(還有什麼時候不該這樣做)
有時你很確定值不會是 null,但編譯器不信你:
string? value = GetSomething();
if (value == null)
throw new Exception("預期要有值!");
Console.WriteLine(value.Length); // 警告消失了
或者你用 ! 運算子:
string? value = GetSomething();
Console.WriteLine(value!.Length); // 沒警告,但有風險
這叫做 null-forgiving operator。他什麼都不檢查——只是跟編譯器說:「放心啦,我有把握」。但如果你搞錯,執行時還是會爆掉。
GO TO FULL VERSION