您自己的JSON转换器或有关ExpressionTrees的更多内容



序列化和反序列化是现代开发人员视为琐碎的典型操作。 我们与数据库进行通信,生成HTTP请求,通过REST API接收数据,甚至常常不考虑其工作方式。 今天,我建议编写用于JSON的序列化器和反序列化器,以了解底层情况。

免责声明


上次一样,我会注意到:我们将编写一个原始的序列化器,也许有人会说是一辆自行车。 如果您需要交钥匙解决方案,请使用Json.NET 。 这些家伙发布了一款出色的产品,该产品具有高度可定制性,可以做很多事情,并且已经解决了使用JSON时出现的问题 。 使用您自己的解决方案确实很酷,但前提是您需要最高的性能,特殊的自定义设置或以我喜欢的方式喜欢自行车。

学科领域


从JSON转换为对象表示的服务至少包含两个子系统。 Deserializer是一个子系统,它将有效的JSON (文本)转换为程序内部的对象表示形式。 反序列化涉及令牌化,即将JSON解析为逻辑元素。 序列化器是一个执行相反任务的子系统:将数据的对象表示形式转换为JSON。

消费者最常看到以下界面。 我故意简化它以突出最常用的主要方法。

public interface IJsonConverter { T Deserialize<T>(string json); string Serialize(object source); } 

“幕后”反序列化包括令牌化(解析JSON文本)和构建一些原语,这些原语使以后创建对象表示更加容易。 出于培训目的,我们将跳过中间基元的构造(例如,Json.NET的JObject,JProperty),我们将立即将数据写入对象。 这是一个缺点,因为它减少了自定义的选项,但是不可能在一篇文章的框架内创建整个库。

代币化


让我提醒您,标记化或词法分析的过程是对文本的解析,目的是获得其中包含的数据的不同,更严格的表示形式。 通常,此表示形式称为令牌或令牌。 出于解析JSON的目的,我们必须突出显示属性,它们的值,结构开始和结束的符号-即可以在代码中表示为JsonToken的令牌。

JsonToken是一个包含值(文本)以及令牌类型的结构。 JSON是一种严格的表示法,因此所有类型的令牌都可以简化为下一个enum 。 当然,将令牌在输入数据(行和列)中的坐标添加到令牌中会很好,但是调试超出了实现的范围,这意味着JsonToken不包含此数据。

因此,将文本解析为标记的最简单方法是顺序读取每个字符并将其与模式进行比较。 我们需要了解该符号的含义。 关键字(true,false,null)可能以该字符开头,这可能是行的开头(引号),或者该字符本身是标记([,],{,})。 总体思路如下:

 var tokens = new List<JsonToken>(); for (int i = 0; i < json.Length; i++) { char ch = json[i]; switch (ch) { case '[': tokens.Add(new JsonToken(JsonTokenType.ArrayStart)); break; case ']': tokens.Add(new JsonToken(JsonTokenType.ArrayEnd)); break; case '"': string stringValue = ReadString(); tokens.Add(new JsonToken(JsonTokenType.String, stringValue); break; ... } } 

查看代码,您似乎可以读取并立即对读取的数据进行操作。 它们不需要存储,必须立即发送给消费者。 因此,某个IEnumerator会乞求,它将把文本解析成碎片。 首先,这将减少分配,因为我们不需要存储中间结果(令牌的数组)。 其次,我们将提高工作速度-是的,在我们的示例中,输入是字符串,但是在实际情况下,它将被Stream (来自文件或网络) 替换 ,我们按顺序读取。

我已经准备了JsonTokenizer代码,可以在这里找到 。 想法是一样的-分词器依次沿线移动,试图确定符号或其顺序所指的是什么。 如果事实证明这是可以理解的,那么我们将创建一个令牌并将控制权转移给消费者。 如果尚不清楚,请继续阅读。

准备反序列化对象


通常,从JSON转换数据的请求是对Deserialize通用方法的调用,其中TOut是JSON令牌应映射到的数据类型。 Type在哪里:是时候应用ReflectionExpressionTrees了 。 我在上一篇有关如何制作AutoMapper的文章中描述了使用ExpressionTrees的基础知识,以及为什么编译的表达式比“裸”反射更好。 如果您对Expression.Labmda.Compile()一无所知,建议您阅读一下。 在我看来,映射器的示例非常容易理解。

因此,创建对象反序列化器的计划是基于以下知识:我们可以随时从TOut类型获取属性类型,即PropertyInfo集合。 同时,属性类型受JSON表示法的限制:数字,字符串,数组和对象。 即使我们没有忘记null,但这也不是乍一看。 并且如果对于每种基本类型,我们将被迫创建一个单独的反序列化器,那么对于数组和对象,我们可以创建泛型类。 如果稍作考虑,可以将所有序列化器/解串器(或转换器 )简化为以下接口:

 public interface IJsonConverter<T> { T Deserialize(JsonTokenizer tokenizer); void Serialize(T value, StringBuilder builder); } 

基本类型的强类型转换器的代码尽可能地简单:我们从令牌生成器中提取当前的JsonToken,并通过解析将其转换为值。 例如,float.Parse(currentToken.Value)。 看看BoolConverterFloatConverter-没什么复杂的。 接下来,是否需要解串器用于bool? 或float?,也可以添加。

数组反序列化


用于从JSON转换数组的通用类代码也相对简单。 它由我们可以提取Type.GetElementType()的元素类型进行参数化。 确定类型是否为数组也很简单: Type.IsArray 。 数组反序列化可以说成是tokenizer.MoveNext(),直到到达ArrayEnd类型的令牌为止。 数组元素的反序列化是数组元素类型的反序列化,因此,在创建ArrayConverter时,将元素反序列化器传递给它。

有时,通用实现的实例化会遇到困难,因此我将立即告诉您如何实现。 反射允许您实时创建通用类型,这意味着我们可以将创建的类型用作Activator.CreateInstance的参数。 利用这一点:

 Type elementType = arrayType.GetElementType(); Type converterType = typeof(ArrayConverter<>).MakeGenericType(elementType); var converterInstance = Activator.CreateInstance(converterType, object[] args); 

完成创建对象反序列化器的准备工作之后,您可以将与反序列化器的创建和存储相关的所有基础结构代码放在JConverter的外观中。 他将负责所有JSON序列化和反序列化操作,并可以作为服务提供给消费者。

对象反序列化


让我提醒您,您可以像这样获得T类型的所有属性:typeof(T).GetProperties()。 对于每个属性,您可以提取PropertyInfo.PropertyType ,这将使我们有机会创建类型化的IJsonConverter,以对特定类型的数据进行序列化和反序列化。 如果属性的类型是数组,则我们将实例化ArrayConverter或在现有的数组中找到一个合适的数组。 如果属性类型是原始类型,则已经在JConverter构造函数中为其创建了反序列化器(转换器)。

可以在通用类ObjectConverter中查看生成的代码。 在其构造函数中创建一个激活器,从专门准备的字典中提取属性,并为每个属性创建反序列化方法-动作<TObject,JsonTokenizer>。 首先,需要立即将IJsonConverter与所需的属性相关联;其次,需要避免在提取和编写基本类型时使用装箱。 每种反序列化方法都知道将记录传出对象的哪个属性,对值的反序列化器进行严格键入,并以所需的形式准确返回值。

IJsonConverter与属性的绑定如下:

 Type converterType = propertyValueConverter.GetType(); ConstantExpression Expression.Constant(propertyValueConverter, converterType); MethodInfo deserializeMethod = converterType.GetMethod("Deserialize"); var value = Expression.Call(converter, deserializeMethod, tokenizer); 

直接在表达式中创建Expression.Constant常量,该常量存储对反序列化器实例的对属性值的引用。 这不是我们在“常规C#”中编写的常量,因为它可以存储引用类型。 接下来,从反序列化器类型检索Deserialize方法,该方法返回所需类型的值,然后将其称为-Expression.Call 。 因此,我们得到了一种确切知道该在哪里写什么的方法。 剩下的就是将其放入字典中,并在令牌生成器中使用所需名称的“属性”类型的令牌“到来”时调用它。 另一个优点是,所有操作都非常快。

多快


就像一开始就提到的那样,在几种情况下写自行车是有意义的:如果这是试图了解该技术的工作方式,或者您需要获得一些特殊的结果。 例如速度。 您可以确保反序列化程序确实与准备好的测试反序列化(我使用AutoFixture获取测试数据)。 顺便说一句,您可能注意到我也写了对象的序列化。 但是,由于本文的篇幅很大,因此我将不对其进行描述,而只是给出基准。 是的,就像上一篇文章一样,我使用BenchmarkDotNet库编写了基准测试

当然,我反序列化速度与Newtonsoft(Json.NET) 进行了比较 ,后者是使用JSON的最常见和推荐的解决方案。 而且,在他们的网站上写着:比DataContractJsonSerializer快50%,比JavaScriptSerializer快250%。 简而言之,我想知道我的代码会损失多少。 结果令我惊讶:请注意,数据分配几乎减少了三倍,反序列化速度快了两倍。
方法均值失误标准差比例已分配
牛顿软件75.39毫秒0.3027毫​​秒0.2364毫秒1.0035.47兆字节
韦洛31.78毫秒0.1135毫秒0.1062毫秒0.4212.36兆字节

数据序列化期间速度和分配的比较产生了更加有趣的结果。 事实证明,自行车序列化器的分配减少了将近五倍,工作速度提高了近三倍。 如果速度真的困扰我(真的很多),那将是明显的成功。
方法均值失误标准差比例已分配
牛顿软件54.83毫秒0.5582毫秒0.5222毫秒1.0025.44兆字节
韦洛20.66毫秒0.0484毫秒0.0429毫秒0.385.93兆字节

是的,在测量速度时,我没有使用Json.NET网站上发布的提高生产率提示 。 我开箱即用,即根据最常用的场景进行测量:JsonConvert.DeserializeObject。 可能还有其他提高性能的方法,但我不知道。

结论


尽管序列化和反序列化的速度相对较高,但我不建议您放弃Json.NET来支持我自己的解决方案。 速度的增益以毫秒为单位进行计算,它们很容易被网络延迟,磁盘或代码“淹没”,而网络延迟,磁盘或代码位于应用序列化的位置上方。 要支持这样的专有解决方案是一件令人头疼的事情,在那儿只能允许精通该主题的开发人员。

此类自行车的范围是经过全面设计以实现高性能的应用程序,或者是您了解该技术是如何工作的宠物项目。 希望我在所有这些方面都对您有所帮助。

Source: https://habr.com/ru/post/zh-CN464525/


All Articles