CodeGym /課程 /C# SELF /Source Generators 和 ...

Source Generators 和 System.Reflection.Emit

C# SELF
等級 63, 課堂 4
開放

1. 簡介

老實說:大多數人對程式碼生成的想像,大概跟切蛋器一樣 — 有用,但很少是必需的。不過現在的 .NET 專案越來越複雜,把例行或模板化的工作自動化,不只是「花招」,而是提升品質和生產力的重要工具。

Source Generators 是從 C# 9 跟 .NET 5 開始有的機制。它們像是在編譯階段的超級英雄,可以生成 C# 原始碼,然後這些原始碼會作為你專案的一部分被編譯。它們不會在執行時改動已編譯的 assembly,而是把新的原始碼在編譯前加入到專案裡。

下面會列出一些 Source Generators 的典型使用場景。

自動生成樣板程式碼

很多專案會需要寫一模一樣的程式碼:建構子、ToString 方法、序列化器、property change notification (INotifyPropertyChanged),等等。Source Generators 可以自動生成這些程式碼,讓開發者免於例行性的工作和打字錯誤的風險。

範例:ToString 生成器

假設你在寫 DTO 類別 (Data Transfer Object) 來傳遞資料。你需要為每個類別實作有意義的 ToString,列出所有屬性。

與其手動複製,不如寫一個 Source Generator,對於每個帶有屬性 [AutoToString] 的類別生成 ToString 的實作。像 AutoToString 這類的套件就是這樣做的。

// 你的類別帶有屬性
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator 會生成(簡化版):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

優點:程式碼可讀且在新增屬性時會自動更新。

簡化序列化與反序列化

Source Generators 在標準函式庫裡也被大量使用,像是 System.Text.Json,用來生成高效的序列化/反序列化程式碼。生成器出現之前,序列化常常要靠 reflection(效能昂貴),現在預生成的程式碼讓速度快又安全。

開發者得到什麼? 你標註 [JsonSerializable(typeof(MyType))] — 生成器就會為該型別生成高效的序列化程式碼。

為設定、mapper、DI-container 生成程式碼

  • 設定檔:生成器可以依照 JSON 檔自動生成設定類別。
  • Mapper:例如 Mapster 用生成器在型別間建立 mapping,免去手動拷貝欄位。
  • Dependency Injection:有些容器(像 StrongInject)用生成器來寫註冊服務的程式碼。

整合外部基礎設施

有些生成器會分析外部資源(API 描述、GraphQL schema、Thrift 協定等),生成對應的 C# 類別來使用它們。這樣在契約變更時就不用手動更新程式碼。

編譯時的檢查與診斷

Source Generators 也能用來檢查程式碼:「如果專案中出現方法 X,但沒滿足條件 Y — 在編譯器給出警告!」很多 linter 和 analyzer 就是這樣工作的;生成器也可以加自訂訊息,甚至塞入一段預設的 stub 程式碼。

2. 自己的 Source Generator,做自動序列化

來看一個簡單的直觀範例:一個對帶有屬性 [AutoJson] 的類別生成 JSON 序列化方法的生成器。

下面的程式碼只是示範;實務上請使用 System.Text.Json.SourceGeneration。

// 手動撰寫:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// 生成器會加入:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Source Generator 與專案的互動流程圖

flowchart TD
    A(你的原始碼) -->|編譯器呼叫| B(Source Generator)
    B -->|新增的 Code| C(新的 .cs 檔案)
    C --> D(專案編譯)
  • 首先編譯器(Roslyn)會分析你的原始碼。
  • 然後呼叫「你的」生成器(實作了 ISourceGenerator)。
  • 生成器新增 .cs 檔案,這些檔案會成為整個編譯樹的一部分。
  • 最終組建包含你寫的程式碼和生成的程式碼。

專門任務:還可以生成些什麼

  • Native binding 的綁定層(C code 或 WinAPI 的 wrapper)。
  • AOP:自動記錄呼叫、aspect(像 Fody、PostSharp 等的思路)。
  • API 文件描述 給自動生成文件的工具使用。
  • 處理外部資源:SVG、SQL、Razor — 生成器產出 strongly-typed 類別以利型別安全的存取。

3. 用 System.Reflection.Emit 做動態生成程式碼

如果 Source Generators 在編譯前生成 C# 原始碼,那 System.Reflection.Emit 就是用來做執行期的魔法。你的程式可以在執行時自己建立新的型別、方法、甚至 assembly — 就是動態產生東西!

聽起來有點嚇人?有一點。但有時候別無選擇:像是你寫動態 proxy 的框架(AOP、profiling、mocking)、根據 runtime 資料生成的序列化器、動態 ORM 等等,就會用到它。

什麼時候需要 Reflection.Emit

  • 型別無法在事先知道(使用者在執行時定義結構)。
  • 動態 proxy(攔截呼叫的 wrapper)。
  • 高效能序列化(例如 protobuf-net)。
  • 複雜載入情境的 plugin 或 script engine。

透過 Reflection.Emit 可以建立的東西

  • AssemblyBuilder — 建立新的 assembly。
  • ModuleBuilder — assembly 裡的模組。
  • TypeBuilder — 新型別的描述。
  • MethodBuilder — 含 IL 的方法。
  • PropertyBuilder, FieldBuilder, EventBuilder — 屬性、欄位、事件。

迷你範例:在執行時組裝一個新類別

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. 建立 assembly 和 module
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. 建立一個新類別
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. 新增公開的字串屬性 Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. 完成!建立 Type
        return typeBuilder.CreateTypeInfo();
    }
}

現在可以像普通 C# 物件一樣使用這個型別,例如用 reflection:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# 人物");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# 人物

這就是 ORM、proxy、序列化器、profiler 以及某些測試框架誕生的方式。

4. 好用的小技巧

Source Generators vs Reflection.Emit:誰適合誰?

Source Generators 在編譯階段運作:讓你的原始碼更聰明,結果會進到已編譯的 assembly。它們不能根據在程式執行時才取得的資料來生成程式碼。

Reflection.Emit 在 runtime 運作:可以動態建立 assembly、型別和方法,但這類程式碼比較難除錯和維護。

什麼時候用哪一個?

生活化比喻:

  • Source Generators — 像工廠,在裝車前就生好零件。
  • Reflection.Emit — 像工程師在行駛中即時往車上焊接火箭引擎。

注意事項與坑

  • 生成的程式碼可能不好除錯。生成器通常可以把原始碼存到磁碟 — 找找看 obj\Generated 資料夾。
  • Reflection.Emit 建立的 assembly 通常在記憶體中,且不會被從 AppDomain 卸載。若是暫時性的 assembly,使用 AssemblyBuilderAccess.RunAndCollect(若支援的話)。
  • 別為簡單任務濫用 Reflection.Emit — 有時用 Source Generator 或簡單的程式模板更簡單可靠。
  • 生成器需要懂 Roslyn(對於 Source Generators)或 IL(對於 Reflection.Emit)。

Source Generators 和 Reflection.Emit

準則 Source Generators Reflection.Emit
使用階段 在編譯階段 在 runtime
結果 C# 原始碼,成為組建的一部分 IL 程式碼,新的型別/assembly
常見場景 自動生成樣板程式碼、DI、mapping、序列化 proxy、動態 ORM、特殊的 runtime pipeline
使用難度 中等,需要 Roslyn 知識 高,需要 IL 知識
IDE 與除錯支援 很好(可以看到原始檔) 比較困難
效能 非常高 可能很高

5. 實務場景

1. 生成 strongly-typed API

公司提供 OpenAPI 規格。生成器解析它並產生 controller 類別、DTO 和客戶端程式碼來安全地與 REST API 互動 — 支援 IntelliSense。

程式碼(偽):
// Spec: GET /users -> returns User[]
// Source Generator 會生成:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. 自動化 injection/DI-container(Compile-time IoC)

生成器可以為依賴註冊與物件圖建構生成 builder。你就不用手動寫 services.AddSingleton<IMyService, MyService>() 這類的註冊程式碼。

程式碼(偽):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator 會生成:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. 動態 proxy — Reflection.Emit 範例

像 Castle DynamicProxy 這類庫會建立 proxy 型別來攔截方法呼叫 — 這是 AOP、記錄、tracing、mocking 的基礎。

程式碼(簡化):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // 呼叫被攔截,可以記錄或修改結果

4. 快速序列化,避免 reflection

與其在 runtime 用 reflection 去構建型別描述(慢),Source Generators 可以事先生成序列化/反序列化程式碼 — 速度最快且沒額外開銷。

2
任務
C# SELF, 等級 63, 課堂 4
上鎖
使用 Reflection.Emit 動態建立類別
使用 Reflection.Emit 動態建立類別
1
問卷/小測驗
反思,等級 63,課堂 4
未開放
反思
反思跟動態型別
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION