C# 中的动态类型
翻譯自 Camilo Reyes 2018年10月15日的文章?《Working with the Dynamic Type in C#》?[1]??
.NET 4 中引入了動態類型。動態對象使您可以處理諸如 JSON 文檔之類的結構,這些結構的組成可能要到運行時才能知道。在本文中,Camilo Reyes 解釋了如何使用動態類型。
.NET 4.0 中引入的?dynamic?關鍵字為 C# 編程帶來了一個范式轉變。對于 C# 程序員來說,強類型系統之上的動態行為可能會讓人感到不適 —— 當您在編譯過程中失去類型安全性時,這似乎是一種倒退。
動態編程可能使您面臨運行時錯誤。聲明一個在執行過程中會發生變化的動態變量是可怕的,當開發人員對數據做出錯誤的假設時,代碼質量就會受到影響。
對 C# 程序員來說,避免代碼中的動態行為是合乎邏輯的,具有強類型的經典方法有很多好處。通過類型檢查得到的數據類型的良好反饋對于正常運行的程序是至關重要的,一個好的類型系統可以更好地表達意圖并減少代碼中的歧義。
隨著動態語言運行時(Dynamic Language Runtime,DLR)的引入,這對 C# 意味著什么呢?.NET 提供了豐富的類型系統,可用于編寫企業級軟件。讓我們來仔細看看?dynamic?關鍵字,并探索一下它的功能。
類型層次結構
公共語言運行時(Common Language Runtime,CLR)中的每種類型都繼承自?System.Object,現在,請重復閱讀這句話,直到將其銘記于心。這意味著?object?類型是整個類型系統的公共父類。當我們研究更神奇的動態行為時,這一事實本身就能為我們提供幫助。這里的想法是開發這種“代碼感”,以便于您了解如何駕馭 C# 中的動態類型。
為了演示這一點,您可以編寫以下程序:
Console.WriteLine("long inherits from ValueType: " + typeof(long).IsSubclassOf(typeof(ValueType)));我將忽略?using?語句直到本文結束,以保持對代碼示例的專注。然后,我再介紹每個命名空間及其作用。這樣我就不必重復說過的話,并提供了一個回顧所有類型的機會。
上面的代碼在控制臺中的運算結果為?True。.NET 中的?long?類型是值類型,因此它更像是枚舉或結構體。ValueType?重寫來自?object?類的默認行為。ValueType?的子類在棧(stack)上運行,它們的生命周期較短,效率更高。
要驗證?ValueType?是繼承自?System.Object?的,請執行以下代碼:
Console.WriteLine("ValueType inherits from System.Object: " + typeof(ValueType).IsSubclassOf(typeof(Object)));它的運算結果為?True。這是一條可以追溯到?System.Object?的繼承鏈。對于值類型,鏈中至少有兩個父級。
再看一下從?System.Object?派生的另一個 C# 類型,例如:
Console.WriteLine("string inherits from System.Object: " + typeof(string).IsSubclassOf(typeof(Object)));此代碼在控制臺中顯示為?True。另一種從?object?繼承的類型是引用類型,引用類型在堆(heap)上分配并進行垃圾回收,CLR 管理著引用類型,并在必要時從堆中釋放它們。
查看下圖,您可以直觀地看到 CLR 的類型系統:
值類型和引用類型都是 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 是如何結合在一起的,請看下圖:
DLR 位于 CLR 之上。回想一下,我說過的每種類型都是從?System.Object?派生而來的。嗯,這句話對于 CLR 是適用的,但是對于 DLR 呢?我們使用下面的程序來測試一下這個理論:
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 添加此依賴項,例如:
dotnet add package Newtonsoft.Json –-version 11.0.2您可以使用這個序列化程序來處理?ExpandoObject?和?DynamicObject。探索每種動態類型給動態編程帶來了什么。
ExpandoObject 動態類型
ExpandoObject?是一種方便的類型,允許設置和檢索動態成員。它實現了?IDynamicMetaObjectProvider,該接口允許在 DLR 中的語言之間共享實例。因為它實現了?IDictionary?和?IEnumerable,所以它也可以處理 CLR 中的類型。舉例來說,它允許將?ExpandoObject?的實例轉換為?IDictionary,然后像其它任意的?IDictionary?類型一樣枚舉成員。
要用?ExpandoObject?處理任意 JSON,您可以編寫以下程序:
var exObj = JsonConvert.DeserializeObject<ExpandoObject>("{\"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:
foreach (var exObjProp in exObj as IDictionary<string, object> ?? new Dictionary<string, object>()) {Console.WriteLine($"IDictionary = {exObjProp.Key}: {exObjProp.Value}"); }它在控制臺中輸出?IDictionary = a: 1。請確保使用?string?和?object?作為鍵和值的類型。否則,將在轉換的過程中拋出?RuntimeBinderException?異常。
DynamicObject 動態類型
DynamicObject?提供對動態類型的精確控制。您可以繼承該類型并重寫動態行為。例如,您可以定義如何設置和獲取類型中的動態成員。DynamicObject?允許您通過重寫選擇實現哪些動態操作。這比實現?IDynamicMetaObjectProvider?的語言實現方式更易訪問。它是一個抽象類,需要繼承它而不是實例化它。該類有 14 個虛方法,它們定義了類型的動態操作,每個虛方法都允許重寫以指定動態行為。
假設您想要精確控制動態 JSON 中的內容。盡管事先不知道其屬性,您卻可以使用?DynamicObject?來控制類型。
讓我們來重寫三個方法,TryGetMember、TrySetMember?和?GetDynamicMemberNames:
public class TypedDynamicJson<T> : DynamicObject {private readonly IDictionary<string, T> _typedProperty;public TypedDynamicJson(){_typedProperty = new Dictionary<string, T>();}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<string> GetDynamicMemberNames(){return _typedProperty.Keys;} }C# 泛型強類型?_typedProperty?以泛型的方式驅動成員類型。這意味著其屬性類型來自泛型類型?T。動態 JSON 成員位于字典中,并且僅存儲泛型類型。此動態類型允許同一類型的同類成員集合。盡管它允許動態成員集,但您可以強類型其行為。假設您只關心任意 JSON 中的?long?類型:
var dynObj = JsonConvert.DeserializeObject<TypedDynamicJson<long>>("{\"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?通過虛方法對動態類型進行精確控制
看一下在控制臺的結果截圖:
單元測試
對于單元測試,我將使用 xUnit 測試框架。在 .NET Core 中,您可以使用?dotnet new xunit?命令添加一個測試項目。一個顯而易見的問題是模擬和驗證動態參數,例如,假設您想驗證一個方法調用是否具有動態屬性。
要使用 Moq 模擬庫,您可以通過 NuGet 添加此依賴項,例如:
dotnet add package Moq –-version 4.10.0假設您有一個接口,其想法是驗證它是否被正確的動態對象調用。
public interface IMessageBus {void Send(dynamic message); }忽略該接口的實現。這些實現細節對于編寫單元測試不是必需的。下面是被測試的系統:
public class MessageService {private readonly IMessageBus _messageBus;public MessageService(IMessageBus messageBus){_messageBus = messageBus;}public void SendRawJson<T>(string json){var message = JsonConvert.DeserializeObject<T>(json) as dynamic;_messageBus.Send(message);} }您可以使用泛型,這樣就可以為序列化程序傳入動態類型。然后調用?IMessageBus?并發送動態消息。被測試的方法接受一個?string?參數,并使用?dynamic?類型進行調用。
對于單元測試,請將其封裝在?MessageServiceTests?類中。首先初始化 Mock 和被測試的服務:
public class MessageServiceTests {private readonly Mock<IMessageBus> _messageBus;private readonly MessageService _service;public MessageServiceTests(){_messageBus = new Mock<IMessageBus>();_service = new MessageService(_messageBus.Object);} }使用 Moq 庫中的 C# 泛型來模擬?IMessageBus,然后使用?Object?屬性創建一個模擬實例。在所有的單元測試中私有實例變量都很有用,高可重用性的私有實例增加了類的內聚性。
使用 Moq 驗證調用,一種直觀的方式是嘗試這么做:
_messageBus.Verify(m => m.Send(It.Is<ExpandoObject>(o => o != null && (o as dynamic).a == 1)));但是,遺憾的是,您將看到這樣的錯誤消息:“表達式樹不能包含動態操作。” 這是因為 C# lambda 表達式無法訪問 DLR,它期望一個來自 CLR 的類型,這使得此動態參數難以驗證。記得您的訓練,利用您的“代碼感”來解決這個問題。
要處理諸如類型之間不一致的問題,請使用?Callback?方法:
dynamic message = null;_messageBus.Setup(m => m.Send(It.IsAny<ExpandoObject>())).Callback<object>(o => message = o);請注意,Callback?方法將類型轉換為?System.Object。因為所有類型都繼承自?object?類型,所以可以將其賦值為?dynamic?類型。C# 可以把此 lambda 表達式中的?object?拆箱成?dynamic message。
是時候為?ExpandoObject?類型編寫一個漂亮的單元測試了。使用 xUnit 作為測試框架,您將看到帶有?Fact?屬性的方法。
[Fact] public void SendsWithExpandoObject() {// arrangeconst string json = "{\"a\":1}";dynamic message = null;_messageBus.Setup(m => m.Send(It.IsAny<ExpandoObject>())).Callback<object>(o => message = o);// act_service.SendRawJson<ExpandoObject>(json);// assertAssert.NotNull(message);Assert.Equal(1, message.a); }使用?DynamicObject?類型進行測試,重用您之前看到的?TypedDynamicJson:
[Fact] public void SendsWithDynamicObject() {// arrangeconst string json = "{\"a\":1,\"b\":\"1\"}";dynamic message = null;_messageBus.Setup(m => m.Send(It.IsAny<TypedDynamicJson<long>>())).Callback<object>(o => message = o);// act_service.SendRawJson<TypedDynamicJson<long>>(json);// assertAssert.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# 中,動態和靜態編程之間并沒有對立,這兩種類型系統共同協作,以創造性的方式解決動態問題。
相關鏈接:
https://www.red-gate.com/simple-talk/dotnet/c-programming/working-with-the-dynamic-type-in-c/?Working with the Dynamic Type in C#???
作者 :Camilo Reyes
譯者 :技術譯民?
出品 :技術譯站(https://ITTranslator.cn/)
END
總結
- 上一篇: IdentityServer4(八)使用
- 下一篇: 如何在 C# 8 中使用 Index 和