前言
C# 4.0 引入 dynamic 關鍵字,標誌著靜態型別語言向動態語言特性的重大跨越。它允許在編譯期繞過型別檢查,將成員存取解析延遲至執行期。本文深度解析 dynamic 的運作原理、適用場景、效能影響與最佳實務。
dynamic 核心機制
編譯期 vs 執行期
// 靜態型別:編譯期檢查
string s = "hello";
s.ToUpper(); // ✅ 編譯通過
s.NonExistMethod(); // ❌ 編譯錯誤 CS1061
// 動態型別:執行期解析
dynamic d = "hello";
d.ToUpper(); // ✅ 執行期解析成功
d.NonExistMethod(); // ✅ 編譯通過,執行期拋出 RuntimeBinderException
底層實作:DLR (Dynamic Language Runtime)
dynamic 本質上是 object 的別名,編譯後 IL 中標記為 [Dynamic] 屬性
- 呼叫端建立
CallSite 快取,包含作業類型 (呼叫方法、存取屬性、轉型等)
- 執行期透過
IDynamicMetaObjectProvider 介面進行動態派發
- 首次呼叫較慢 (建立表達式樹、編譯委派),後續呼叫透過快取極快
四大核心應用場景
1. 自動反射 簡化反射程式碼
// 傳統反射:冗長、易錯、無智能感知
object obj = GetUnknownObject();
MethodInfo method = obj.GetType().GetMethod("Process");
object result = method.Invoke(obj, new object[] { "arg1", 123 });
// dynamic:簡潔、可讀、維護性高
dynamic obj = GetUnknownObject();
var result = obj.Process("arg1", 123); // 如同強型別呼叫
2. COM 元件互操作 免去繁瑣的 Type Library 匯入
// Excel 自動化:傳統需加入 Microsoft.Office.Interop.Excel 參考
// dynamic 可直接操作 COM 物件,無需編譯期型別
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true;
dynamic workbook = excel.Workbooks.Add();
dynamic sheet = workbook.ActiveSheet;
sheet.Cells[1, 1] = "Hello Dynamic COM!";
workbook.SaveAs(@"C:\Temp\DynamicExcel.xlsx");
excel.Quit();
// 注意:仍需手動釋放 COM 物件 (Marshal.ReleaseComObject)
3. 動態語言互操作 (IronPython, IronRuby, JavaScript)
// IronPython 整合範例
var engine = IronPython.Hosting.Python.CreateEngine();
dynamic scope = engine.CreateScope();
engine.Execute("def add(a, b): return a + b", scope);
// 直接呼叫 Python 函數
int result = scope.add(10, 20); // result = 30
Console.WriteLine($"Python 計算結果: {result}");
4. HTML DOM / JSON / XML 動態操作
// JSON 動態存取 (搭配 Newtonsoft.Json / System.Text.Json)
string json = @"{ 'user': { 'name': 'John', 'age': 30, 'skills': ['C#', 'SQL'] } }";
dynamic data = JsonConvert.DeserializeObject<dynamic>(json);
Console.WriteLine(data.user.name); // John
Console.WriteLine(data.user.age); // 30
Console.WriteLine(data.user.skills[0]); // C#
// 新增屬性
data.user.city = "Taipei";
// 序列化回 JSON
string newJson = JsonConvert.SerializeObject(data);
// System.Text.Json (NET Core 3.0+) 原生支援
using System.Text.Json;
using System.Text.Json.Nodes;
JsonNode node = JsonNode.Parse(json);
Console.WriteLine(node["user"]["name"]); // 強型別但靈活
進階技巧與模式
自訂動態物件:實作 IDynamicMetaObjectProvider
using System.Dynamic;
// 簡易動態字典:支援 property 與 indexer 存取
public class DynamicDictionary : DynamicObject
{
private readonly Dictionary<string, object> _dict = new();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return _dict.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
_dict[binder.Name] = value;
return true;
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
if (indexes.Length == 1 && indexes[0] is string key)
return _dict.TryGetValue(key, out result);
result = null;
return false;
}
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
if (indexes.Length == 1 && indexes[0] is string key)
{
_dict[key] = value;
return true;
}
return false;
}
}
// 使用
dynamic dict = new DynamicDictionary();
dict.Name = "Dynamic";
dict["Age"] = 25;
Console.WriteLine($"{dict.Name}, {dict.Age}, {dict["Age"]}");
ExpandoObject:內建動態物件
using System.Dynamic;
dynamic expando = new ExpandoObject();
expando.Name = "Expando";
expando.SayHello = new Action(() => Console.WriteLine($"Hello, {expando.Name}!"));
expando.SayHello(); // Hello, Expando!
// 可動態增減成員,實作 INotifyPropertyChanged,適合 MVVM ViewModel
效能分析與優化
效能成本來源
| 階段 | 成本 | 說明 |
| 首次呼叫 | 高 (微秒級) | 建立 CallSite、表達式樹編譯、DLR 快取 |
| 後續呼叫 | 低 (納秒級) | 直接命中快取委派,接近靜態呼叫 |
| 記憶體 | 中 | 每個 CallSite 佔用記憶體,大量動態呼叫需注意 |
基準測試參考 (100 萬次迴循)
// 靜態呼叫: ~15 ms
// dynamic 首次: ~120 ms (含快取建立)
// dynamic 後續: ~18 ms (接近靜態)
// 反射 Invoke: ~2500 ms
優化策略
- 避免熱路徑使用 dynamic:高頻迴路、遊戲主迴路、即時運算請用靜態型別
- 快取 dynamic 物件:重複使用同一動態物件實例,利用 CallSite 快取
- 區域變數型別推斷:
var result = dynamicObj.Method(); 編譯器推斷為 dynamic,後續存取仍為動態
- 必要時轉回強型別:
string name = (string)dynamicObj.Name; 後續操作回歸靜態
常見陷阱與除錯技巧
| 陷阱 | 現象 | 解決方案 |
| 無智能感知 | VS 無法提供成員提示 | 撰寫單元測試驗證、使用註解文件、搭配強型別介面 |
| 執行期才發現拼字錯誤 | RuntimeBinderException | 完整測試覆蓋、程式碼審查、使用 nameof() 輔助 |
| 擴充方法失效 | dynamic 不支援擴充方法 | 改用靜態類別呼叫:Extensions.Method(obj, arg) |
| 匿名型別跨組件存取 | internal 匿名型別在其他組件為不可見 | 使用 ExpandoObject、具體型別或 public DTO |
| Lambda 運算式無法推斷 | dynamic 不能作為 Lambda 參數 | 明確指定型別或轉換 |
| 遞迴動態呼叫堆疊溢位 | CallSite 快取失效導致無限遞迴 | 避免動態遞迴、增加快取層級 |
dynamic vs 替代方案比較
| 方案 | 適用場景 | 優點 | 缺點 |
| dynamic | COM、動態語言、JSON、反射簡化 | 語法簡潔、DLR 快取快 | 無編譯檢查、無智能感知、效能較靜態慢 |
| 反射 | 框架開發、插件系統、序列化 | 完全控制、支援私有成員 | 程式碼冗長、效能差、易錯 |
| 介面/基類 | 已知契約的多型 | 編譯期安全、智能感知、效能最佳 | 需預先定義契約、耦合度較高 |
| JsonNode / JObject | JSON 處理 | 強型別 DOM、LINQ 查詢、原生支援 | 僅限 JSON 結構 |
| Source Generator | 編譯期生成強型別包裝 | 零執行期成本、完整工具支援 | 需 .NET 5+、增加建置複雜度 |
最佳實務建議
- 明確標記意圖:變數命名加註
dynamic 前綴或註解,提醒維護者
- 邊界隔離:在邊界層 (COM 互操作、JSON 解析) 使用 dynamic,核心業務邏輯盡快轉為強型別
- 單元測試覆蓋:所有 dynamic 呼叫路徑必須有測試案例
- 避免公開 API 使用 dynamic:公開介面請使用強型別,保護呼叫端
- 效能敏感區域禁用:使用
[Dynamic] 屬性標記時請三思
總結
dynamic 是 C# 連結動態世界的橋樑,為 COM 互操作、動態語言整合、JSON/XML 處理、反射簡化帶來極大便利。但「大權在手,責任在肩」——它犧牲了編譯期安全與工具支援,換取執行期靈活性。明智的策略是:在邊界使用 dynamic,核心回歸強型別,並以完整測試守護品質。隨著 System.Text.Json.Nodes、Source Generators、Required Members 等現代功能成熟,許多過去需 dynamic 的場景已有更佳替代方案,選擇時請綜合評估。
- 上一篇文章: C# 空值判斷完全指南:null、String.Empty、DBNull 與可為 Null 型別深度解析
- 下一篇文章: C# 取得磁碟資訊完整指南:DriveInfo 類別與磁碟空間監控實作
Copyright ©
程式碼之家 All Rights Reserved