Skip to content

第 2 步:理解源生成器

加载进度中...

📋 本步目标

完成本步后,你将能够:

  • 理解什么是源生成器
  • 了解源生成器的应用场景
  • 掌握源生成器的工作原理
  • 理解源生成器与反射的区别

⏱️ 预计时间

约 30 分钟


📚 学习内容

1. 什么是源生成器?

源生成器(Source Generator) 是 C# 编译器(Roslyn)的一个强大功能,它允许开发者在编译时检查代码并生成额外的 C# 源文件。

核心特点

  • 编译时执行 - 在编译过程中运行,不影响运行时性能
  • 类型安全 - 生成的代码在编译时检查,确保类型安全
  • 零运行时开销 - 不使用反射,性能优异
  • IDE 支持 - 生成的代码可以被 IntelliSense 识别
  • 可调试 - 生成的代码可以像普通代码一样调试

简单类比

想象你有一个助手,在你编译代码时:

  1. 📖 读取你写的代码
  2. 🔍 分析代码结构
  3. ✍️ 自动生成需要的样板代码
  4. 📦 将生成的代码加入编译

这就是源生成器!

2. 为什么需要源生成器?

传统方式的问题

问题 1:手动编写样板代码

csharp
// 每个类都要手动写 ToString
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    // 😫 手动编写,容易出错
    public override string ToString()
    {
        return $"Person {{ Name = {Name}, Age = {Age} }}";
    }
}

// 添加新属性时容易忘记更新 ToString
// 每个类都要重复写类似代码

问题 2:使用反射的性能开销

csharp
// 使用反射获取属性值
public string ToStringUsingReflection()
{
    var properties = GetType().GetProperties();
    var values = properties.Select(p => 
        $"{p.Name} = {p.GetValue(this)}");
    return $"{GetType().Name} {{ {string.Join(", ", values)} }}";
}

// ❌ 问题:
// - 运行时开销大
// - 类型不安全
// - 难以调试

源生成器的解决方案

csharp
// 只需要添加特性
[GenerateToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// ✅ 源生成器自动生成:
// - 编译时生成代码
// - 零运行时开销
// - 类型安全
// - IDE 可以识别

3. 源生成器的工作原理

源生成器在编译流程中的位置:

详细步骤

  1. 编译器解析代码

    • 将源代码解析为语法树(Syntax Tree)
    • 构建语义模型(Semantic Model)
  2. 调用源生成器

    • 编译器将语法树和语义模型传递给生成器
    • 生成器可以读取和分析代码
  3. 生成新代码

    • 生成器创建新的 C# 代码
    • 返回给编译器
  4. 编译所有代码

    • 编译器将原始代码和生成的代码一起编译
    • 生成最终的程序集

4. 应用场景

源生成器适用于以下场景:

场景 1:自动生成样板代码

csharp
// ToString 生成器
[GenerateToString]
public partial class User { }

// Equals/GetHashCode 生成器
[GenerateEquality]
public partial class Product { }

// Builder 模式生成器
[GenerateBuilder]
public partial class Configuration { }

场景 2:序列化/反序列化

csharp
// JSON 序列化器
[JsonSerializable]
public partial class ApiResponse { }

// 二进制序列化器
[BinarySerializable]
public partial class CacheData { }

场景 3:依赖注入

csharp
// 自动注册服务
[AutoRegister(ServiceLifetime.Scoped)]
public class UserService : IUserService { }

场景 4:ORM 映射

csharp
// 数据库映射
[Table("Users")]
public partial class User
{
    [Column("user_id")]
    public int Id { get; set; }
}

场景 5:代码验证

csharp
// 编译时验证
[ValidateNotNull]
public partial class Config
{
    public string ConnectionString { get; set; }
}

5. 源生成器 vs 反射

特性源生成器反射
执行时机编译时运行时
性能零开销有开销
类型安全编译时检查运行时检查
IDE 支持完全支持有限支持
调试容易困难
代码可见可见不可见

性能对比示例

csharp
// 反射方式(慢)
var value = typeof(Person)
    .GetProperty("Name")
    .GetValue(person);

// 源生成器方式(快)
var value = person.Name;  // 直接访问

✅ 检查点

✅ 检查点

完成以下任务后,可以进入下一步:


🎯 小测验

测试一下你的理解:

  1. 源生成器在什么时候运行?

    查看答案 编译时(Compile Time)。源生成器在编译过程中运行,生成的代码会被一起编译到最终的程序集中。
  2. 源生成器相比反射有什么优势?

    查看答案

    主要优势:

    • 零运行时开销(编译时生成)
    • 类型安全(编译时检查)
    • IDE 支持更好
    • 更容易调试
    • 生成的代码可见
  3. 列举 3 个源生成器的应用场景

    查看答案

    常见场景:

    1. 自动生成样板代码(ToString、Equals 等)
    2. 序列化/反序列化
    3. 依赖注入自动注册
    4. ORM 数据库映射
    5. 代码验证

💡 常见问题

Q: 源生成器会增加编译时间吗?

A: 会有一些影响,但通常很小。源生成器在编译时运行,但现代编译器有增量编译优化,只有相关代码改变时才会重新运行生成器。

Q: 生成的代码存储在哪里?

A: 生成的代码存储在编译输出目录的临时文件夹中(通常是 obj 目录)。你可以在 IDE 中查看生成的代码,但不应该手动修改它们。

Q: 源生成器可以修改现有代码吗?

A: 不可以。源生成器只能添加新代码,不能修改或删除现有代码。这是一个重要的设计限制,确保了代码的可预测性。

Q: 什么时候应该使用源生成器?

A: 当你需要:

  • 自动生成重复的样板代码
  • 避免反射的性能开销
  • 在编译时进行代码验证
  • 基于代码结构生成相关代码

不适合用于复杂的业务逻辑或需要运行时动态性的场景。


📖 延伸阅读


⏭️ 下一步

现在你已经理解了源生成器的概念和原理,下一步我们将动手创建第一个 Hello World 源生成器!

基于 MIT 许可发布