1. 介绍
C# 是强类型语言的一员。也就是说在编译阶段变量必须有明确类型,访问类成员只能根据这个类型来做。这很安全、方便,也利于 IDE 和编译器工作。
但有时候需要操作在编译期结构未知的对象,比如:
- 执行的是另一个 .NET 语言写的代码(比如 IronPython 或 IronRuby),这些语言是动态类型的。
- 与 COM 对象交互(比如通过 Interop 操作 Microsoft Office)。
- 处理动态数据结构:比如结构不太固定的 JSON 对象。
- 不想写一堆模板代码,想“信任”对象并按名字去调用它的方法,试试看就好。
从 C# 4.0 引入 dynamic 和 DLR 后,可以写出“像 Python 那样但在 C# 里”的代码:类型检查在运行时发生,而不是编译时。用这个神奇的关键字可以让编译器放宽限制,直到程序真正运行到那一行才去验证某个方法或属性是否存在。
什么是 DLR?
Dynamic Language Runtime (DLR) — 是对 CLR (Common Language Runtime) 的一个基础设施扩展,允许实现对动态语言的支持(以及在静态语言里加入动态行为)。DLR 负责那些编译器无法提前检查的操作:比如按名字调用方法,而类型只有在运行时才会知道。借助 DLR,C# 有了“动态模式”。
2. 关键字 dynamic:它怎么工作
与 object 的区别
有些新手以为 dynamic 只是更时髦的写法,相当于 object 然后每次都去 cast。事实并非如此。
- object 类型的变量总是需要显式转换到具体类型才能调用方法(编译器会检查这一点)。
- dynamic 类型的变量告诉编译器:“相信我,我知道运行时会是什么类型——到时候再处理!”如果方法不存在,会在运行时抛出异常。
示例:
// 示例:object
object obj = "嗨,C#!";
// obj.ToUpper(); // 编译错误!编译器不知道 obj 是字符串
string s = ((string)obj).ToUpper(); // 只有这样才行
// 示例:dynamic
dynamic dyn = "嗨,C#!";
string upper = dyn.ToUpper(); // 编译器不反对,检查会在运行时进行
用 dynamic 可以访问不存在的属性和方法。如果这些成员不存在——会抛出 RuntimeBinderException。
在示例应用中的使用
假设在我们的迷你应用里有一段处理 JSON 的代码,结构可能会动态变化。用 dynamic 可以像属性总是存在那样去访问它们:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "瓦西";
user.Age = 35;
Console.WriteLine($"名字: {user.Name}, 年龄: {user.Age}");
3. DLR 在运行时是如何工作的
当变量被声明为 dynamic 时,C# 编译器记住:对该对象的任何方法、属性、索引器、运算符等访问都无法在编译期被验证。
取而代之的是,在任何动态调用处插入对系统“动态分发器”的调用。DLR 会检查:我们乐观访问的那个方法或属性真的存在吗?如果存在——执行调用;如果不存在——抛出异常。
可视化流程
+------------------------+
| 带 dynamic 的代码 |
+----------+-------------+
|
v
+----------+-------------+
| C# 编译器插入 |
| “对 DLR 的请求” |
+----------+-------------+
|
v
+----------+-------------+
| 运行时 DLR 查找 |
| 方法/属性 |
+----------+-------------+
|
/---------\
| |
找到 未找到
| |
v v
调用 抛出异常
方法 (RuntimeBinderException)
DLR 会负责在对象上查找方法和属性——并且会对常用的动态调用做缓存!
一点历史
DLR 是为原本就是动态的语言设计的:像 IronPython、IronRuby,同时也方便在 C# 里和动态对象集成。今天它常用于处理 ExpandoObject、DynamicObject,以及 COM 和动态序列化的数据。
4. 对比:static vs dynamic
我们来比较下在静态与动态范式下访问属性和方法的样子:
| 方式 | 声明 | 编译时检查 | 错误时行为 |
|---|---|---|---|
|
|
需要 cast 到需要的类型 | 编译错误或 InvalidCastException |
|
|
没有,全部在运行时检查 | RuntimeBinderException,如果方法不存在 |
| 静态类型 | |
由编译器检查 | 编译错误 |
为了更直观:
// 静态类型化
MyUser u = new MyUser();
u.PrintInfo(); // 由编译器检查
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // 编译器不检查,只有运行时会找到方法
// object
object ou = new MyUser();
// ou.PrintInfo(); // 编译错误
((MyUser)ou).PrintInfo(); // 需要显式 cast
静态方法和 dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — 一切正常
Console.WriteLine(d.FakeMethod()); // 运行时抛出 RuntimeBinderException
但要小心!如果你打错了名字或方法不存在——只有在程序运行时才会显现。这就是典型的“生产环境出错”例子。
5. 实践例子:真实场景下的动态
使用 ExpandoObject
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "谢尔盖";
person.Greet = (Action)(() => Console.WriteLine($"你好,{person.Name}!"));
person.Greet(); // 输出:你好,谢尔盖!
这里我们在运行时给对象添加了属性 Name,甚至添加了一个委托 Greet。很像 JavaScript 里的对象操作。
绑定到 JSON 结构
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""安德烈"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject
(json); Console.WriteLine(obj.name); // 安德烈 Console.WriteLine(obj.age); // 30
注意:如果写 obj.surname —— 会在运行时出错。
COM-Interop(Office 自动化)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // 可以“看到”这个属性,尽管编译器不知道它的类型
6. 有用的小细节
DLR、dynamic 与其他 .NET 语言的结合
- IronPython/IronRuby:可以实例化这些语言的对象并通过 dynamic 在 C# 中调用它们的方法。
- COM 对象:通过 dynamic 自动包装方法和属性。
- 自定义动态对象:实现接口 IDynamicMetaObjectProvider,可以做出自己的支持动态调用的类。
与 IronPython 集成的示例
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
ScriptEngine engine = Python.CreateEngine();
dynamic py = engine.Execute("class Test: pass\nTest()");
py.x = 42;
Console.WriteLine(py.x); // 42
如何实现自定义动态类型
通常继承自 DynamicObject 更简单。
using System.Dynamic;
class MyDynamic : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Ping")
{
result = "Pong!";
return true;
}
result = null;
return false;
}
}
dynamic d = new MyDynamic();
Console.WriteLine(d.Ping); // Pong!
Console.WriteLine(d.Miss); // null(不抛出异常)
什么时候 dynamic 会有害
- 降低性能(动态调用比静态调用慢)。
- 增加调试和维护难度。
- 丢失大部分 IDE 的便利:自动补全、重构、引用查找等。
- 可能导致只在用户或服务器端才暴露出来的错误。
什么时候真的该用 dynamic
- 与外部库集成,不可能或不值得写包装的场景(COM、Python、JavaScript)。
- 操作弱类型结构(JSON/XML/ExpandoObject)。
- 写框架、元编程、需要天生动态的灵活 API。
在其他所有情况下,更推荐用静态类型,享受编译器带来的便利 :-)
7. 使用 dynamic 和 DLR 时的典型错误
错误 #1:访问不存在的成员。
对 dynamic 调用不存在的方法或属性会在运行时引发 RuntimeBinderException,开发阶段很难发现。
错误 #2:忽视类型检查。
如果在调用前不做类型检查(比如 if (obj is IDictionary<string, object>)),运行时可能会出现意外错误。
错误 #3:在公共 API 中使用 dynamic。
动态类型会让代码维护变难,因为 IDE 和分析工具无法提示正确的成员,降低可读性。
错误 #4:在对性能敏感的代码中滥用 dynamic。
动态调用比静态慢,会增加开销。只有在需要时才使用 dynamic。
GO TO FULL VERSION