一、动态编程的核心:dynamic 类型的引入
C# 4.0 引入 dynamic 类型,其核心是运行时绑定(动态绑定),而非编译时绑定(静态绑定)。
- 静态绑定:编译时验证类型、成员是否存在,若不存在直接报错。
- 动态绑定:编译时不验证,运行时才解析对象的类型、成员并执行调用。
二、为什么需要动态编程?
有些场景下,对象的结构在编译时无法确定,典型场景包括:
- 从 XML/CSV、数据库、IE DOM 等动态数据源加载数据;
- 调用 COM 组件(如
IDispatch接口); - 调用动态语言(如 IronPython)定义的类型;
- 简化反射代码的编写。
三、C# 动态对象的 4 种绑定方式
C# 4.0 中,dynamic 支持 4 种运行时绑定逻辑:
- 针对 CLR 类型的反射:通过反射机制动态查找 / 调用类型成员;
- 自定义
IDynamicMetaObjectProvider:自己实现动态对象的绑定逻辑; - COM 的
IUnknown/IDispatch接口:与 COM 组件交互; - 动态语言类型:调用 IronPython 等动态语言定义的类型。
四、方式 1:用 dynamic 简化反射调用
反射的核心是 "运行时查找 / 调用类型成员",但原生反射代码繁琐;dynamic 可以用更简洁的语法实现反射功能。
4.1 原生反射 vs dynamic 反射
原生反射(繁琐):
cs
// 假设要调用 string 的 Length 属性
object str = "Hello";
Type type = str.GetType();
PropertyInfo prop = type.GetProperty("Length");
int length = (int)prop.GetValue(str); // 运行时获取值
dynamic 反射(简洁):
cs
dynamic str = "Hello";
int length = str.Length; // 编译时不验证,运行时通过反射调用 Length
4.2 dynamic 反射的代码示例
cs
using System;
dynamic data = "Hello! My name is Inigo Montoya";
Console.WriteLine(data);
data = (double)data.Length; // 字符串转 double(运行时执行装箱/拆箱)
data = data + 28.6;
if(data == 2.4 + 112 + 26.2)
{
Console.WriteLine($"{data} makes for a long triathlon.");
}
else
{
data.NonExistentMethodCallStillCompiles(); // 编译不报错,运行时抛异常
}
输出:
cs
Hello! My name is Inigo Montoya
40.6 makes for a long triathlon.
4.3 dynamic 反射的注意事项
- 编译时不验证,运行时抛异常 :若调用的成员不存在(如
data.NonExistentMethodCallStillCompiles()),运行时会抛出Microsoft.CSharp.RuntimeBinder.RuntimeBinderException。 - 不支持扩展方法 :扩展方法是 "编译时绑定" 的语法糖,
dynamic是运行时绑定,因此无法直接调用扩展方法(需通过静态类显式调用)。 - 返回值默认是
dynamic:调用dynamic对象的成员,返回值默认是dynamic类型(需显式转换为具体类型)。
五、dynamic 的核心原则和行为
dynamic 不是 "新类型",而是告诉编译器 "运行时再处理绑定" 的指令 ,其底层是 System.Object。
5.1 dynamic 与 object 的关系
dynamic本质是System.Object:IL 代码中,dynamic会被编译为object;- 隐式转换:任何类型都能隐式转换为
dynamic(值类型会自动装箱); - 显式转换:
dynamic转具体类型需要显式转换(值类型会自动拆箱)。
5.2 dynamic 的 "动态性" 体现
- 基础类型可动态变更 :
dynamic变量的底层类型可以在运行时改变(如示例中data从string变为double); - 运行时才验证成员:编译时不检查成员是否存在,运行时通过 "解释机制" 解析调用(底层依赖反射)。
六、方式 2:自定义动态对象(实现 IDynamicMetaObjectProvider)
若内置的动态绑定逻辑不满足需求(如自定义 XML/JSON 动态解析),可以自己实现动态对象。
C# 提供了 System.Dynamic.DynamicObject 抽象类(实现了 IDynamicMetaObjectProvider),只需重写对应的虚方法,即可自定义动态绑定逻辑。
6.1 自定义动态对象的核心:重写 DynamicObject 的方法
DynamicObject 提供了一系列虚方法,用于自定义动态行为,常用的有:
TryGetMember:自定义 "获取动态成员" 的逻辑;TrySetMember:自定义 "设置动态成员" 的逻辑;TryInvokeMember:自定义 "调用动态方法" 的逻辑;TryConvert:自定义 "类型转换" 的逻辑;TryGetIndex/TrySetIndex:自定义 "索引器" 的逻辑。
6.2 示例:实现 DynamicXml(动态解析 XML)
需求:通过 dynamic 语法直接访问 XML 元素(如 person.FirstName 对应 XML 中的 <FirstName> 节点)。
步骤 1:定义 DynamicXml 类(继承 DynamicObject)
cs
using System;
using System.Dynamic;
using System.Xml.Linq;
public class DynamicXml : DynamicObject
{
// 包装的 XML 元素
private XElement Element { get; set; }
// 构造函数
public DynamicXml(XElement element)
{
Element = element;
}
// 工厂方法:解析 XML 字符串
public static DynamicXml Parse(string text)
{
return new DynamicXml(XElement.Parse(text));
}
// 重写:自定义"获取成员"的逻辑
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
bool success = false;
result = null;
// 查找与"成员名"匹配的 XML 子元素
XElement firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
// 若子元素有子节点,递归包装为 DynamicXml;否则返回文本值
if (firstDescendant.Descendants().Any())
{
result = new DynamicXml(firstDescendant);
}
else
{
result = firstDescendant.Value;
}
success = true;
}
return success;
}
// 重写:自定义"设置成员"的逻辑
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
bool success = false;
XElement firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
// 若值是 DynamicXml,取其包装的 XElement;否则直接设为文本
if (value.GetType() == typeof(DynamicXml))
{
firstDescendant.ReplaceWith(((DynamicXml)value).Element);
}
else
{
firstDescendant.Value = value.ToString();
}
success = true;
}
return success;
}
}
步骤 2:使用 DynamicXml
cs
using System;
// 解析 XML 字符串
dynamic person = DynamicXml.Parse(@"
<Person>
<FirstName>Inigo</FirstName>
<LastName>Montoya</LastName>
</Person>");
// 直接通过"动态成员"访问 XML 元素
Console.WriteLine($"{person.FirstName} {person.LastName}");
输出:
cs
Inigo Montoya
6.3 自定义动态对象的逻辑说明
当调用 person.FirstName 时:
- 编译器识别到
person是dynamic,不做编译时验证; - 运行时触发
DynamicXml.TryGetMember方法; TryGetMember中,通过binder.Name获取成员名(FirstName);- 查找 XML 中对应的
<FirstName>节点,返回其值; - 若找不到节点,
TryGetMember返回false,运行时抛出RuntimeBinderException。
七、静态编程 vs 动态编程:对比与适用场景
|-------|--------------|-----------------------|
| 维度 | 静态编程(编译时绑定) | 动态编程(运行时绑定) |
| 编译时验证 | 验证类型、成员是否存在 | 不验证,运行时才检查 |
| 代码可读性 | 依赖强类型,可读性高 | 语法简洁(如动态 XML 访问) |
| 性能 | 编译时绑定,性能高 | 运行时反射 / 解释,性能较低 |
| 类型安全性 | 编译时保证,错误更早暴露 | 运行时才发现错误 |
| 适用场景 | 类型结构固定的场景 | 类型结构动态(XML/COM/ 动态语言) |
八、动态编程的扩展
8.1 动态编程的底层:CallSite 与表达式树
dynamic 调用的底层依赖 System.Runtime.CompilerServices.CallSite<T>:
- 编译时,编译器生成
CallSite对象,用于封装动态调用的上下文; - 运行时,
CallSite会生成表达式树,将动态调用编译为 CIL 代码并缓存; - 后续相同的动态调用会复用缓存的 CIL 代码,减少反射开销。
8.2 动态编程的局限性
- 性能:运行时绑定的性能比静态绑定低(反射 / 表达式树编译有开销);
- 工具支持:IDE 无法提供 "智能感知"(编译时不知道成员);
- 调试难度:错误在运行时暴露,调试需跟踪运行时状态。
九、小结
dynamic是 C# 4.0 引入的 "运行时绑定" 语法糖,底层依赖反射 / 表达式树;- 动态编程适用于类型结构动态的场景(XML/COM/ 动态语言交互);
- 可通过
DynamicObject自定义动态对象,重写TryGetMember/TrySetMember等方法实现自定义绑定逻辑; - 动态编程的优势是语法简洁 ,劣势是性能低、类型不安全,需根据场景选择。