C# 中的動態類型

翻譯自 Camilo Reyes 2018年10月15日的文章 《Working with the Dynamic Type in C#》 [1]php

.NET 4 中引入了動態類型。動態對象使您能夠處理諸如 JSON 文檔之類的結構,這些結構的組成可能要到運行時才能知道。在本文中,Camilo Reyes 解釋瞭如何使用動態類型。程序員

.NET 4.0 中引入的 dynamic 關鍵字爲 C# 編程帶來了一個範式轉變。對於 C# 程序員來講,強類型系統之上的動態行爲可能會讓人感到不適 —— 當您在編譯過程當中失去類型安全性時,這彷佛是一種倒退。編程

動態編程可能使您面臨運行時錯誤。聲明一個在執行過程當中會發生變化的動態變量是可怕的,當開發人員對數據作出錯誤的假設時,代碼質量就會受到影響。json

對 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 語句直到本文結束,以保持對代碼示例的專一。而後,我再介紹每一個命名空間及其做用。這樣我就沒必要重複說過的話,並提供了一個回顧全部類型的機會。ide

上面的代碼在控制檯中的運算結果爲 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’s type system

值類型和引用類型都是 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 之類的操做,並存儲 ab 的特徵。當執行動態操做時,DLR 將檢索先前操做中可用的信息。
  • 動態對象互操做性是可用於訪問 DLR 的 C# 類型。這些類型包括 DynamicObjectExpandoObject。可用的類型還有不少,可是在處理動態類型時請注意這兩種類型。

要了解 DLR 和 CLR 是如何結合在一塊兒的,請看下圖:

how the DLR and CLR fit together

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)));

ExpandoObjectDynamicObject 在命令行中輸出的值都是 True。能夠將這兩個類視爲使用動態類型的基本構建塊,它們清楚地描繪了兩個運行時是如何結合在一塊兒的。

一個 JSON 序列化程序

動態類型解決的一個問題是,當您有一個不知道其成員的 JSON HTTP 請求時,假設要在 C# 中使用此任意的 JSON。要解決這個問題,請將此 JSON 序列化爲 C# 動態類型。

我將使用 Newtonsoft 序列化庫,您能夠經過 NuGet 添加此依賴項,例如:

dotnet add package Newtonsoft.Json –-version 11.0.2

您可使用這個序列化程序來處理 ExpandoObjectDynamicObject。探索每種動態類型給動態編程帶來了什麼。

ExpandoObject 動態類型

ExpandoObject 是一種方便的類型,容許設置和檢索動態成員。它實現了 IDynamicMetaObjectProvider,該接口容許在 DLR 中的語言之間共享實例。由於它實現了 IDictionaryIEnumerable,因此它也能夠處理 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

它將會在控制檯打印 1long。請注意,儘管它是一個動態 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。請確保使用 stringobject 做爲鍵和值的類型。不然,將在轉換的過程當中拋出 RuntimeBinderException 異常。

DynamicObject 動態類型

DynamicObject 提供對動態類型的精確控制。您能夠繼承該類型並重寫動態行爲。例如,您能夠定義如何設置和獲取類型中的動態成員。DynamicObject 容許您經過重寫選擇實現哪些動態操做。這比實現 IDynamicMetaObjectProvider 的語言實現方式更易訪問。它是一個抽象類,須要繼承它而不是實例化它。該類有 14 個虛方法,它們定義了類型的動態操做,每一個虛方法都容許重寫以指定動態行爲。

假設您想要精確控制動態 JSON 中的內容。儘管事先不知道其屬性,您卻可使用 DynamicObject 來控制類型。

讓咱們來重寫三個方法,TryGetMemberTrySetMemberGetDynamicMemberNames

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 經過虛方法對動態類型進行精確控制

看一下在控制檯的結果截圖:

dynamic type results

單元測試

對於單元測試,我將使用 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()
{
    // arrange
    const 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);

    // assert
    Assert.NotNull(message);
    Assert.Equal(1, message.a);
}

使用 DynamicObject 類型進行測試,重用您以前看到的 TypedDynamicJson

[Fact]
public void SendsWithDynamicObject()
{
    // arrange
    const 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);

    // 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# 中,動態和靜態編程之間並無對立,這兩種類型系統共同協做,以創造性的方式解決動態問題。


🤞 源碼下載:


做者 : Camilo Reyes
譯者 : 技術譯民
出品 : 技術譯站
連接 : 英文原文


  1. https://www.red-gate.com/simple-talk/dotnet/c-programming/working-with-the-dynamic-type-in-c/ Working with the Dynamic Type in C# ↩︎

相關文章
相關標籤/搜索