本文整理了 .NET Null Reference Warnings 的核心處理策略,並分享我如何與 AI 合作,在升級舊專案時高效消除數百個警告的實戰心得。
前言
在升級一個 .NET Windows Forms 專案到 .NET 10 的時候,我鼓起勇氣,將專案的 .csproj 中的 <Nullable>enable</Nullable> 設定開啟。心裡想著,現在有 AI 工具輔助,應該可以輕鬆處理這些警告。儘管過程有一點小波折,但結果確實比完全人工處理輕鬆許多。
這個 .NET 專案從 .NET 5、6、7,到 .NET 10 一路升級,我一直沒勇氣啟用 Nullable Reference Types,因為我有預感一定會有超多編譯警告。我的預感沒錯,啟用了 Nullable Reference Types 後,出現數百個編譯警告(若沒記錯,有八百個以上的警告)。這個專案的總程式碼行數,我讓 AI 工具幫我統計了一下,排除測試程式碼,總共 29,626 行。
這篇筆記包含三個部分:
- 我用什麼 AI 工具,以及給 AI 什麼指示來處理那些數百個編譯警告。
- 整理 .NET Null Reference Warnings 的處理策略。
- 最後是心得總結。
AI 工具以及給 AI 的指示
我用的程式碼編輯器是 Google Antigravity,目前還在預覽階段,其內建的 AI 模型目前是免費的。就我的理解,目前即使是 Google AI Pro 訂閱戶也無法使用付費模型,亦即每個人的 token limit 都是相同的。(也許這篇文章發布之後不久就會有付費方案)
就我目前使用 Antigravity 的體驗而言,這樣的工具組合寫出來的程式碼和文件的品質都令我很滿意。我只用這兩個模型:
- Gemini 3 Pro
- Claude 4.5 Sonnet (Thinking)
若 token 達到免費限額了,有時我會改用 Gemini CLI,有時則去休息或者做別的事,等五小時過後,免費限額狀態重設了,再回到 Antigravity 繼續使用。
Null Reference Warnings 的相關編譯警告都是以 CS86xx 開頭。如果編譯警告很少,也許可以直接請 AI agent 處理所有 CS86xx 系列的警告。但我覺得這個專案的編譯警告數量實在太多,直接處理恐怕太難為 AI agent 了(恐怕一直鬼打牆),而且免費額度的 token 可能在處理過程中達到限額,而導致任務被迫中斷。
基於上述原因,我決定針對個別警告來分批處理。我給 AI 的指示大概類似底下兩種:
Prompt 1:
解決此 solution 中各專案的 CS8603 警告。這些專案是從比較老舊的 .NET 專案升級上來的,而且一直沒有啟用 Nullable Reference 編譯機制。因此,我研判大部分的 CS8603 警告,只要是 string 或參考型別,應該優先把 null 視為合法值,也就是把型別標成 nullable(T?)。如果真的沒有辦法判斷,才使用
!來壓警告。
Prompt 2:
處理 CS8602 警告。請優先在能發生 null 的點做明確檢查或改變型別以反映真實語意;如果邏輯上永遠不該為 null,確保編譯器能看出來(或使用 [MemberNotNull] 等屬性教它)。盡量少用
!壓警告,除非你確實能保證 null 不會發生。
可以看得出來,上面兩個指示都是告訴 AI agent 不要偷懶直接用 ! 來壓警告。
心得
處理這類警告,不要完全倚賴 AI,而且一定要 review AI 的修改結果。
我在 AI 修改程式碼的過程中,是會介入修改的,因為我曾看到它有時改完後,編譯警告本來縮減成一百多個,後來又增加為兩百多個,類似這種情形。而且,還是自己最了解程式邏輯,知道哪些地方可以放心使用 !,哪些地方不能(當然 AI 在很多時候也能判斷得出來)。
有時候,AI 還會自己發揮創意胡亂加 code。以下是真實案例,我當時還抓了圖:
還好 AI agent 修改完後,我有看了一下 git diff,發現它自行加了一行程式碼(驚!😲),而且是可以通過編譯和測試的。說實話,如果這次不是單純要 AI 處理編譯警告,我可能不會發現這個問題,因為它無端添加的程式碼,是它在別的檔案中的某行程式碼複製過來的。處理編譯警告是不可能會去改動任何商業邏輯,所以比較容易發現。總之,AI 寫的程式碼,我們自己一定要 review 才保險。
接下來就與個人心得沒有太大關係,主要是整理 .NET Null Reference Warnings 的警告類型、發生原因,以及在不同情境下的最佳處理策略。(你知道的,是讓 AI 幫我整理,我再做一點小修而已。)
簡介:.NET 的 Null Reference Warnings 主要是在 C# 8.0 引入了 Nullable Reference Types (NRT) 之後才會出現。當專案的
.csproj設定了<Nullable>enable</Nullable>時,編譯器會進行靜態分析,並針對可能導致NullReferenceException的程式碼發出警告(CS86xx 系列)。
常見警告列表
以下是開發時最常遇到的 Null Reference Warnings:
| 代碼 | 說明 | 常見情境 |
|---|---|---|
| CS8600 | Converting null literal or possible null value to non-nullable type. | 嘗試將 null 或「可能為 null 的變數」指派給「不可為 null 的型別」。例如: string s = null; |
| CS8601 | Possible null reference assignment. | 類似 CS8600,通常發生在屬性指派或變數賦值時。 |
| CS8602 | Dereference of a possibly null reference. | 存取了一個可能為 null 的物件的成員。 例如: s.Length (若 s 可能為 null) |
| CS8603 | Possible null reference return. | 方法宣告回傳不可為 null 的型別,但程式碼回傳了可能為 null 的值。 |
| CS8604 | Possible null reference argument. | 呼叫方法時,將可能為 null 的變數傳入不接受 null 的參數。 |
| CS8618 | Non-nullable field must contain a non-null value when exiting constructor. | 建構函式結束時,某個不可為 null 的欄位尚未被初始化。 常見於 Entity Framework 的 Model 或 DI 注入的屬性。 |
| CS8625 | Cannot convert null literal to non-nullable reference type. | 明確將 null 常值轉換或指派給不可為 null 的型別。 |
針對不同警告類型的處理策略
1. CS8618 (Uninitialized Field)
問題:類別中的非 Nullable 屬性或欄位沒有在建構函式中初始化。
解法:
給予預設值:
public string Name { get; set; } = string.Empty;標記為 Nullable(如果該欄位確實允許為空):
public string? Name { get; set; }使用
required關鍵字 (C# 11+): 強制呼叫端在物件初始化時賦值。public required string Name { get; set; }延遲初始化 (DI / ORM): 如果屬性是由依賴注入容器或 ORM (如 EF Core) 在物件建立後填入,可使用
null!(Null-forgiving operator)。// 告訴編譯器:雖然這裡給 null,但我保證它在使用前會有值 public IService Service { get; set; } = null!;
2. CS8602 (Dereference of possibly null reference)
問題:你正在存取一個編譯器認為可能為 null 的變數。這是最危險的警告,因為它直接對應到執行時期的 NullReferenceException。
解法(依推薦順序):
顯式檢查並提早返回 (fail fast):
public void PrintLength(string? s) { if (s is null) throw new ArgumentNullException(nameof(s)); Console.WriteLine(s.Length); // 編譯器現在知道 s 不為 null }使用 Null 條件運算子 (
?.): 如果你接受結果為 null。int? len = user?.Name?.Length;使用 Null 合併運算子 (
??): 提供預設值。return s?.Length ?? 0;使用 Attributes 協助編譯器分析: 使用
[MemberNotNull]等屬性告訴編譯器某個方法會負責初始化。[MemberNotNull(nameof(_s))] public void Initialize() { _s = "ready"; }
3. CS8603 (Possible null reference return)
問題:方法簽章承諾回傳非 null 值,但實作中可能回傳 null。
解法:
修改回傳型別: 如果
null是合法的回傳值,將回傳型別改為T?。public string? FindName(int id) { ... }確保不回傳 null: 修改邏輯以回傳預設物件或拋出例外。
public string GetName() { return _name ?? string.Empty; // 或 throw new InvalidOperationException(); }使用
!(慎用): 只有當你確定該值絕對不為 null,但編譯器無法推斷時。
深入理解 Null-forgiving Operator (!)
在 C# 中,null! (例如 string x = null!;) 被稱為 Null-forgiving operator。
它的作用
它是一個純編譯時期的指令,告訴編譯器:
「我知道這裡看起來是 null,但我保證在執行時它不會是 null,請不要發出警告。」
它不會改變執行時期的行為。如果你對一個真的是 null 的變數使用了 !,執行時依然會拋出 NullReferenceException。
何時使用?
- 單元測試:測試 setup 過程中。
- 依賴注入 (DI):屬性注入場景。
- Entity Framework Core:導覽屬性或由資料庫填入的欄位。
- Interop:與舊有程式碼或未標註 Nullable 的函式庫互動時。
風險
string name = null!; // 壓掉警告
Console.WriteLine(name.Length); // 🚨 執行時崩潰 (NullReferenceException)
原則:除非你確實能保證 null 不會發生,否則盡量少用 !。在 Code Review 時,所有的 ! 都應被視為潛在的 Code Smell 並仔細審查。
決策指南與 Code Review 檢查清單
在處理 Nullable 警告時,可以參考以下決策流程:
| 情境 | 建議作法 |
|---|---|
| API 參數不應為 null | 保持 T,在方法入口檢查並 throw ArgumentNullException。 |
| API 參數允許為 null | 使用 T?,並在文件中說明 null 的意義。 |
| 回傳值可能為 null | 將回傳型別改為 T?。 |
| 欄位由 DI/ORM/序列化賦值 | 使用 = null!; 並註明原因。 |
| 編譯器無法推斷但你確定非 null | 使用 ! (局部或回傳),必須加上註解解釋原因。 |
Code Review 重點
- 參數檢查:Public API 是否有適當的 null 檢查?
- 回傳型別:是否誠實反映了可能回傳 null 的情況?
-
!的使用:每一個!是否都有合理的解釋?是否可以用?或??取代? - LINQ 查詢:
FirstOrDefault的結果是否有做 null 檢查? - 多執行緒:是否有 Race Condition 導致檢查後變為 null 的風險?
心得總結
若以人工方式解決這些 Null Reference Warnings,我會在 Visual Studio 裡面逐一點擊編譯警告,讓編輯器自動跳到發生警告的程式碼,接著查看程式邏輯並選擇最佳處理方式,例如:
- 變數宣告型別的地方加上
?(例如string?) - 或者加上
if xxx != null的保護 - 又或者在變數後面加上
!來壓制警告(這是比較偷懶的解法,放到最後才考慮)
AI 比人工修正的好處自然是讓人解放雙手,只要把指令下好,人就可以去做別的事。不過,等 AI 把事情做完,人還是得 review 它做了哪些事情,查看 git diff 以確保 AI 沒有胡亂加 code 甚至刪 code。例如文中提到的真實案例,只是讓它解決 nullable reference 編譯警告,AI 卻添加了一行影響應用程式邏輯的程式碼,而且語法還正確、能通過測試。若 AI 改完以後沒有人工逐一 review,可能就在程式裏面埋下了地雷,等將來有一天引爆,要再回溯過往修改歷史來找出問題根源,又要費更多力氣。
Keep coding! (with AI) 🤖
沒有留言: