聊到 C# 的 dynamic,很多人第一反应是“运行时类型”,但这种理解其实不太对。它并不是运行时态,而更像是一张编译器给开的“后门通行证”。简单说,你把变量声明为 dynamic,编译器就不再检查你有没有写对成员名、参数个数对不对、能不能赋值——这些事全推到运行时,交给 DLR(动态语言运行时)去处理。要是用错了地方,直接甩你一个 RuntimeBinderException,连个调试的余地都不给。
什么场景非用 dynamic 不可?
关键不在于“能不能装任意值”,而在于“调用成员的时候要不要走 DLR 绑定”。你可以试试看,object 上调用方法直接编译不过去(除非你用反射),而 dynamic 会把整个调用链——方法名、参数、重载选择——打包成一个 CallSite,等到运行时才去查目标对象到底有没有那个方法。哪些场景非它不可?
- 和 COM 对象打交道,比如 Excel 互操作,不想写一大串
Marshal.Invoke或者Type.InvokeMember - 接收来自 IronPython、Ruby.NET 这类动态语言的返回值,签名在 C# 编译期根本不知道
- 解析 HTML DOM(比如
mshtml或Microsoft.Office.Interop.Excel),属性名和方法名是字符串驱动的 - 做高度泛化的 JSON 反序列化桥接,比如
Newtonsoft.Json.Linq.JObject转dynamic后直接点属性
dynamic 赋值后类型变了吗?
这个要分清楚:变量本身类型始终是 dynamic,但所指向的底层值可以是任意类型。IntelliSense 显示的是声明类型,不是值类型。看个例子就明白了:
dynamic d = 42;
Console.WriteLine(d.GetType()); // System.Int32
d = "hello";
Console.WriteLine(d.GetType()); // System.String
d = new[] { 1, 2, 3 };
Console.WriteLine(d.GetType()); // System.Int32[]
这里 var 真代替不了。var d = 42; 推导出来是 int,后面不能再赋值字符串。但 dynamic 可以跨类型赋值,而且每次 .GetType() 返回的都是真实运行时类型。
为什么 dynamic_ec.exampleMethod1(10, 4) 是运行时抛异常而不是编译报错?
编译器对 dynamic 表达式的任何成员访问——方法、属性、索引器——都生成 CallSite,把调用信息缓存起来,等到第一次执行才尝试绑定。DLR 的查找顺序是这样的:
- 看看目标对象有没有公开实例方法
exampleMethod1,并且参数个数、类型能隐式转换匹配 - 查有没有扩展方法能用(需 using 命名空间已引入)
- 查是否实现了
IDynamicMetaObjectProvider,走自定义绑定逻辑 - 全部失败:抛
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
所以即使 ExampleClass 只定义了 exampleMethod1(int i),dynamic_ec.exampleMethod1(10, 4) 还是会走到第 4 步失败,错误信息类似 'ExampleClass' does not contain a definition for 'exampleMethod1'。
那些容易被忽略的性能与调试陷阱
dynamic 绑定的开销比想象中大。首次调用慢——要构建和缓存 CallSite;后续同签名调用快——命中缓存;但一旦参数类型组合发生变化(比如从 int 换成 long),又触发新绑定。更麻烦的是调试体验:
- 断点停在
dynamic行时,Locals 窗口只显示dynamic,看不到实际值结构(得鼠标悬停或用QuickWatch输入d.ToString()) - 异常堆栈里看不到原始调用点,而是深埋在
Microsoft.CSharp.RuntimeBinder内部 - ReSharper 或 Roslyn 分析器做不了空引用或成员缺失预警
dynamic传入泛型方法时,类型推导会失效:DoSomething里(dynamicValue) T会被推为dynamic,而不是实际运行时类型
真正难缠的不是语法本身,而是你得想清楚:这个 dynamic 值从哪来、生命周期内可能是什么类型、谁负责保证它有你要调的方法。这些责任全移交给了程序员,编译器不再兜底。