1. 介紹
當你寫程式時,應該有時會在心裡想:「為什麼我老是要為不同的類別一再複製相同的樣板程式碼?」或者:「為什麼序列化、logging、資料 mapping 要寫這麼多重複的行?」有時真的希望有人(或某個東西)幫你把那些累人的樣板寫好。
這時 Source Generators 就登場了 — 這是 C# 在 .NET 5 出現的一個新功能,後續還在持續發展。Source Generator 是個在編譯階段執行的函式庫,可以動態產生 C# 程式碼,並在最終建置前自動把這些程式碼加入到你的專案裡。
為什麼要用它?
- 自動化例行工作:免去手動撰寫重複的類別/方法(boilerplate)。
- 由編譯器保證的安全性:產生的程式碼會和你的程式一起編譯(不像 T4 或反射那樣在 runtime 才檢查)。
- 高效能:序列化、DI、mapping 等可以在 compile-time 產生,避免 runtime 反射的成本。
- 支援現代模式:能實作那些在沒有程式碼生成下很難或很貴的方案。
那 Source Generators 是怎麼「在底層」運作的?
Source Generator 是一個 .NET 函式庫(通常是專案類型為 Class Library),實作介面 ISourceGenerator。在編譯期間 Roslyn 會啟動所有註冊的生成器,並把你的程式碼的語法樹(syntax tree)提供給它們。
生成器會分析你的程式碼、決定需要在哪裡產生什麼,然後建立新的 C# 檔案,編譯器會立即把這些檔案編譯進去。
自動生成 ToString
我們先從簡單開始。假設我們有一個擁有很多屬性的類別,需要實作 ToString。手寫看起來像這樣:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
=> $"Person(Name={Name}, Age={Age})";
}
但是當屬性變很多時就會懶得寫,而且也可能忘了更新。Source Generator 可以幫你自動完成這件事!
2. 如何建立自己的 Source Generator?
建立專案
打開 JetBrains Rider 或 Visual Studio,建立一個新的專案,類型選 Class Library (.NET Standard) — 這類專案可當作生成器。接著加入以下 NuGet 套件:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
重要的屬性
- [Generator] — 表示這個類別是個 Source Generator。
最簡單的生成器範本
下面是一個最小可用的範例:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 可以註冊額外的動作(可選)
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""嗨,世界!我是自動生成的!"";
}
}";
context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
}
}
這個簡單的生成器會在編譯時永遠加入一個靜態類別 HelloWorld,裡面有方法 SayHello。
如何在主應用程式中使用 Source Generators?
把生成器專案當作 NuGet 套件或以 Project Reference 的方式加入,並放到 Analyzer 區段(更多細節請看 官方文件)。
生成的程式碼會立即在你的專案中可用 — 不需要額外引用,只要直接使用:
// 這會自動被生成!
using Generated;
Console.WriteLine(HelloWorld.SayHello());
3. 真實範例:自動生成 ToString
假設我們想要所有被標記了屬性 [AutoToString] 的類別都自動取得 ToString 的實作。需要做的事情包括:
- 建立一個自訂屬性。
- 分析所有帶這個屬性的類別。
- 為每個這樣的類別生成 ToString 方法。
屬性
[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}
在程式碼中的用法
[AutoToString]
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
簡單的生成邏輯
生成器會尋找帶有 [AutoToString] 的類別,並生成大致如下的程式碼:
public override string ToString()
=> $"Product(Name={Name}, Price={Price})";
真實生成器的程式碼片段
基本想法是用 Roslyn 遍歷語法樹:
public void Execute(GeneratorExecutionContext context)
{
// 分析所有的語法樹
foreach (var tree in context.Compilation.SyntaxTrees)
{
var root = tree.GetRoot();
// 找到所有帶有目標屬性的類別(僅示意!)
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("AutoToString")));
foreach (var @class in classes)
{
var className = @class.Identifier.Text;
// 取得類別的所有屬性
var props = @class.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Identifier.Text)
.ToArray();
var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
var generated = $@"
partial class {className}
{{
public override string ToString() => $""{className}({toStringCode})"";
}}";
context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
}
}
}
注意:在 production 程式碼中通常會透過 Roslyn 的 SemanticModel 做更正確的分析。
4. 實用的小細節
要注意的點
Source Generators 無法修改既有的原始程式碼 — 只能新增檔案(例如額外的 partial 類別、方法等)。這表示如果你的類別宣告為 partial,就可以為它生成額外的方法或屬性。
有時正確解析語法並處理語言所有細節(例如巢狀類別、泛型、存取修飾子等)會比較難。生成器作者必須確保生成的程式碼可以編譯,不會破壞專案。
另一個陷阱是:如果你生成的方法用來實作介面,請確保檔案在每次建置時都會被生成。否則可能出現奇怪的編譯錯誤。現代工具多半能處理這類問題,但還是要留意。
Source Generators vs. 反射
反射:在 runtime 被呼叫、耗費資源、不由編譯器檢查,在大量資料上通常也比較慢。
Source Generator:在編譯階段產生程式碼。所有東西都是靜態檢查的,IDE 可以看到方法、提供自動完成,效能和一般 C# 程式碼相同。
實務上的好處
- System.Text.Json:可以在不使用反射的情況下生成序列化/反序列化程式碼。
- 設計 DI container:例如用 Microsoft.Extensions.DependencyInjection 生成依賴圖。
- 像 Mapster 這樣的 mapper:從反射轉向 compile-time 生成 mapping 程式碼。
- 測試框架:根據屬性自動生成測試方法。
- ASP.NET Minimal APIs(從 .NET 7 開始):生成 endpoint handler。
設定、參數與選項
生成器可以透過 MSBuild 參數、額外檔案和命名慣例來配置。舉例來說,你可以根據環境(Debug/Release)或應用設定來生成不同的 ToString。
Source Generators 與實際任務的關聯
對追求乾淨且高效程式碼的開發者來說,這是個很棒的工具:少寫例行設定、更多 compile-time 檢查、IDE 更友善的提示,以及更容易的重構。會用生成器的能力也越來越常在面試中被問到 — 從序列化、DI 到 mapping。
Source Generator 的生命週期
| 階段 | 發生了什麼 |
|---|---|
| 1. 專案被加入 | 你的生成器被當作 analyzer/reference 加入 |
| 2. Roslyn 編譯原始碼 | 生成器收到 AST(抽象語法樹) |
| 3. 生成器執行 | 加入新的 .cs 檔案供編譯使用 |
| 4. 一切被編譯 | 生成的檔案成為你組件的一部分 |
| 5. 程式碼就緒! | 可以呼叫生成的方法/類別 |
5. 除錯與常見錯誤
初學者寫生成器時最常犯的一個錯誤是忘記在你想要補充程式碼的類別上加上關鍵字 partial。如果沒加 partial,編譯器不會看到你的變更。有時生成的檔案在 IDE 裡不會立即顯示,直到第一次重建專案 — 不用太緊張。
小心命名生成檔案:如果都用同一個名稱,會互相覆蓋。小技巧是把要生成程式碼的類別名稱放進檔名,例如 context.AddSource($"{className}_ToString", ...)。
如果你生成了已在主專案中存在的屬性,可能會出現重複匯入的問題。比較好的做法是把需要的屬性放到共用專案,或者僅在必要時才生成該屬性。
GO TO FULL VERSION