dynamic 类型与 DLR 深入
大约 11 分钟约 3370 字
dynamic 类型与 DLR 深入
简介
C# 的 dynamic 关键字启用了动态类型绑定,底层由动态语言运行时(DLR,Dynamic Language Runtime)支撑。DLR 为 C# 提供了与动态语言(Python、JavaScript 等)互操作的能力,以及灵活的运行时方法分发机制。
特点
dynamic 基础
动态类型行为
// dynamic 在编译时跳过类型检查,运行时解析
dynamic d = 42;
d = "hello"; // 可以重新赋值为不同类型
d = new List<int> { 1, 2, 3 };
d.Add(4); // 运行时查找 Add 方法
Console.WriteLine(d.Count); // 4
// dynamic vs object
object obj = 42;
// obj++; // 编译错误
// ((int)obj)++; // 需要显式转换
dynamic dyn = 42;
dyn++; // 运行时解析,OK
Console.WriteLine(dyn); // 43
// dynamic 参数
dynamic Add(dynamic a, dynamic b) => a + b; // 运行时决定加法行为
Console.WriteLine(Add(1, 2)); // 3 (int)
Console.WriteLine(Add(1.5, 2.5)); // 4.0 (double)
Console.WriteLine(Add("a", "b")); // "ab" (string)
// dynamic 与反射对比
// 反射方式(繁琐)
object obj2 = "hello";
var method = obj2.GetType().GetMethod("ToUpper");
string? result1 = method?.Invoke(obj2, null) as string;
// dynamic 方式(简洁)
dynamic dyn2 = "hello";
string result2 = dyn2.ToUpper(); // 运行时调用
// 类型转换
dynamic d2 = "123";
int num = (int)d2; // RuntimeBinderException! string 不能转 int
int parsed = int.Parse(d2); // OKdynamic 与 var 的区别
// var — 编译时推断,强类型
var s = "hello"; // s 是 string
// s = 42; // 编译错误
// dynamic — 运行时解析
dynamic d = "hello"; // d 是 dynamic
d = 42; // OK
// 隐式转换
var list1 = new List<int> { 1, 2, 3 };
// list1.Add("string"); // 编译错误
dynamic list2 = new List<int> { 1, 2, 3 };
// list2.Add("string"); // 运行时错误(但编译通过!)
// 方法重载解析
void Print(int value) => Console.WriteLine($"int: {value}");
void Print(string value) => Console.WriteLine($"string: {value}");
var v = 42;
Print(v); // 编译时选择 Print(int)
dynamic d3 = 42;
Print(d3); // 运行时选择 Print(int)DLR 机制
CallSite 与缓存
// DLR 核心:CallSite(调用点)+ 缓存
// 编译器将 dynamic 调用转换为 CallSite
// C# 代码:dynamic d = ...; d.Foo(42);
// 编译器生成(简化):
var site = CallSite<Func<CallSite, object, int, object>>.Create(
Binder.InvokeMember(
CSharpBinderFlags.None,
"Foo",
null,
typeof(Program),
new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }
)
);
object result = site.Target(site, d, 42);
// DLR 三级缓存(L1 → L2 → L3):
// L1: CallSite 自己的 Target 委托(最近一次成功绑定的规则)
// L2: CallSite 的 Rules 列表(近期成功的规则)
// L3: 全局缓存(所有 CallSite 共享)
// 缓存规则示例:
// 如果 receiver 是 string → 调用 string.Foo(int)
// 如果 receiver 是 StringBuilder → 调用 StringBuilder.Foo(int)
// 命中缓存时性能接近直接调用(纳秒级)
// 缓存未命中时需要重新绑定(微秒级)
// 性能对比
var sw = Stopwatch.StartNew();
string str = "hello";
for (int i = 0; i < 1_000_000; i++)
{
int len = str.Length; // 直接调用
}
sw.Stop();
Console.WriteLine($"直接调用: {sw.ElapsedMilliseconds}ms");
sw.Restart();
dynamic dstr = "hello";
for (int i = 0; i < 1_000_000; i++)
{
int len = dstr.Length; // DLR 调用(缓存命中后)
}
sw.Stop();
Console.WriteLine($"DLR 调用: {sw.ElapsedMilliseconds}ms");
// DLR 调用通常比直接调用慢 2-5 倍(缓存命中后)ExpandoObject 与 DynamicObject
ExpandoObject 动态属性
// ExpandoObject — 动态添加属性和方法
dynamic person = new ExpandoObject();
person.Name = "张三";
person.Age = 30;
person.SayHello = (Func<string>)(() => $"Hello, I'm {person.Name}");
Console.WriteLine(person.Name); // 张三
Console.WriteLine(person.SayHello()); // Hello, I'm 张三
// 添加事件
person.PropertyChanged = null; // EventHandler
person.Name = "李四";
person.PropertyChanged?.Invoke(person, EventArgs.Empty);
// 转为字典访问
var dict = (IDictionary<string, object>)person;
foreach (var kv in dict)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
// 动态构建配置对象
dynamic config = new ExpandoObject();
config.Database = new ExpandoObject();
config.Database.ConnectionString = "Server=...";
config.Database.Timeout = 30;
config.Logging = new ExpandoObject();
config.Logging.Level = "Information";DynamicObject 自定义行为
// DynamicObject — 完全自定义动态行为
public class DynamicDictionary : DynamicObject
{
private readonly Dictionary<string, object> _dict = new();
// 属性获取
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
return _dict.TryGetValue(binder.Name, out result);
}
// 属性设置
public override bool TrySetMember(SetMemberBinder binder, object? value)
{
_dict[binder.Name] = value!;
return true;
}
// 方法调用
public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
{
if (_dict.TryGetValue(binder.Name, out var method) && method is Delegate del)
{
result = del.DynamicInvoke(args);
return true;
}
result = null;
return false;
}
// 索引器
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
{
if (indexes[0] is string key)
return _dict.TryGetValue(key, out result);
result = null;
return false;
}
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object? value)
{
if (indexes[0] is string key)
{
_dict[key] = value!;
return true;
}
return false;
}
// 运算符
public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object? result)
{
if (binder.Operation == ExpressionType.Add && arg is DynamicDictionary other)
{
var merged = new DynamicDictionary();
foreach (var kv in _dict) merged._dict[kv.Key] = kv.Value;
foreach (var kv in other._dict) merged._dict[kv.Key] = kv.Value;
result = merged;
return true;
}
result = null;
return false;
}
public override IEnumerable<string> GetDynamicMemberNames() => _dict.Keys;
}
// 使用
dynamic dict = new DynamicDictionary();
dict.Name = "张三"; // TrySetMember
dict.Age = 30; // TrySetMember
Console.WriteLine(dict.Name); // TryGetMember → "张三"
Console.WriteLine(dict["Age"]); // TryGetIndex → 30IDynamicMetaObjectProvider
自定义元对象
// IDynamicMetaObjectProvider — DLR 的核心接口
// DynamicObject 是它的便利基类
public class FluentBuilder : IDynamicMetaObjectProvider
{
private readonly List<string> _calls = new();
public DynamicMetaObject GetMetaObject(Expression parameter)
{
return new FluentMetaObject(parameter, BindingRestrictions.Empty, this);
}
public void AddCall(string method) => _calls.Add(method);
public string GetCalls() => string.Join(" → ", _calls);
private class FluentMetaObject : DynamicMetaObject
{
public FluentMetaObject(Expression expr, BindingRestrictions restrictions, object value)
: base(expr, restrictions, value) { }
public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
// 将方法调用记录到 builder 中
var builder = (FluentBuilder)Value;
builder.AddCall(binder.Name);
// 返回自身以支持链式调用
return new DynamicMetaObject(
Expression.Constant(builder),
BindingRestrictions.GetTypeRestriction(Expression, typeof(FluentBuilder))
);
}
}
}
// 使用:链式动态调用
dynamic builder = new FluentBuilder();
builder.Connect().Authenticate().SendData().Disconnect();
Console.WriteLine(builder.GetCalls());
// 输出: Connect → Authenticate → SendData → Disconnect互操作实战
与 Python 互操作
// 使用 IronPython
// dotnet add package IronPython
var engine = Python.CreateEngine();
var scope = engine.CreateScope();
// 执行 Python 代码
engine.Execute(@"
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
", scope);
// 通过 dynamic 调用 Python 函数
dynamic fibonacci = scope.GetVariable("fibonacci");
Console.WriteLine(fibonacci(10)); // 55
// 传递 C# 对象到 Python
scope.SetVariable("data", new[] { 1, 2, 3, 4, 5 });
engine.Execute("result = sum(data) * 2", scope);
Console.WriteLine(scope.GetVariable("result")); // 30优点
缺点
总结
dynamic 关键字由 DLR 支撑,将类型检查推迟到运行时。DLR 通过 CallSite 的三级缓存(L1/L2/L3)优化重复调用的性能。ExpandoObject 提供动态属性添加,DynamicObject 允许完全自定义动态行为。IDynamicMetaObjectProvider 是 DLR 的核心接口,用于构建表达式树级别的动态绑定。适用场景:COM 互操作、JSON 解析、动态配置、Python 互操作。避免在性能敏感的热路径中使用 dynamic。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《dynamic 类型与 DLR 深入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《dynamic 类型与 DLR 深入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《dynamic 类型与 DLR 深入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《dynamic 类型与 DLR 深入》最大的收益和代价分别是什么?
dynamic 与 JSON 处理
// dynamic 在 JSON 处理中的应用
// 1. System.Text.Json.Nodes — AOT 安全的动态 JSON
using System.Text.Json.Nodes;
var json = """
{
"name": "张三",
"age": 30,
"address": {
"city": "北京",
"zip": "100000"
},
"hobbies": ["编程", "阅读"]
}
""";
var node = JsonNode.Parse(json)!;
// 动态访问 JSON 属性
Console.WriteLine(node["name"]); // 张三
Console.WriteLine(node["address"]["city"]); // 北京
Console.WriteLine(node["hobbies"][0]); // 编程
// 动态修改
node["age"] = 31;
node["email"] = "zhangsan@example.com";
node["address"]["city"] = "上海";
// 遍历
foreach (var property in node.AsObject())
{
Console.WriteLine($"{property.Key}: {property.Value}");
}
// 2. JsonNode vs dynamic — 选择建议
// JsonNode: AOT 安全,类型安全(有 JsonValue/JsonArray/JsonObject)
// dynamic + Newtonsoft.Json: 灵活但 AOT 不兼容
// 推荐在新项目中使用 JsonNode// 3. 强类型 + 动态混合的 JSON 处理
public class UserProfile
{
public string Name { get; set; } = "";
public int Age { get; set; }
// 动态扩展字段
public Dictionary<string, JsonElement> ExtraFields { get; set; } = new();
}
// 反序列化时保留未知字段
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Populate
};
var user = JsonSerializer.Deserialize<UserProfile>(json, options);
// user.ExtraFields 包含所有未映射到属性的 JSON 字段dynamic 与 COM 互操作
// dynamic 在 COM 互操作中的经典应用
// 1. Office 自动化
using Microsoft.Office.Interop.Excel;
// 传统方式(需要 PIAs,繁琐)
// var app = new Application();
// var workbooks = app.Workbooks;
// dynamic 方式(简洁,无需 PIAs)
dynamic excel = Type.GetTypeFromProgID("Excel.Application");
dynamic app = Activator.CreateInstance(excel);
app.Visible = true;
dynamic workbook = app.Workbooks.Add();
dynamic worksheet = workbook.Worksheets[1];
worksheet.Name = "数据表";
// 写入数据
for (int row = 1; row <= 10; row++)
{
worksheet.Cells[row, 1].Value = $"Item {row}";
worksheet.Cells[row, 2].Value = row * 10;
}
// 格式化
worksheet.Columns["A:B"].AutoFit();
// 保存
workbook.SaveAs("C:\\temp\\data.xlsx");
app.Quit();
// 释放 COM 对象
if (app != null)
System.Runtime.InteropServices.Marshal.ReleaseComObject(app);dynamic 性能优化模式
// dynamic 性能优化的几种模式
// 1. 缓存 CallSite — 多次调用同一方法
// 编译器自动生成 CallSite 并缓存,通常不需要手动优化
// 但如果你手动使用 dynamic,可以缓存 Binder
// 2. 使用 CSharpArgumentInfo 缓存
using Microsoft.CSharp.RuntimeBinder;
// 手动创建缓用的 Binder(高级场景)
var binder = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
CSharpBinderFlags.None,
"MethodName",
null,
typeof(Program),
new[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null)
}
);
// 3. 接口替代 dynamic — 如果类型已知
// 动态调度 -> 接口调度(性能提升 10-50 倍)
public interface IDataProvider
{
string GetData(string key);
}
// 不要这样:
// dynamic provider = GetProvider();
// var data = provider.GetData("key");
// 如果类型已知,用接口:
IDataProvider provider = GetProvider();
var data = provider.GetData("key");
// 4. 表达式树替代动态调用(可编译为委托)
using System.Linq.Expressions;
public static Func<object, object> CreateFastGetter(Type type, string propertyName)
{
var param = Expression.Parameter(typeof(object), "obj");
var cast = Expression.Convert(param, type);
var property = Expression.Property(cast, propertyName);
var convert = Expression.Convert(property, typeof(object));
return Expression.Lambda<Func<object, object>>(convert, param).Compile();
}
// 使用:编译一次,调用多次(接近直接调用的性能)
var getter = CreateFastGetter(typeof(User), "Name");
var name = getter(user); // 快速调用dynamic 的安全使用规范
// 团队规范:何时允许使用 dynamic
// 允许使用的场景:
// 1. COM 互操作(Office、IE 等)
// 2. JSON 处理(结构不固定的数据)
// 3. 与动态语言互操作(IronPython、Lua 等)
// 4. 测试 Mock 框架内部实现
// 5. 插件系统的消息传递
// 禁止使用的场景:
// 1. 业务逻辑核心路径
// 2. 高频调用的热路径(性能敏感)
// 3. 类型已知但偷懒不写接口
// 4. 公共 API 返回 dynamic(调用方无法智能感知)
// 动态调用的异常处理
try
{
dynamic obj = GetUnknownObject();
var result = obj.SomeMethod("param");
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
{
// 运行时绑定失败 — 成员不存在或签名不匹配
Console.WriteLine($"动态调用失败: {ex.Message}");
}
catch (RuntimeBinderInternalCompilerException ex)
{
// DLR 内部错误(通常不应发生)
Console.WriteLine($"DLR 内部错误: {ex.Message}");
}dynamic 在序列化中的应用
// dynamic 在数据转换中的应用
// 1. 动态构建请求体
dynamic request = new System.Dynamic.ExpandoObject();
request.Name = "张三";
request.Email = "zhangsan@example.com";
// 动态添加字段
var dict = (IDictionary<string, object>)request;
dict["CustomField"] = "自定义值";
dict["Tags"] = new[] { "VIP", "活跃" };
// 序列化为 JSON
var json = JsonSerializer.Serialize(request);
// {"Name":"张三","Email":"zhangsan@example.com","CustomField":"自定义值","Tags":["VIP","活跃"]}
// 2. 数据映射器 — 动态字段映射
public class DataMapper
{
private readonly Dictionary<string, Func<object, object>> _mappings = new();
public DataMapper Map(string targetProperty, Func<object, object> mapper)
{
_mappings[targetProperty] = mapper;
return this;
}
public dynamic MapTo(IDictionary<string, object> source)
{
dynamic result = new ExpandoObject();
var resultDict = (IDictionary<string, object>)result;
foreach (var mapping in _mappings)
{
if (source.TryGetValue(mapping.Key, out var value))
{
resultDict[mapping.Key] = mapping.Value(value);
}
}
return result;
}
}
// 使用
var mapper = new DataMapper();
mapper.Map("Name", v => v.ToString().ToUpper());
mapper.Map("CreatedAt", v => DateTime.Parse(v.ToString()).ToString("yyyy-MM-dd"));
var source = new Dictionary<string, object>
{
{ "Name", "张三" },
{ "CreatedAt", "2024-01-15T10:30:00" }
};
var result = mapper.MapTo(source);
// result.Name = "张三", result.CreatedAt = "2024-01-15"