我與 AI 處理 .NET Null Reference Warnings 的心得

本文整理了 .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:

代碼說明常見情境
CS8600Converting null literal or possible null value to non-nullable type.嘗試將 null 或「可能為 null 的變數」指派給「不可為 null 的型別」。
例如:string s = null;
CS8601Possible null reference assignment.類似 CS8600,通常發生在屬性指派或變數賦值時。
CS8602Dereference of a possibly null reference.存取了一個可能為 null 的物件的成員。
例如:s.Length (若 s 可能為 null)
CS8603Possible null reference return.方法宣告回傳不可為 null 的型別,但程式碼回傳了可能為 null 的值。
CS8604Possible null reference argument.呼叫方法時,將可能為 null 的變數傳入不接受 null 的參數。
CS8618Non-nullable field must contain a non-null value when exiting constructor.建構函式結束時,某個不可為 null 的欄位尚未被初始化。
常見於 Entity Framework 的 Model 或 DI 注入的屬性。
CS8625Cannot convert null literal to non-nullable reference type.明確將 null 常值轉換或指派給不可為 null 的型別。

針對不同警告類型的處理策略

1. CS8618 (Uninitialized Field)

問題:類別中的非 Nullable 屬性或欄位沒有在建構函式中初始化。

解法

  1. 給予預設值

    public string Name { get; set; } = string.Empty;
  2. 標記為 Nullable(如果該欄位確實允許為空):

    public string? Name { get; set; }
  3. 使用 required 關鍵字 (C# 11+): 強制呼叫端在物件初始化時賦值。

    public required string Name { get; set; }
  4. 延遲初始化 (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

解法(依推薦順序):

  1. 顯式檢查並提早返回 (fail fast)

    public void PrintLength(string? s) { if (s is null) throw new ArgumentNullException(nameof(s)); Console.WriteLine(s.Length); // 編譯器現在知道 s 不為 null }
  2. 使用 Null 條件運算子 (?.): 如果你接受結果為 null。

    int? len = user?.Name?.Length;
  3. 使用 Null 合併運算子 (??): 提供預設值。

    return s?.Length ?? 0;
  4. 使用 Attributes 協助編譯器分析: 使用 [MemberNotNull] 等屬性告訴編譯器某個方法會負責初始化。

    [MemberNotNull(nameof(_s))] public void Initialize() { _s = "ready"; }

3. CS8603 (Possible null reference return)

問題:方法簽章承諾回傳非 null 值,但實作中可能回傳 null。

解法

  1. 修改回傳型別: 如果 null 是合法的回傳值,將回傳型別改為 T?

    public string? FindName(int id) { ... }
  2. 確保不回傳 null: 修改邏輯以回傳預設物件或拋出例外。

    public string GetName() { return _name ?? string.Empty; // 或 throw new InvalidOperationException(); }
  3. 使用 ! (慎用): 只有當你確定該值絕對不為 null,但編譯器無法推斷時。


深入理解 Null-forgiving Operator (!)

在 C# 中,null! (例如 string x = null!;) 被稱為 Null-forgiving operator

它的作用

它是一個純編譯時期的指令,告訴編譯器:

「我知道這裡看起來是 null,但我保證在執行時它不會是 null,請不要發出警告。」

不會改變執行時期的行為。如果你對一個真的是 null 的變數使用了 !,執行時依然會拋出 NullReferenceException

何時使用?

  1. 單元測試:測試 setup 過程中。
  2. 依賴注入 (DI):屬性注入場景。
  3. Entity Framework Core:導覽屬性或由資料庫填入的欄位。
  4. 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) 🤖

沒有留言:

技術提供:Blogger.
回頂端⬆️