编译时 vs 运行时
理解编译时代码生成和运行时反射的区别
概述
在 .NET 开发中,有两种主要的代码生成和类型检查方式:
- 编译时 (Compile-time): 在代码编译阶段执行
- 运行时 (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
// 复杂的源生成器可能增加编译时间
// 但增量生成器可以最小化影响编译时技术
源生成器 (Source Generators)
- C# 9.0+ 的官方功能
- 集成到编译管道
- 本项目的重点
T4 模板 (Text Template Transformation Toolkit)
- Visual Studio 的传统工具
- 在编译前运行
- 生成独立文件
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
// 性能差距:100x2. 类型安全性弱
csharp
// 编译时不检查,运行时可能出错
var method = type.GetMethod("NonExistentMethod"); // null
method.Invoke(obj, null); // NullReferenceException
// 字符串错误不会被编译器发现
var property = type.GetProperty("Namee"); // 拼写错误,返回 null3. 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 ns | 1x (基准) |
| 缓存反射 | 50 ns | 100x 慢 |
| 反射访问 | 500 ns | 1000x 慢 |
场景 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 ns | 1x (基准) |
| Builder 模式 | 15 ns | 1.5x 慢 |
| 反射创建 | 1000 ns | 100x 慢 |
场景 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 ns | 344 B |
| 反射 | 650 ns | 472 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);
}使用场景对比
何时使用编译时生成(源生成器)
✅ 适合的场景:
性能关键的代码
csharp// JSON 序列化、日志记录、数据访问 [JsonSerializable(typeof(User))] public partial class MyJsonContext : JsonSerializerContext { }重复的代码模式
csharp// INotifyPropertyChanged、Builder 模式、ToString [AutoNotify] public partial class ViewModel { private string _name; private int _age; }类型在编译时已知
csharp// 实体类、DTO、配置类 [GenerateBuilder] public class UserDto { public string Name { get; set; } public string Email { get; set; } }需要 AOT 编译
csharp// iOS、WebAssembly、Native AOT // 源生成器完全兼容需要强类型安全
csharp// 编译时检查,IDE 智能提示 var user = User.CreateBuilder() .WithName("Alice") // ✅ 编译时检查 .WithEmail("alice@example.com") .Build();
何时使用运行时反射
✅ 适合的场景:
处理未知类型
csharp// 插件系统、动态加载 public void LoadPlugin(string assemblyPath) { var assembly = Assembly.LoadFrom(assemblyPath); var types = assembly.GetTypes() .Where(t => typeof(IPlugin).IsAssignableFrom(t)); // ... }需要运行时灵活性
csharp// 配置驱动的行为 public void ExecuteAction(string actionName) { var method = this.GetType().GetMethod(actionName); method?.Invoke(this, null); }通用工具和框架
csharp// 对象映射、验证框架 public void Validate(object obj) { var properties = obj.GetType().GetProperties(); foreach (var prop in properties) { var attributes = prop.GetCustomAttributes<ValidationAttribute>(); // 验证逻辑 } }简单的场景
csharp// 不频繁调用,性能不关键 public void LogObject(object obj) { var properties = obj.GetType().GetProperties(); foreach (var prop in properties) { Console.WriteLine($"{prop.Name} = {prop.GetValue(obj)}"); } }需要处理动态数据
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);
}总结
关键要点
编译时生成(源生成器)
- ✅ 性能优秀(零运行时开销)
- ✅ 类型安全(编译时检查)
- ✅ IDE 支持好
- ✅ AOT 兼容
- ❌ 灵活性低(编译时确定)
- ❌ 学习曲线陡
运行时反射
- ✅ 灵活性高(动态决策)
- ✅ 处理未知类型
- ✅ 简单易用
- ❌ 性能开销大
- ❌ 类型安全弱
- ❌ AOT 兼容性差
决策树
需要处理未知类型?
├─ 是 → 使用反射
└─ 否 → 性能关键?
├─ 是 → 使用源生成器
└─ 否 → 调用频率高?
├─ 是 → 使用源生成器
└─ 否 → 两者都可以(推荐源生成器)最佳实践
- 优先考虑源生成器:对于新项目和已知类型
- 保留反射:对于动态场景和插件系统
- 混合使用:根据具体场景选择合适的技术
- 性能测试:使用基准测试验证性能改进
- 逐步迁移:不要一次性重写所有代码
下一步: 开始学习 示例 1: Hello World 源生成器 实践编译时代码生成。