1. 程式人生 > >C# 中的動態型別

C# 中的動態型別

> 翻譯自 Camilo Reyes 2018年10月15日的文章 [《Working with the Dynamic Type in C#》](https://www.red-gate.com/simple-talk/dotnet/c-programming/working-with-the-dynamic-type-in-c/) [^1] > > .NET 4 中引入了動態型別。動態物件使您可以處理諸如 JSON 文件之類的結構,這些結構的組成可能要到執行時才能知道。在本文中,Camilo Reyes 解釋瞭如何使用動態型別。 [^1]: Working with the Dynamic Type in C# .NET 4.0 中引入的 `dynamic` 關鍵字為 C# 程式設計帶來了一個正規化轉變。對於 C# 程式設計師來說,強型別系統之上的動態行為可能會讓人感到不適 —— 當您在編譯過程中失去型別安全性時,這似乎是一種倒退。 動態程式設計可能使您面臨執行時錯誤。宣告一個在執行過程中會發生變化的動態變數是可怕的,當開發人員對資料做出錯誤的假設時,程式碼質量就會受到影響。 對 C# 程式設計師來說,避免程式碼中的動態行為是合乎邏輯的,具有強型別的經典方法有很多好處。通過型別檢查得到的資料型別的良好反饋對於正常執行的程式是至關重要的,一個好的型別系統可以更好地表達意圖並減少程式碼中的歧義。 隨著動態語言執行時(Dynamic Language Runtime,DLR)的引入,這對 C# 意味著什麼呢? .NET 提供了豐富的型別系統,可用於編寫企業級軟體。讓我們來仔細看看 `dynamic` 關鍵字,並探索一下它的功能。 ## 型別層次結構 公共語言執行時(Common Language Runtime,CLR)中的每種型別都繼承自 `System.Object`,現在,請重複閱讀這句話,直到將其銘記於心。這意味著 `object` 型別是整個型別系統的公共父類。當我們研究更神奇的動態行為時,這一事實本身就能為我們提供幫助。這裡的想法是開發這種“程式碼感”,以便於您瞭解如何駕馭 C# 中的動態型別。 為了演示這一點,您可以編寫以下程式: ```csharp Console.WriteLine("long inherits from ValueType: " + typeof(long).IsSubclassOf(typeof(ValueType))); ``` 我將忽略 `using` 語句直到本文結束,以保持對程式碼示例的專注。然後,我再介紹每個名稱空間及其作用。這樣我就不必重複說過的話,並提供了一個回顧所有型別的機會。 上面的程式碼在控制檯中的運算結果為 `True`。.NET 中的 `long` 型別是值型別,因此它更像是列舉或結構體。`ValueType` 重寫來自 `object` 類的預設行為。`ValueType` 的子類在棧(stack)上執行,它們的生命週期較短,效率更高。 要驗證 `ValueType` 是繼承自 `System.Object` 的,請執行以下程式碼: ```csharp Console.WriteLine("ValueType inherits from System.Object: " + typeof(ValueType).IsSubclassOf(typeof(Object))); ``` 它的運算結果為 `True`。這是一條可以追溯到 `System.Object` 的繼承鏈。對於值型別,鏈中至少有兩個父級。 再看一下從 `System.Object` 派生的另一個 C# 型別,例如: ```csharp Console.WriteLine("string inherits from System.Object: " + typeof(string).IsSubclassOf(typeof(Object))); ``` 此程式碼在控制檯中顯示為 `True`。另一種從 `object` 繼承的型別是引用型別,引用型別在堆(heap)上分配並進行垃圾回收,CLR 管理著引用型別,並在必要時從堆中釋放它們。 檢視下圖,您可以直觀地看到 CLR 的型別系統: ![CLR’s type system](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210120011840012-1269059406.png) 值型別和引用型別都是 CLR 的基本構建塊,這種優雅的型別系統在 .NET 4.0 和動態型別之前就有了。我建議您在使用 C# 中的型別時,在腦海中記住這張圖。那麼,DLR 是如何適應這張圖的呢? ## 動態語言執行時(DLR) 動態語言執行時(Dynamic Language Runtime, DLR)是處理動態物件的一種便捷方法。比如,假設您有 XML 或 JSON 格式的資料,其中的成員事先並不知道。DLR 允許您使用自然程式碼來處理物件和訪問成員。 對於 C#,這使您可以處理在編譯時不知道其型別的庫。動態型別消除了自然 API 程式碼中的萬能字串。這就開啟了像 IronPython 一樣位於 CLR 之上的動態語言。 可以將 DLR 視為支援三項主要服務: - 表示式樹,來自 System.Linq.Expressions 名稱空間。編譯器在執行時生成具有動態語言互操作性的表示式樹。動態語言超出了本文的討論範圍,這裡就不作介紹了。 - 呼叫站點快取,即快取動態操作的結果。DLR 快取像 `a + b` 之類的操作,並存儲 `a` 和 `b` 的特徵。當執行動態操作時,DLR 將檢索先前操作中可用的資訊。 - 動態物件互操作性是可用於訪問 DLR 的 C# 型別。這些型別包括 `DynamicObject` 和 `ExpandoObject`。可用的型別還有很多,但是在處理動態型別時請注意這兩種型別。 要了解 DLR 和 CLR 是如何結合在一起的,請看下圖: ![how the DLR and CLR fit together](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210120012034916-698849766.png) DLR 位於 CLR 之上。回想一下,我說過的*每種型別都是從 `System.Object` 派生而來的*。嗯,這句話對於 CLR 是適用的,但是對於 DLR 呢?我們使用下面的程式來測試一下這個理論: ```csharp Console.WriteLine("ExpandoObject inherits from System.Object: " + typeof(ExpandoObject).IsSubclassOf(typeof(Object))); Console.WriteLine("DynamicObject inherits from System.Object: " + typeof(DynamicObject).IsSubclassOf(typeof(Object))); ``` `ExpandoObject` 和 `DynamicObject` 在命令列中輸出的值都是 `True`。可以將這兩個類視為使用動態型別的基本構建塊,它們清楚地描繪了兩個執行時是如何結合在一起的。 ## 一個 JSON 序列化程式 動態型別解決的一個問題是,當您有一個不知道其成員的 JSON HTTP 請求時,假設要在 C# 中使用此任意的 JSON。要解決這個問題,請將此 JSON 序列化為 C# 動態型別。 我將使用 Newtonsoft 序列化庫,您可以通過 NuGet 新增此依賴項,例如: ```csharp dotnet add package Newtonsoft.Json –-version 11.0.2 ``` 您可以使用這個序列化程式來處理 `ExpandoObject` 和 `DynamicObject`。探索每種動態型別給動態程式設計帶來了什麼。 ## ExpandoObject 動態型別 `ExpandoObject` 是一種方便的型別,允許設定和檢索動態成員。它實現了 `IDynamicMetaObjectProvider`,該介面允許在 DLR 中的語言之間共享例項。因為它實現了 `IDictionary` 和 `IEnumerable`,所以它也可以處理 CLR 中的型別。舉例來說,它允許將 `ExpandoObject` 的例項轉換為 `IDictionary`,然後像其它任意的 `IDictionary` 型別一樣列舉成員。 要用 `ExpandoObject` 處理任意 JSON,您可以編寫以下程式: ```csharp var exObj = JsonConvert.DeserializeObject("{\"a\":1}") as dynamic; Console.WriteLine($"exObj.a = {exObj?.a}, type of {exObj?.a.GetType()}"); //exObj.a = 1, type of System.Int64 ``` 它將會在控制檯列印 `1` 和 `long`。請注意,儘管它是一個動態 JSON,但它會繫結到 CLR 中的 C# 型別。由於數字的型別未知,因此序列化程式預設會選擇最大的 `long` 型別。注意,我成功地將序列化結果轉換成了具有 null 檢查的 `dynamic` 型別,其原因是序列化程式返回來自 CLR 的 `object` 型別。因為 `ExpandoObject` 繼承自 `System.Object`,所以可以被拆箱成 DLR 型別。 更奇妙的是,可以用 `IDictionary` 列舉 `exObj`: ```csharp foreach (var exObjProp in exObj as IDictionary ?? new Dictionary()) { Console.WriteLine($"IDictionary = {exObjProp.Key}: {exObjProp.Value}"); } ``` 它在控制檯中輸出 `IDictionary = a: 1`。請確保使用 `string` 和 `object` 作為鍵和值的型別。否則,將在轉換的過程中丟擲 `RuntimeBinderException` 異常。 ## DynamicObject 動態型別 `DynamicObject` 提供對動態型別的精確控制。您可以繼承該型別並重寫動態行為。例如,您可以定義如何設定和獲取型別中的動態成員。`DynamicObject` 允許您通過重寫選擇實現哪些動態操作。這比實現 `IDynamicMetaObjectProvider` 的語言實現方式更易訪問。它是一個抽象類,需要繼承它而不是例項化它。該類有 14 個虛方法,它們定義了型別的動態操作,每個虛方法都允許重寫以指定動態行為。 假設您想要精確控制動態 JSON 中的內容。儘管事先不知道其屬性,您卻可以使用 `DynamicObject` 來控制型別。 讓我們來重寫三個方法,`TryGetMember`、`TrySetMember` 和 `GetDynamicMemberNames`: ```csharp public class TypedDynamicJson : DynamicObject { private readonly IDictionary _typedProperty; public TypedDynamicJson() { _typedProperty = new Dictionary(); } public override bool TryGetMember(GetMemberBinder binder, out object result) { T typedObj; if (_typedProperty.TryGetValue(binder.Name, out typedObj)) { result = typedObj; return true; } result = null; return false; } public override bool TrySetMember(SetMemberBinder binder, object value) { if (value.GetType() != typeof(T)) { return false; } _typedProperty[binder.Name] = (T)value; return true; } public override IEnumerable GetDynamicMemberNames() { return _typedProperty.Keys; } } ``` C# 泛型強型別 `_typedProperty` 以泛型的方式驅動成員型別。這意味著其屬性型別來自泛型型別 `T`。動態 JSON 成員位於字典中,並且僅儲存泛型型別。此動態型別允許同一型別的同類成員集合。儘管它允許動態成員集,但您可以強型別其行為。假設您只關心任意 JSON 中的 `long` 型別: ```csharp var dynObj = JsonConvert.DeserializeObject>("{\"a\":1,\"b\":\"1\"}") as dynamic; Console.WriteLine($"dynObj.a = {dynObj?.a}, type of {dynObj?.a.GetType()}"); var members = string.Join(",", dynObj?.GetDynamicMemberNames()); Console.WriteLine($"dynObj member names: {members}"); ``` 結果是,您將看到一個值為 `1` 的屬性,因為第二個屬性是 `string` 型別。如果將泛型型別更改為 `string`,將會獲得第二個屬性。 ## 型別結果 到目前為止,已經涉及了相當多的領域; 以下是一些亮點: - CLR 和 DLR 中的所有型別都繼承自 `System.Object` - DLR 是所有動態操作發生的地方 - `ExpandoObject` 實現了 CLR 中諸如 `IDictionary` 的可列舉型別 - `DynamicObject` 通過虛方法對動態型別進行精確控制 看一下在控制檯的結果截圖: ![dynamic type results](https://img2020.cnblogs.com/blog/2074831/202101/2074831-20210120011936703-888767629.png) ## 單元測試 對於單元測試,我將使用 xUnit 測試框架。 在 .NET Core 中,您可以使用 `dotnet new xunit` 命令新增一個測試專案。一個顯而易見的問題是模擬和驗證動態引數,例如,假設您想驗證一個方法呼叫是否具有動態屬性。 要使用 Moq 模擬庫,您可以通過 NuGet 新增此依賴項,例如: ```csharp dotnet add package Moq –-version 4.10.0 ``` 假設您有一個介面,其想法是驗證它是否被正確的動態物件呼叫。 ```csharp public interface IMessageBus { void Send(dynamic message); } ``` 忽略該介面的實現。這些實現細節對於編寫單元測試不是必需的。下面是被測試的系統: ```csharp public class MessageService { private readonly IMessageBus _messageBus; public MessageService(IMessageBus messageBus) { _messageBus = messageBus; } public void SendRawJson(string json) { var message = JsonConvert.DeserializeObject(json) as dynamic; _messageBus.Send(message); } } ``` 您可以使用泛型,這樣就可以為序列化程式傳入動態型別。然後呼叫 `IMessageBus` 併發送動態訊息。被測試的方法接受一個 `string` 引數,並使用 `dynamic` 型別進行呼叫。 對於單元測試,請將其封裝在 `MessageServiceTests` 類中。首先初始化 Mock 和被測試的服務: ```csharp public class MessageServiceTests { private readonly Mock _messageBus; private readonly MessageService _service; public MessageServiceTests() { _messageBus = new Mock(); _service = new MessageService(_messageBus.Object); } } ``` 使用 Moq 庫中的 C# 泛型來模擬 `IMessageBus`,然後使用 `Object` 屬性建立一個模擬例項。在所有的單元測試中私有例項變數都很有用,高可重用性的私有例項增加了類的內聚性。 使用 Moq 驗證呼叫,一種直觀的方式是嘗試這麼做: ```csharp _messageBus.Verify(m => m.Send(It.Is(o => o != null && (o as dynamic).a == 1))); ``` 但是,遺憾的是,您將看到這樣的錯誤訊息:“表示式樹不能包含動態操作。” 這是因為 C# lambda 表示式無法訪問 DLR,它期望一個來自 CLR 的型別,這使得此動態引數難以驗證。記得您的訓練,利用您的“程式碼感”來解決這個問題。 要處理諸如型別之間不一致的問題,請使用 `Callback` 方法: ```csharp dynamic message = null; _messageBus.Setup(m => m.Send(It.IsAny())).Callback(o => message = o); ``` 請注意,`Callback` 方法將型別轉換為 `System.Object`。因為所有型別都繼承自 `object` 型別,所以可以將其賦值為 `dynamic` 型別。C# 可以把此 lambda 表示式中的 `object` 拆箱成 `dynamic message`。 是時候為 `ExpandoObject` 型別編寫一個漂亮的單元測試了。使用 xUnit 作為測試框架,您將看到帶有 `Fact` 屬性的方法。 ```csharp [Fact] public void SendsWithExpandoObject() { // arrange const string json = "{\"a\":1}"; dynamic message = null; _messageBus.Setup(m => m.Send(It.IsAny())).Callback(o => message = o); // act _service.SendRawJson(json); // assert Assert.NotNull(message); Assert.Equal(1, message.a); } ``` 使用 `DynamicObject` 型別進行測試,重用您之前看到的 `TypedDynamicJson`: ```csharp [Fact] public void SendsWithDynamicObject() { // arrange const string json = "{\"a\":1,\"b\":\"1\"}"; dynamic message = null; _messageBus.Setup(m => m.Send(It.IsAny>())).Callback(o => message = o); // act _service.SendRawJson>(json); // assert Assert.NotNull(message); Assert.Equal(1, message.a); Assert.Equal("a", string.Join(",", message.GetDynamicMemberNames())); } ``` 使用 C# 泛型,您可以在重用程式碼的同時轉換序列化程式的動態型別。Moq 中的 `Callback` 方法允許您在兩種型別系統之間進行必要的跳轉。擁有一個優雅的型別層次結構和一個共同的父類成為了一個救星。 ## Using 語句 下面的 using 語句是程式碼示例的一部分: - System: CLR 的基礎型別,例如 Object 和 Console - System.Collections.Generic: 可列舉型別,例如 IDictionary - System.Dynamic: DLR 的動態型別,例如 ExpandoObject 和 DynamicObject - Newtonsonft.Json: JSON 序列化程式 - Moq: 模擬庫 - Xunit: 測試框架 ## 總結 C# 動態型別或許看起來令人望而生畏,但它在強型別系統之上有很多好處。DLR 是所有動態操作發生和與 CLR 互動的地方,型別繼承使同時處理這兩個型別系統變得容易。在 C# 中,動態和靜態程式設計之間並沒有對立,這兩種型別系統共同協作,以創造性的方式解決動態問題。 ---