翻译自 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 的基本构建块,这种优雅的类型系统在 .NET 4.0 和动态类型以前就有了。我建议您在使用 C# 中的类型时,在脑海中记住这张图。那么,DLR 是如何适应这张图的呢?
动态语言运行时(Dynamic Language Runtime, DLR)是处理动态对象的一种便捷方法。好比,假设您有 XML 或 JSON 格式的数据,其中的成员事先并不知道。DLR 容许您使用天然代码来处理对象和访问成员。
对于 C#,这使您能够处理在编译时不知道其类型的库。动态类型消除了天然 API 代码中的万能字符串。这就开启了像 IronPython 同样位于 CLR 之上的动态语言。
能够将 DLR 视为支持三项主要服务:
a + b
之类的操做,并存储 a
和 b
的特征。当执行动态操做时,DLR 将检索先前操做中可用的信息。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 HTTP 请求时,假设要在 C# 中使用此任意的 JSON。要解决这个问题,请将此 JSON 序列化为 C# 动态类型。
我将使用 Newtonsoft 序列化库,您能够经过 NuGet 添加此依赖项,例如:
dotnet add package Newtonsoft.Json –-version 11.0.2
您可使用这个序列化程序来处理 ExpandoObject
和 DynamicObject
。探索每种动态类型给动态编程带来了什么。
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
容许您经过重写选择实现哪些动态操做。这比实现 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
,将会得到第二个属性。
到目前为止,已经涉及了至关多的领域; 如下是一些亮点:
System.Object
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() { // 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 语句是代码示例的一部分:
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# ↩︎