CodeGym /課程 /C# SELF /動態類型 ( dynamic) 與 DLR

動態類型 ( dynamic) 與 DLR

C# SELF
等級 63, 課堂 2
開放

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

來比較一下在靜態與動態範式下對屬性與方法的呼叫長什麼樣子:

方式 宣告 編譯時檢查 錯誤時行為
object
object obj = ...
需要 cast 到正確型別 編譯錯誤或 InvalidCastException
dynamic
dynamic dyn = ...
沒有,全部在執行期檢查 RuntimeBinderException,若方法不存在
靜態型別
MyClass a = ...
由編譯器檢查 編譯錯誤

示例看得更清楚:


// 靜態型別
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。

2
任務
C# SELF, 等級 63, 課堂 2
上鎖
簡單使用 `dynamic`
簡單使用 `dynamic`
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION