在 C# 4.0 引入 dynamic 关键字之前,变量的类型在编译阶段就已经确定。dynamic 的出现让 C# 具备了一定的动态编程能力,它告诉编译器暂时不要对这个变量进行类型检查,而是把绑定操作推迟到运行时。这意味着你可以在编写代码时调用一个编译器并不知道是否存在的方法或属性,只要运行时对象能够响应这个调用,程序就不会报错。静态类型语言强调的是先定义契约再执行,而动态类型则把契约检查放到最后一刻。

dynamic 背后的运行机制:DLR 与运行时绑定
dynamic 并不是一个实际的 CLR 类型,它只是告诉 C# 编译器把这个变量的所有操作标记为动态绑定。编译完成后,变量的真实类型仍然是 object,但调用信息会进入 DLR(Dynamic Language Runtime)的绑定流程。DLR 是 .NET 4.0 引入的一组运行时服务,它在程序执行到动态调用点时,根据操作数和成员名称去查找实际类型,再通过缓存机制加速后续相同签名的调用。
举例来说,如果声明一个 dynamic value = 10;,编译器不会在编译时检查 value 是否有 GetType 方法,而是生成一段运行时绑定代码。执行到 value.GetType() 时,DLR 发现 value 实际是 int,于是通过反射或预编译表达式树找到 System.Int32 的 GetType 方法并调用。如果 value 后来被赋值为字符串,下一次绑定时 DLR 会重新解析成 string 的方法。这种机制让 C# 能调用编译期完全未知的 API,但也会带来额外的解析步骤。
DLR 还引入了调用点缓存和规则缓存:同一位置的动态调用如果参数类型相同,后续调用几乎可以接近普通反射的性能。不过与编译时直接生成 IL 的静态调用相比,动态绑定仍需要经历运行时查找和规则匹配,这是它无法完全消除的开销来源。
静态类型与动态类型的工作差异
C# 默认的静态类型体系在编译阶段完成类型检查、方法重载决议和成员访问验证。比如 string name = "Alice"; Console.WriteLine(name.Length); 这行代码,编译器明确知道 name 是 string,因此直接生成调用 String.get_Length 的 IL 指令,运行时不需再判断类型。这种提前检查能发现多数拼写错误和类型不匹配,也带来了完善的 IDE 智能提示。
动态类型则把上述过程整体后移。编译器看到 dynamic 变量后,会停止对它的静态分析,所有成员访问、运算符、索引器甚至类型转换都变成动态操作。这样做的好处是代码可以面向运行时才确定的结构编写,例如与 IronPython 交互、操作 COM 对象或反序列化成 ExpandoObject。坏处同样明显:拼错方法名、传错参数类型、属性不存在等问题都会从编译错误变成运行时异常,调试成本显著增加。
从语言设计角度看,C# 选择在静态语言中引入动态能力,是为了避免为每一类动态互操作场景单独设计复杂的类型系统。static 和 dynamic 并不是非此即彼,而是可以在同一个项目中按需混用。关键是如何控制动态区域的边界,尽量让动态调用集中在一小段代码中,避免污染整个类型系统。
dynamic、object、var 的用法边界
这三个概念容易被混淆,但它们的职责完全不同。var 是编译时类型推断,编译器根据右侧初始化表达式推断出具体类型,变量仍然是静态类型的。比如 var name = "Alice"; 编译后 name 就是 string,后续对 name 的成员访问依然有静态检查和智能提示。var 只是在声明时少写几个字,不代表任何动态性。
object 是 .NET 类型系统的根类型,所有类型都可以隐式转换成 object。但 object 变量本身是静态类型 object,编译器只允许访问 object 上定义的少数成员,如 ToString、GetHashCode 等。如果想调用实际类型的成员,必须显式进行类型转换。dynamic 则在编译期完全跳过成员访问检查,允许直接调用任何成员。可以把 dynamic 理解为一种带有运行时绑定行为的 object 包装。
object obj = "hello"; // 编译错误:object 没有 Length 属性 // Console.WriteLine(obj.Length); dynamic dyn = "hello"; // 运行时可以调用 string.Length Console.WriteLine(dyn.Length);
上面的代码展示了 object 和 dynamic 的核心差异。需要注意的是,dynamic 变量在运行时确实以 object 形式存储,但编译器会为它的成员访问生成 DLR 调用点,因此可以绕过编译期检查。如果你只需要存储任意类型的值而不会动态调用其成员,object 是更安全、更高效的选择。
适合使用 dynamic 的典型场景
dynamic 最自然的应用场景是与 COM 组件互操作。例如操作 Excel 或 Word 时,COM 对象模型大量依赖运行时反射和 IDispatch 接口,使用 dynamic 可以省去大量显式接口转换代码。以下示例演示通过 ProgID 创建 Excel 应用并操作工作簿,编译器完全不清楚 excel 变量的具体类型,但运行时代码仍然可以工作。
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true;
excel.Workbooks.Add();
另一个常见场景是处理弱类型 JSON 或外部 API 返回的动态结构。在不需要预先定义 DTO 类的情况下,可以将 JSON 反序列化为 ExpandoObject 或 JObject,再用 dynamic 直接读取属性,适合快速验证和脚本式开发。比如读取一个动态对象中的 Name 和 Age,可以写成 dynamic person = GetUserData(); Console.WriteLine(person.Name);,编译器不会抱怨 person 上是否有 Name 属性。
当 C# 需要与动态语言运行时交互,例如在应用程序中嵌入 IronPython 脚本时,dynamic 也能减少类型转换代码。此外,某些框架内部也会使用 dynamic 来实现更友好的 API,如 ViewBag 在 ASP.NET MVC 中就是 dynamic 类型的属性。不过这些便利都建立在运行时绑定和异常延迟的基础上,需要权衡。
动态调用带来的性能与维护成本
性能方面,dynamic 调用的第一步是运行时绑定,这个阶段可能涉及反射、表达式树编译和缓存。对于循环中的动态调用,即使 DLR 有缓存,也比静态调用慢一到两个数量级。尤其在频繁执行的热路径中,把 dynamic 放在循环体内可能导致明显的性能下降。可以通过提前将动态对象转换为已知类型来削减开销。
维护成本是更隐蔽的问题。动态代码绕过了编译器检查后,原本由编译器发现的错误会推迟到运行时。一个拼写错误的方法名可能在代码上线后才因为特定数据路径而触发异常,而单元测试如果覆盖不足,就会造成线上故障。此外,阅读代码时,开发者无法通过类型声明快速了解对象的契约,必须依靠文档或运行时调试才能知道对象支持哪些成员。
因此,更稳妥的做法是把 dynamic 看作互操作胶水,而不是常规业务逻辑的主要载体。在核心领域模型中尽量使用接口和具体类型,只在边界处使用 dynamic 接收或传递不确定结构的数据。动态调用完成后,应尽快将结果转换为强类型,让编译器重新参与检查。
如何安全地使用 dynamic:实践建议
第一,限制动态区域。如果一个方法中只有一两个变量需要动态绑定,不要因为贪图方便把整个方法的返回值也改成 dynamic。可以先用动态变量完成调用,再把结果赋给强类型变量,例如 string name = person.Name; 即使 person 是 dynamic,赋给 string 时也会发生运行时类型转换,但后续代码恢复静态检查。
第二,为动态调用补充异常处理。动态成员不存在、参数不匹配或类型转换失败都会抛出 RuntimeBinderException 或 InvalidCastException。在关键的动态调用周围使用 try-catch 捕获并记录具体错误,可以帮助快速定位是哪个成员或参数导致的问题。相比让异常直接冒泡到全局处理器,局部捕获能提供更多上下文。
第三,如果动态对象的结构来自 JSON 或外部系统,建议先用 IDictionary<string, object> 方式验证字段是否存在,再用 dynamic 访问,或使用 JObject 的 TryGetValue 方法。不要假设外部数据一定包含某个字段,弱类型数据本身就带有不确定性,结合防御性判断能避免很多运行时异常。最后,如果项目对性能要求较高,可以借助 BenchmarkDotNet 对比静态调用和 dynamic 调用的实际差距,再决定是否替换。
C# dynamic关键字动态类型静态类型修改时间:2026-09-24 02:34:35