Skip to content

编译时 vs 运行时

理解编译时代码生成和运行时反射的区别

概述

在 .NET 开发中,有两种主要的代码生成和类型检查方式:

  1. 编译时 (Compile-time): 在代码编译阶段执行
  2. 运行时 (Runtime): 在程序运行阶段执行

理解这两种方式的区别对于选择正确的技术方案至关重要。源生成器是编译时技术,而反射是运行时技术。

核心区别

维度编译时运行时
执行时机编译期间程序运行期间
性能开销零(已编译为 IL)有(需要查询和调用)
类型安全强(编译器检查)弱(运行时错误)
灵活性低(编译时确定)高(动态决策)
调试难度低(可查看生成的代码)高(动态执行)
AOT 兼容性完全兼容有限(需要特殊处理)

编译时代码生成

什么是编译时代码生成?

编译时代码生成是指在编译过程中自动生成代码的技术。生成的代码会被编译器处理,成为最终程序集的一部分。

编译时的优势

1. 零运行时开销

csharp
// 编译时生成的代码
public class Person
{
    public string Name { get; set; }
    
    // 源生成器生成的 ToString 方法
    public override string ToString()
    {
        return $"Person {{ Name = {Name} }}";
    }
}

// 运行时调用(快速)
var person = new Person { Name = "Alice" };
var str = person.ToString(); // 直接调用,无反射开销

生成的代码已经编译为 IL,调用时没有额外开销。

2. 编译时类型检查

csharp
// 如果生成的代码有错误,编译器会报错
[GenerateToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// 编译时错误示例:
// Error CS0103: The name 'Namee' does not exist in the current context
// 如果生成器错误地使用了 'Namee' 而不是 'Name'

3. IDE 支持

csharp
// IDE 可以提供智能提示
var person = new Person();
person.Name = "Alice"; // ✅ 智能提示
person.ToString();     // ✅ 智能提示显示生成的方法

// 可以 F12 跳转到生成的代码
// 可以在调试时单步进入生成的代码

4. AOT 编译兼容

csharp
// 源生成器生成的代码完全兼容 AOT 编译
// 不需要运行时 JIT 编译
// 适合 iOS、WebAssembly 等平台

编译时的局限性

1. 只能添加代码,不能修改

csharp
// ✅ 可以:添加新方法
public partial class Person
{
    public string Name { get; set; }
}

// 源生成器添加:
// public override string ToString() { ... }

// ❌ 不可以:修改现有方法
// 源生成器不能修改 Name 属性的实现

2. 需要编译时信息

csharp
// ✅ 可以:基于编译时已知的类型
[GenerateBuilder]
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// ❌ 不可以:基于运行时才知道的类型
// 无法为运行时动态加载的类型生成代码

3. 增加编译时间

csharp
// 复杂的源生成器可能增加编译时间
// 但增量生成器可以最小化影响

编译时技术

  1. 源生成器 (Source Generators)

    • C# 9.0+ 的官方功能
    • 集成到编译管道
    • 本项目的重点
  2. T4 模板 (Text Template Transformation Toolkit)

    • Visual Studio 的传统工具
    • 在编译前运行
    • 生成独立文件
  3. MSBuild 任务

    • 自定义构建步骤
    • 可以生成代码文件
    • 灵活但复杂

运行时反射

什么是运行时反射?

反射是 .NET 提供的在运行时检查和操作类型信息的能力。它允许程序在运行时动态地发现类型、调用方法、访问属性等。

运行时反射的优势

1. 动态性

csharp
// 可以处理编译时未知的类型
public object CreateInstance(string typeName)
{
    var type = Type.GetType(typeName);
    return Activator.CreateInstance(type);
}

// 运行时决定调用哪个方法
public void CallMethod(object obj, string methodName)
{
    var method = obj.GetType().GetMethod(methodName);
    method?.Invoke(obj, null);
}

2. 灵活性

csharp
// 可以处理任意类型
public void PrintProperties(object obj)
{
    var type = obj.GetType();
    var properties = type.GetProperties();
    
    foreach (var prop in properties)
    {
        var value = prop.GetValue(obj);
        Console.WriteLine($"{prop.Name} = {value}");
    }
}

// 适用于任何对象
PrintProperties(new Person { Name = "Alice" });
PrintProperties(new Product { Name = "Book", Price = 29.99 });

3. 插件系统

csharp
// 动态加载程序集
var assembly = Assembly.LoadFrom("Plugin.dll");
var types = assembly.GetTypes();

// 查找实现特定接口的类型
var pluginTypes = types.Where(t => 
    typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);

// 创建实例
foreach (var type in pluginTypes)
{
    var plugin = (IPlugin)Activator.CreateInstance(type);
    plugin.Execute();
}

运行时反射的局限性

1. 性能开销

csharp
// 反射调用(慢)
var type = obj.GetType();
var property = type.GetProperty("Name");
var value = property.GetValue(obj); // ~500 ns

// 直接调用(快)
var value = person.Name; // ~5 ns

// 性能差距:100x

2. 类型安全性弱

csharp
// 编译时不检查,运行时可能出错
var method = type.GetMethod("NonExistentMethod"); // null
method.Invoke(obj, null); // NullReferenceException

// 字符串错误不会被编译器发现
var property = type.GetProperty("Namee"); // 拼写错误,返回 null

3. AOT 编译问题

csharp
// 在 AOT 编译环境中,反射可能不工作
// 需要特殊配置保留类型信息

// iOS、WebAssembly 等平台可能需要:
[Preserve]
public class MyClass { }

// 或使用 ILLinker 配置文件

4. 调试困难

csharp
// 反射调用的堆栈跟踪不清晰
try
{
    method.Invoke(obj, null);
}
catch (Exception ex)
{
    // 异常堆栈包含反射层,难以定位问题
    Console.WriteLine(ex.StackTrace);
}

性能分析

基准测试对比

让我们通过实际的基准测试来对比编译时和运行时的性能差异。

场景 1: 属性访问

csharp
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// 方法 1: 直接访问(编译时)
public string GetNameDirect(Person person)
{
    return person.Name;
}

// 方法 2: 反射访问(运行时)
public string GetNameReflection(Person person)
{
    var property = person.GetType().GetProperty("Name");
    return (string)property.GetValue(person);
}

// 方法 3: 缓存反射(运行时优化)
private static readonly PropertyInfo NameProperty = 
    typeof(Person).GetProperty("Name");

public string GetNameCachedReflection(Person person)
{
    return (string)NameProperty.GetValue(person);
}

性能结果(示意):

方法时间相对速度
直接访问0.5 ns1x (基准)
缓存反射50 ns100x 慢
反射访问500 ns1000x 慢

场景 2: 对象创建

csharp
// 方法 1: 直接创建(编译时)
public Person CreateDirect()
{
    return new Person { Name = "Alice", Age = 30 };
}

// 方法 2: 反射创建(运行时)
public Person CreateReflection()
{
    var type = typeof(Person);
    var person = (Person)Activator.CreateInstance(type);
    
    var nameProperty = type.GetProperty("Name");
    nameProperty.SetValue(person, "Alice");
    
    var ageProperty = type.GetProperty("Age");
    ageProperty.SetValue(person, 30);
    
    return person;
}

// 方法 3: 源生成器(编译时)
// 生成的 Builder 模式
public Person CreateBuilder()
{
    return Person.CreateBuilder()
        .WithName("Alice")
        .WithAge(30)
        .Build();
}

性能结果(示意):

方法时间相对速度
直接创建10 ns1x (基准)
Builder 模式15 ns1.5x 慢
反射创建1000 ns100x 慢

场景 3: JSON 序列化

csharp
// 方法 1: 反射序列化(运行时)
var options = new JsonSerializerOptions();
var json = JsonSerializer.Serialize(person, options);
// 使用反射遍历属性

// 方法 2: 源生成器序列化(编译时)
var options = new JsonSerializerOptions
{
    TypeInfoResolver = MyJsonContext.Default
};
var json = JsonSerializer.Serialize(person, options);
// 使用生成的代码,无反射

[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext { }

性能结果(实际测试):

方法时间内存分配
源生成器400 ns344 B
反射650 ns472 B
提升40%+27%+

内存分配对比

csharp
// 编译时生成的代码:零额外分配
public string GetName(Person person)
{
    return person.Name; // 直接访问
}

// 运行时反射:多次分配
public string GetName(Person person)
{
    var type = person.GetType();        // 可能分配
    var property = type.GetProperty("Name"); // 可能分配
    var value = property.GetValue(person);   // 装箱分配(如果是值类型)
    return (string)value;
}

何时性能差异重要?

高频调用场景

csharp
// ❌ 不好:在循环中使用反射
for (int i = 0; i < 1000000; i++)
{
    var value = property.GetValue(obj); // 每次都有开销
}

// ✅ 好:使用编译时生成的代码
for (int i = 0; i < 1000000; i++)
{
    var value = person.Name; // 零开销
}

性能关键路径

csharp
// Web API 请求处理
public IActionResult GetUser(int id)
{
    var user = _repository.GetById(id);
    
    // ❌ 不好:使用反射序列化(慢)
    var json = ReflectionSerializer.Serialize(user);
    
    // ✅ 好:使用源生成器序列化(快)
    var json = JsonSerializer.Serialize(user, MyJsonContext.Default.User);
    
    return Content(json, "application/json");
}

启动时间敏感

csharp
// 移动应用或 Serverless 函数
// 启动时间很重要

// ❌ 不好:启动时扫描和反射
public void Startup()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();
    foreach (var type in types)
    {
        // 反射检查和注册
    }
}

// ✅ 好:使用源生成器预生成注册代码
public void Startup()
{
    // 生成的注册代码,无反射
    GeneratedServiceRegistration.Register(services);
}

使用场景对比

何时使用编译时生成(源生成器)

适合的场景

  1. 性能关键的代码

    csharp
    // JSON 序列化、日志记录、数据访问
    [JsonSerializable(typeof(User))]
    public partial class MyJsonContext : JsonSerializerContext { }
  2. 重复的代码模式

    csharp
    // INotifyPropertyChanged、Builder 模式、ToString
    [AutoNotify]
    public partial class ViewModel
    {
        private string _name;
        private int _age;
    }
  3. 类型在编译时已知

    csharp
    // 实体类、DTO、配置类
    [GenerateBuilder]
    public class UserDto
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }
  4. 需要 AOT 编译

    csharp
    // iOS、WebAssembly、Native AOT
    // 源生成器完全兼容
  5. 需要强类型安全

    csharp
    // 编译时检查,IDE 智能提示
    var user = User.CreateBuilder()
        .WithName("Alice")  // ✅ 编译时检查
        .WithEmail("alice@example.com")
        .Build();

何时使用运行时反射

适合的场景

  1. 处理未知类型

    csharp
    // 插件系统、动态加载
    public void LoadPlugin(string assemblyPath)
    {
        var assembly = Assembly.LoadFrom(assemblyPath);
        var types = assembly.GetTypes()
            .Where(t => typeof(IPlugin).IsAssignableFrom(t));
        // ...
    }
  2. 需要运行时灵活性

    csharp
    // 配置驱动的行为
    public void ExecuteAction(string actionName)
    {
        var method = this.GetType().GetMethod(actionName);
        method?.Invoke(this, null);
    }
  3. 通用工具和框架

    csharp
    // 对象映射、验证框架
    public void Validate(object obj)
    {
        var properties = obj.GetType().GetProperties();
        foreach (var prop in properties)
        {
            var attributes = prop.GetCustomAttributes<ValidationAttribute>();
            // 验证逻辑
        }
    }
  4. 简单的场景

    csharp
    // 不频繁调用,性能不关键
    public void LogObject(object obj)
    {
        var properties = obj.GetType().GetProperties();
        foreach (var prop in properties)
        {
            Console.WriteLine($"{prop.Name} = {prop.GetValue(obj)}");
        }
    }
  5. 需要处理动态数据

    csharp
    // 从数据库或 API 获取的动态结构
    public void ProcessDynamicData(Dictionary<string, object> data)
    {
        foreach (var kvp in data)
        {
            // 动态处理
        }
    }

混合使用

在实际项目中,通常会混合使用两种技术:

csharp
// 编译时:为已知类型生成高性能代码
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Product))]
public partial class MyJsonContext : JsonSerializerContext { }

// 运行时:处理未知类型
public string SerializeObject(object obj)
{
    // 检查是否有生成的序列化器
    if (obj is User user)
    {
        return JsonSerializer.Serialize(user, MyJsonContext.Default.User);
    }
    else if (obj is Product product)
    {
        return JsonSerializer.Serialize(product, MyJsonContext.Default.Product);
    }
    else
    {
        // 回退到反射
        return JsonSerializer.Serialize(obj);
    }
}

迁移策略

从反射迁移到源生成器

步骤 1: 识别性能瓶颈

csharp
// 使用性能分析工具(如 BenchmarkDotNet)
// 找出反射调用的热点

[Benchmark]
public void ReflectionVersion()
{
    // 现有的反射代码
}

[Benchmark]
public void SourceGeneratorVersion()
{
    // 使用源生成器的新代码
}

步骤 2: 逐步替换

csharp
// 不要一次性替换所有代码
// 从最影响性能的部分开始

// 阶段 1: 替换 JSON 序列化
// 阶段 2: 替换日志记录
// 阶段 3: 替换其他反射使用

步骤 3: 保持兼容性

csharp
// 提供两种实现,逐步迁移
public interface ISerializer
{
    string Serialize(object obj);
}

public class ReflectionSerializer : ISerializer
{
    // 旧的反射实现
}

public class SourceGeneratorSerializer : ISerializer
{
    // 新的源生成器实现
}

// 配置中切换
services.AddSingleton<ISerializer, SourceGeneratorSerializer>();

步骤 4: 测试和验证

csharp
// 确保行为一致
[Fact]
public void BothSerializersProduceSameResult()
{
    var obj = new User { Name = "Alice", Age = 30 };
    
    var reflectionResult = reflectionSerializer.Serialize(obj);
    var generatorResult = generatorSerializer.Serialize(obj);
    
    Assert.Equal(reflectionResult, generatorResult);
}

总结

关键要点

  1. 编译时生成(源生成器)

    • ✅ 性能优秀(零运行时开销)
    • ✅ 类型安全(编译时检查)
    • ✅ IDE 支持好
    • ✅ AOT 兼容
    • ❌ 灵活性低(编译时确定)
    • ❌ 学习曲线陡
  2. 运行时反射

    • ✅ 灵活性高(动态决策)
    • ✅ 处理未知类型
    • ✅ 简单易用
    • ❌ 性能开销大
    • ❌ 类型安全弱
    • ❌ AOT 兼容性差

决策树

需要处理未知类型?
├─ 是 → 使用反射
└─ 否 → 性能关键?
    ├─ 是 → 使用源生成器
    └─ 否 → 调用频率高?
        ├─ 是 → 使用源生成器
        └─ 否 → 两者都可以(推荐源生成器)

最佳实践

  1. 优先考虑源生成器:对于新项目和已知类型
  2. 保留反射:对于动态场景和插件系统
  3. 混合使用:根据具体场景选择合适的技术
  4. 性能测试:使用基准测试验证性能改进
  5. 逐步迁移:不要一次性重写所有代码

下一步: 开始学习 示例 1: Hello World 源生成器 实践编译时代码生成。

基于 MIT 许可发布