1. 为什么需要 dynamic 类型?
在 C# 4.0 之前,我们主要使用静态类型系统。静态类型在编译时就能捕获类型错误,提供智能提示和更好的性能。但有些场景下,我们需要更灵活的类型处理:
- 处理 COM 对象(如 Office 自动化)
- 与动态语言交互(IronPython, IronRuby)
- 处理 JSON/XML 等弱类型数据
- 实现动态代理和 AOP
dynamic 关键字就是为此设计的运行时绑定机制。它告诉编译器:"这个对象的成员解析推迟到运行时"。
注意:dynamic 不等于 var。var 是编译时类型推断,而 dynamic 是完全的运行时绑定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. dynamic 的核心工作机制
2.1 DLR(动态语言运行时)
dynamic 的背后是 DLR,它包含三个关键组件:
- 表达式树:将操作表示为数据结构
- 调用点缓存:缓存方法绑定结果
- 动态对象互操作:统一动态对象模型
当调用 dynamic 对象时:
csharp复制dynamic obj = GetDynamicObject();
obj.DoSomething(); // 运行时绑定
编译器会生成调用点(Call Site),DLR 会:
- 检查缓存
- 若无缓存,执行绑定
- 生成规则并缓存
- 执行操作
2.2 与反射的性能对比
虽然反射也能实现动态调用,但 dynamic 有显著优势:
| 特性 | Reflection | dynamic |
|---|---|---|
| 语法简洁性 | 差 | 优 |
| 性能 | 差 | 较好 |
| 缓存机制 | 无 | 有 |
| 代码可读性 | 差 | 优 |
实测数据(百万次调用):
- 直接调用:~15ms
- dynamic:~180ms
- 反射:~1200ms
3. 实战应用场景
3.1 Office 自动化
传统方式需要繁琐的 COM 互操作:
csharp复制var excel = new Microsoft.Office.Interop.Excel.Application();
excel.Visible = true;
Workbook workbook = excel.Workbooks.Add();
使用 dynamic 后:
csharp复制dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true;
dynamic workbook = excel.Workbooks.Add();
3.2 JSON 处理
Newtonsoft.Json 的经典用法:
csharp复制var obj = JObject.Parse(json);
string name = obj["person"]["name"].Value<string>();
使用 dynamic 更直观:
csharp复制dynamic obj = JsonConvert.DeserializeObject(json);
string name = obj.person.name;
3.3 动态构建对象
csharp复制dynamic expando = new ExpandoObject();
expando.Name = "Test";
expando.Print = (Action)(() => Console.WriteLine(expando.Name));
expando.Print(); // 输出 "Test"
4. 高级技巧与陷阱
4.1 性能优化
- 缓存动态调用:
csharp复制// 不好的做法
for(int i=0; i<10000; i++) {
dynamic result = obj.Calculate(i);
}
// 优化方案
var func = (Func<int, dynamic>)obj.Calculate;
for(int i=0; i<10000; i++) {
dynamic result = func(i);
}
- 避免频繁类型切换:
csharp复制// 反模式
dynamic value = GetValue();
if(value is int) {
// ...
} else if(value is string) {
// ...
}
// 更好的方式
var staticValue = (object)value;
if(staticValue is int num) {
// ...
}
4.2 常见陷阱
- 丢失编译时检查:
csharp复制dynamic text = "hello";
text.ToLwerCase(); // 运行时错误
- 重载解析问题:
csharp复制void Process(int num) { }
void Process(string text) { }
dynamic value = GetValue();
Process(value); // 运行时才能确定调用哪个重载
- 调试困难:
- 断点时看不到动态成员
- 调用栈信息不完整
5. 最佳实践指南
-
使用范围建议:
- 优先用于明确的动态场景(COM/JSON)
- 避免在核心业务逻辑中滥用
-
类型安全建议:
csharp复制// 不安全
dynamic result = GetResult();
int sum = result.Value1 + result.Value2;
// 更安全
dynamic result = GetResult();
try {
int sum = (int)result.Value1 + (int)result.Value2;
} catch(RuntimeBinderException ex) {
// 处理类型错误
}
- 与静态类型混合:
csharp复制public static T DynamicToStatic<T>(dynamic obj) {
return (T)obj;
}
var person = DynamicToStatic<Person>(dynamicObj);
6. 真实案例:构建动态查询引擎
假设我们需要实现一个类似 LINQ 但更动态的查询接口:
csharp复制public class DynamicQuery
{
private readonly List<dynamic> _source;
public DynamicQuery(IEnumerable<dynamic> source) {
_source = source.ToList();
}
public DynamicQuery Where(string propertyName, Func<dynamic, bool> predicate) {
return new DynamicQuery(_source.Where(item => predicate(item[propertyName])));
}
public DynamicQuery OrderBy(string propertyName) {
return new DynamicQuery(_source.OrderBy(item => item[propertyName]));
}
public List<dynamic> ToList() => _source;
}
// 使用示例
var data = new List<dynamic> {
new { Id = 1, Name = "Alice" },
new { Id = 2, Name = "Bob" }
};
var results = new DynamicQuery(data)
.Where("Id", id => (int)id > 1)
.OrderBy("Name")
.ToList();
这个实现展示了如何:
- 动态访问属性
- 构建流畅接口
- 保持类型安全边界
7. 单元测试策略
测试 dynamic 代码需要特殊处理:
csharp复制[TestMethod]
public void TestDynamicOperation()
{
dynamic calculator = new Calculator();
// 测试正常情况
Assert.AreEqual(5, calculator.Add(2, 3));
// 测试动态成员缺失
try {
calculator.NonExistentMethod();
Assert.Fail("应该抛出异常");
} catch(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) {
// 预期异常
}
// 测试类型不匹配
try {
calculator.Add("a", "b");
Assert.Fail("应该抛出异常");
} catch(Exception ex) when (ex is RuntimeBinderException || ex is InvalidCastException) {
// 预期异常
}
}
关键测试点:
- 动态成员存在性
- 参数类型兼容性
- 返回值类型转换
- 异常情况处理
8. 与其它动态特性的对比
8.1 dynamic vs ExpandoObject
| 特性 | dynamic 关键字 | ExpandoObject |
|---|---|---|
| 主要用途 | 动态绑定 | 动态添加成员 |
| 是否可添加成员 | 否 | 是 |
| 实现接口 | 无 | IDynamicMetaObjectProvider |
| 典型应用场景 | COM互操作 | 运行时构建对象 |
8.2 dynamic vs 反射
csharp复制// 反射方式
MethodInfo method = typeof(MyClass).GetMethod("Calculate");
object result = method.Invoke(instance, new object[] { arg1, arg2 });
// dynamic方式
dynamic dynamicInstance = instance;
var result = dynamicInstance.Calculate(arg1, arg2);
dynamic 的优势:
- 代码更简洁
- 有调用缓存
- 支持运算符重载
9. 性能敏感场景的替代方案
当 dynamic 成为性能瓶颈时,可以考虑:
- 表达式树编译:
csharp复制var param = Expression.Parameter(typeof(MyType));
var propAccess = Expression.Property(param, "Name");
var lambda = Expression.Lambda<Func<MyType, string>>(propAccess, param);
var func = lambda.Compile();
string name = func(myInstance); // 快如直接调用
- Emit 动态生成代码:
csharp复制var method = new DynamicMethod("GetName", typeof(string), new[] { typeof(MyType) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, typeof(MyType).GetProperty("Name").GetMethod);
il.Emit(OpCodes.Ret);
var func = (Func<MyType, string>)method.CreateDelegate(typeof(Func<MyType, string>));
- 预编译模板:
使用 RazorEngine 或类似库预编译动态模板。
10. 设计模式中的应用
10.1 动态装饰器模式
csharp复制public static dynamic Decorate(dynamic target, Action<dynamic> preAction = null, Action<dynamic> postAction = null)
{
return new DecoratorProxy(target, preAction, postAction);
}
private class DecoratorProxy : DynamicObject
{
private readonly dynamic _target;
private readonly Action<dynamic> _preAction;
private readonly Action<dynamic> _postAction;
public DecoratorProxy(dynamic target, Action<dynamic> preAction, Action<dynamic> postAction)
{
_target = target;
_preAction = preAction;
_postAction = postAction;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
_preAction?.Invoke(_target);
result = binder.Invoke(_target, args);
_postAction?.Invoke(_target);
return true;
}
}
// 使用示例
dynamic service = new MyService();
dynamic decorated = Decorate(service,
pre: s => Console.WriteLine($"Before {s.GetType().Name}"),
post: s => Console.WriteLine($"After {s.GetType().Name}")
);
decorated.Process(); // 会自动调用前后钩子
10.2 动态策略模式
csharp复制public class DynamicStrategy
{
private readonly dynamic _strategy;
public DynamicStrategy(dynamic strategy) {
_strategy = strategy;
}
public dynamic Execute(params object[] args) {
if(_strategy == null) throw new InvalidOperationException("Strategy not set");
try {
return _strategy.Execute(args);
} catch(RuntimeBinderException) {
// 尝试直接调用
return _strategy(args);
}
}
}
// 使用示例
dynamic addStrategy = new Func<int[], int>(nums => nums.Sum());
dynamic concatStrategy = new Func<string[], string>(strings => string.Join("", strings));
var processor = new DynamicStrategy(addStrategy);
Console.WriteLine(processor.Execute(1, 2, 3)); // 输出 6
processor = new DynamicStrategy(concatStrategy);
Console.WriteLine(processor.Execute("a", "b", "c")); // 输出 "abc"
11. 动态类型与并发
使用 dynamic 在多线程环境下需要特别注意:
-
DLR 的线程安全性:
- 调用点缓存是线程安全的
- 但动态对象本身可能不是
-
安全模式:
csharp复制// 不安全
dynamic shared = new ExpandoObject();
Parallel.For(0, 1000, i => {
shared.Count = i; // 竞态条件
});
// 安全方案1:使用锁
var syncLock = new object();
dynamic shared = new ExpandoObject();
Parallel.For(0, 1000, i => {
lock(syncLock) {
shared.Count = i;
}
});
// 安全方案2:线程本地存储
dynamic threadLocal = new ThreadLocal<dynamic>(() => new ExpandoObject());
Parallel.For(0, 1000, i => {
threadLocal.Value.Count = i;
});
- 性能考虑:
- 动态调用本身有开销
- 在高并发场景要谨慎使用
- 考虑预编译或缓存调用委托
12. 动态代码生成进阶
使用 Roslyn 实现更强大的动态代码生成:
csharp复制using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
public static T CompileAndCreate<T>(string code) where T : class
{
var syntaxTree = CSharpSyntaxTree.ParseText($@"
using System;
public class DynamicGenerated {{
{code}
}}
");
var references = new MetadataReference[] {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(T).Assembly.Location)
};
var compilation = CSharpCompilation.Create(
"DynamicAssembly",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var ms = new MemoryStream();
EmitResult result = compilation.Emit(ms);
if(!result.Success) {
throw new InvalidOperationException("编译失败: " +
string.Join(", ", result.Diagnostics));
}
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
return (T)Activator.CreateInstance(assembly.GetType("DynamicGenerated"));
}
// 使用示例
var calculator = CompileAndCreate<dynamic>(@"
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
");
Console.WriteLine(calculator.Add(5, 3)); // 输出 8
这种方式的优势:
- 完整的编译时检查
- 支持复杂逻辑
- 性能接近静态代码
- 可以动态引用其他类型
13. 诊断与调试技巧
13.1 日志记录动态调用
csharp复制public class LoggingDynamicProxy : DynamicObject
{
private readonly dynamic _target;
private readonly Action<string> _logger;
public LoggingDynamicProxy(dynamic target, Action<string> logger) {
_target = target;
_logger = logger;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
_logger?.Invoke($"调用 {binder.Name},参数: {string.Join(", ", args)}");
try {
result = binder.Invoke(_target, args);
_logger?.Invoke($"调用成功,结果: {result}");
return true;
} catch(Exception ex) {
_logger?.Invoke($"调用失败: {ex.Message}");
throw;
}
}
}
// 使用示例
dynamic service = new MyService();
dynamic loggedService = new LoggingDynamicProxy(service, Console.WriteLine);
loggedService.Process("test"); // 会自动记录调用信息
13.2 调试动态代码
-
条件断点:
- 在可能抛出 RuntimeBinderException 的地方设置断点
- 条件设置为
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException != null
-
即时窗口检查:
- 调试时可以在即时窗口转换类型:
csharp复制(object)dynamicObj // 查看实际类型
- 调试时可以在即时窗口转换类型:
-
自定义调试可视化工具:
实现 DebuggerTypeProxy 来更好地显示动态对象:csharp复制[DebuggerTypeProxy(typeof(DynamicObjectDebugView))] public class MyDynamicObject : DynamicObject { ... } class DynamicObjectDebugView { private readonly dynamic _obj; public DynamicObjectDebugView(dynamic obj) { _obj = obj; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public Dictionary<string, object> Properties { get { var dict = new Dictionary<string, object>(); foreach(var prop in (IDictionary<string, object>)_obj) { dict.Add(prop.Key, prop.Value); } return dict; } } }
14. 兼容性考虑
14.1 跨平台问题
在 .NET Core/.NET 5+ 上:
- DLR 是完整支持的
- 但某些 COM 互操作场景可能受限
14.2 版本兼容性
| C# 版本 | dynamic 支持情况 |
|---|---|
| 4.0 | 基本功能 |
| 5.0 | 改进异常处理 |
| 6.0+ | 更好的性能 |
14.3 AOT 编译限制
在 iOS 等 AOT 编译平台:
- 动态代码生成受限
- 需要预注册所有可能的调用
- 考虑使用预编译的表达式树替代
15. 安全注意事项
-
注入风险:
csharp复制// 危险!可能执行任意代码 dynamic script = GetUserProvidedScript(); script.Execute(); // 安全方案:在沙箱中运行 var permissionSet = new PermissionSet(PermissionState.None); var sandbox = new AppDomainSetup { ApplicationBase = Directory.GetCurrentDirectory() }; var secureDomain = AppDomain.CreateDomain("Sandbox", null, sandbox, permissionSet); dynamic safeScript = secureDomain.CreateInstanceAndUnwrap(...); -
类型转换安全:
csharp复制dynamic input = GetUserInput(); // 不安全 int value = input.Number; // 安全方式 if(input is IDictionary<string, object> dict && dict.TryGetValue("Number", out var num)) { int value = Convert.ToInt32(num); } -
序列化风险:
- 避免直接反序列化到 dynamic
- 使用 JsonSerializerSettings 限制类型
16. 性能优化深度解析
16.1 DLR 缓存机制
DLR 使用三级缓存:
- 调用点缓存:存储最近使用的绑定
- 规则缓存:存储通用绑定规则
- 动态站点缓存:存储整个表达式树
缓存键包含:
- 调用点类型
- 操作类型(方法调用/属性访问等)
- 参数类型组合
可以通过自定义 Binder 影响缓存行为:
csharp复制public class CustomBinder : CallSiteBinder
{
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
// 自定义绑定逻辑
}
}
16.2 基准测试对比
使用 BenchmarkDotNet 测试不同场景:
csharp复制[MemoryDiagnoser]
public class DynamicBenchmarks
{
private static readonly dynamic DynamicMath = new MathOperations();
private static readonly MathOperations StaticMath = new MathOperations();
private static readonly Func<int, int, int> CachedDelegate = StaticMath.Add;
[Benchmark]
public int StaticCall() => StaticMath.Add(1, 2);
[Benchmark]
public int DynamicCall() => DynamicMath.Add(1, 2);
[Benchmark]
public int CachedDelegateCall() => CachedDelegate(1, 2);
[Benchmark]
public int ReflectionCall() =>
(int)typeof(MathOperations).GetMethod("Add").Invoke(StaticMath, new object[] {1, 2});
}
public class MathOperations
{
public int Add(int a, int b) => a + b;
}
典型结果(纳秒/操作):
| 方法 | 均值 | 分配 |
|---|---|---|
| StaticCall | 0.3 | 0 B |
| CachedDelegateCall | 0.5 | 0 B |
| DynamicCall | 45.2 | 120 B |
| ReflectionCall | 320.6 | 240 B |
17. 动态类型与依赖注入
17.1 动态服务解析
csharp复制public class DynamicServiceProvider
{
private readonly IServiceProvider _provider;
public DynamicServiceProvider(IServiceProvider provider) {
_provider = provider;
}
public dynamic GetService(string name) {
var type = Type.GetType(name) ??
Assembly.GetExecutingAssembly().GetType(name);
return _provider.GetService(type);
}
}
// 注册
services.AddSingleton<DynamicServiceProvider>();
// 使用
dynamic service = dynamicProvider.GetService("MyNamespace.MyService");
service.Process();
17.2 动态代理拦截
结合 Castle DynamicProxy:
csharp复制public static dynamic CreateProxy<T>(T target, Action<string> interceptor)
{
var generator = new ProxyGenerator();
return generator.CreateInterfaceProxyWithTarget(
typeof(T),
target,
new DynamicInterceptor(interceptor));
}
private class DynamicInterceptor : IInterceptor
{
private readonly Action<string> _log;
public DynamicInterceptor(Action<string> log) {
_log = log;
}
public void Intercept(IInvocation invocation) {
_log?.Invoke($"调用 {invocation.Method.Name}");
invocation.Proceed();
}
}
// 使用
dynamic proxy = CreateProxy<IMyService>(new MyService(), Console.WriteLine);
proxy.DoWork(); // 会自动记录调用
18. 动态编译与热重载
实现简单的热重载功能:
csharp复制public class HotReloadableScript
{
private dynamic _instance;
private DateTime _lastWriteTime;
private readonly string _filePath;
public HotReloadableScript(string filePath) {
_filePath = filePath;
Reload();
}
public void Reload() {
var code = File.ReadAllText(_filePath);
_lastWriteTime = File.GetLastWriteTime(_filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.CreateScriptCompilation(
"DynamicScript",
syntaxTree,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if(!emitResult.Success) {
throw new InvalidOperationException("编译错误: " +
string.Join("\n", emitResult.Diagnostics));
}
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
_instance = assembly.CreateInstance("Submission#0");
}
public dynamic Execute() {
if(File.GetLastWriteTime(_filePath) > _lastWriteTime) {
Reload();
}
return _instance.Execute();
}
}
// 使用示例
var script = new HotReloadableScript("script.cs");
while(true) {
Console.WriteLine(script.Execute());
Thread.Sleep(1000);
}
19. 动态类型与并行编程
19.1 PLINQ 与 dynamic
csharp复制dynamic[] data = GetDynamicData();
var results = data.AsParallel()
.Where(item => item.IsActive)
.Select(item => {
dynamic result = item.Process();
return new {
Id = item.Id,
Value = result.Value
};
})
.ToArray();
注意事项:
- 确保动态操作是线程安全的
- 避免在并行循环中修改共享动态对象
- 考虑使用 ThreadLocal
存储线程特定状态
19.2 动态任务工厂
csharp复制public static Task<dynamic> RunDynamic(Func<dynamic> function)
{
return Task.Run(() => {
try {
return function();
} catch(RuntimeBinderException ex) {
throw new InvalidOperationException("动态调用失败", ex);
}
});
}
// 使用示例
dynamic result = await RunDynamic(() => {
dynamic calculator = GetCalculator();
return calculator.ComplexCalculation(1, 2, 3);
});
20. 未来发展与替代方案
20.1 源代码生成器替代方案
C# 9.0 引入的源代码生成器可以替代部分动态场景:
csharp复制[Generator]
public class DynamicProxyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context) {
// 分析上下文中的类型
// 生成静态代理类
string source = @"...";
context.AddSource("DynamicProxy.g.cs", source);
}
}
优势:
- 编译时完成
- 完全静态类型
- 更好的性能
20.2 动态类型的演进方向
-
更智能的缓存:
- 基于使用模式的预测性缓存
- 自适应缓存策略
-
更好的 AOT 支持:
- 预生成动态调用桩
- 改进的反射 emit 替代方案
-
与模式匹配的深度集成:
csharp复制if(dynamicObj is { Property: int value }) { // 使用 value } -
性能分析工具增强:
- 专门的 dynamic 性能分析器
- 调用跟踪可视化
