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# 編譯器會記住對該物件的任何方法、屬性、索引器、運算子等的存取在編譯期無法檢查。
取而代之的是,在每個動態呼叫的位置插入一段去呼叫系統「動態 dispatcher」的程式碼。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(); // 必須明確轉型
靜態方法與 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,甚至新增 delegate 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 Automation)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // 可以直接操作 Visible 屬性,雖然編譯器不知道它的型別
6. 有用的細節
DLR、dynamic 與與其他 .NET 語言的協作
- IronPython/IronRuby:可以在 C# 中以 dynamic 實例化這些語言的物件並呼叫它們的方法。
- 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
- 整合外部函式庫、無法或不想寫 wrapper 的時候(COM、Python、JavaScript)。
- 操作弱型別的結構(JSON/XML/ExpandoObject)。
- 撰寫 framework、元程式或需要有彈性的動態 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