找回密码
 立即注册
首页 业界区 业界 .NET 高级开发 | .NET 中的序列化和反序列化 ...

.NET 高级开发 | .NET 中的序列化和反序列化

篁瞑普 5 小时前
目录

  • .NET 中的序列化和反序列化

    • 编写类型转换器

      • 枚举转换器

        • .NET 是如何序列化枚举
        • 实现枚举转换器
        • 如何使用类型转换器
        • 使用官方的转换器

      • 字符串和值类型转换
      • 时间类型转换器

    • 从底层处理 JSON

      • Utf8JsonReader
      • Utf8JsonReader 和 JsonNode 解析 JSON 性能测试



.NET 中的序列化和反序列化

在 ASP.NET Core 应用中,框架会屏蔽了很多实现序列化和反序列化的细节,我们只需要定义参数模型,ASP.NET Core 会自动将 http 请求的 Body 反序列化为模型对象。但是日常开发中我们会对序列化和反序列化做许多定制配置,比如忽略值为 null 的字段、时间格式处理、忽略大小写、字段类型转换等各种情况。因此笔者单独使用一章讲解序列化框架的使用以及如何进行定制,深入了解 .NET 中序列化和反序列化机制。
System.Text.Json 是 .NET 框架自带的序列化框架,简单易用并且性能也很出色,使用  System.Text.Json 反序列化字符串为对象是很简单的,示例如下:
  1. // 自定义序列化配置
  2. static JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
  3. {
  4.         PropertyNameCaseInsensitive = true,
  5.         WriteIndented = true
  6. };
  7. public static void Main()
  8. {
  9.         const string json =
  10.                 """
  11.             {
  12.                 "Name": "工良"
  13.             }
  14.             """;
  15.         var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
  16. }
  17. public class Model
  18. {
  19.         public string Name { get; set; }
  20. }
复制代码
JsonSerializerOptions 的属性定义了如何序列化和反序列化,其常用属性如下:
属性类型说明AllowTrailingCommasbool忽略 JSON 中多余的逗号ConvertersIList转换器列表DefaultBufferSizeint默认缓冲区大小DefaultIgnoreConditionJsonIgnoreCondition当字段/属性的值为默认值时,是否忽略DictionaryKeyPolicyJsonNamingPolicy字典 Key 重命名规则,如首字母生成小写IgnoreNullValuesbool忽略 JSON 中值为 null 的字段/属性IgnoreReadOnlyFieldsbool忽略只读字段IgnoreReadOnlyPropertiesbool忽略只读属性IncludeFieldsbool是否处理字段,默认只处理属性MaxDepthint最大嵌套深度,默认最大深度为 64NumberHandlingJsonNumberHandling如何处理数字类型PropertyNameCaseInsensitivebool忽略大小写PropertyNamingPolicyJsonNamingPolicy重命名规则,如首字母生成小写ReadCommentHandlingJsonCommentHandling处理注释WriteIndentedbool序列化时格式化 JSON,如换行、空格、缩进接下来笔者将会列举一些常用的定制场景和编码方法,为了避免混肴,在本章中所指的 “字段” 或 “属性”,等同于类型的“字段和属性”。
编写类型转换器

类型转换器的作用是当 json 对象字段和模型类字段类型不一致时,可以自动转换对应的类型,下面笔者介绍常用的几种类型转换器。
枚举转换器

.NET 是如何序列化枚举

编写 WebAPI 的模型类时常常会用到枚举,枚举类型默认会以数值的形式输出到 json 中。
C# 代码示例如下:
  1. // 枚举
  2. public enum NetworkType
  3.     {
  4.         Unknown = 0,
  5.         IPV4 = 1,
  6.         IPV6 = 2
  7.     }
  8. // 类型
  9. public class Model
  10.     {
  11.         public string Name { get; set; }
  12.         public NetworkType Netwotk1 { get; set; }
  13.         public NetworkType? Netwotk2 { get; set; }
  14.     }
  15. var model = new Model
  16.                 {
  17.                         Name = "工良",
  18.                         Netwotk1 = NetworkType.IPV4,
  19.                         Netwotk2 = NetworkType.IPV6
  20.                 }
复制代码
当我们序列化对象时,会得到这样的结果:
  1. {
  2.         "Name": "工良",
  3.         "Netwotk1": 1,
  4.         "Netwotk2": 2
  5. }
复制代码
但是这样会在阅读上带来难题,数字记忆比较困难,并且后期需要扩展枚举字段时,可能会导致对应数值的变化,那么已经对接的代码都需要修改,如果枚举涉及的范围比较广,那么要做出修改就会变得十分困难。
比如说突然出现了一个 IPV5,那么我们除了改代码,可能还要修改以及对接的其它应用。
  1. public enum NetworkType
  2. {
  3.         Unknown = 0,
  4.         IPV4 = 1,
  5.         IPV5 = 2,
  6.         IPV6 = 3
  7. }
复制代码
因此,我们需要一种方法,能够让枚举序列化后使用对应的名称表示,以及能够使用这个字符串转化为对应的枚举类型,后期需要扩展或中间插入时,对以前的代码和数据库完全没有影响。
比如反序列化时,得到的是这样的 json:
  1. "Netwotk1": "IPV4"
  2. "Netwotk2": "IPV6"
复制代码
即使后来中间插入一个 IPV5,生成新的字符串即可,完全不需要重新排序枚举值。
  1. "Netwotk1": "IPV4"
  2. "Netwotk2": "IPV6""Netwotk3": "IPV5"
复制代码
在 C# 模型类中使用枚举而 json 中使用字符串,要实现这种形式的枚举转换,有两种方法。

  • 在模型类的枚举字段或属性上放置一个特性注解,序列化反序列化时从这个特性注解中获取转换器。
  • 使用 JsonSerializerOptions 添加转换器,在反序列化或序列化时传递自定义配置。
无论哪种方法,我们都需要实现一个转换器,能够将模型类中的枚举使用对应的名称序列化到 json 中。在实现自定义转换器示例之前,我们来了解相关的一些知识。
自定义转换器需要继承 JsonConverter 或 JsonConverter,当反序列化 json 的字段或序列化对象的字段属性时,框架会自动调用转换器。
以 JsonConverter 为例,里面有好几个抽象接口,我们一般只需要实现转换器的两个抽象接口即可:
  1. // json 值 => 对象字段
  2. public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
  3. // 对象字段 => json 值
  4. public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
复制代码
不过我们一定要注意 C# 中的可空类型,比如 NetworkType 和 NetworkType? 实际上是两种类型,可空类型本质是使用 Nullable 包装的类型。
Nullable 的定义如下:
  1. public struct Nullable<T> where T : struct
复制代码
另外 Nullable 实现了和 T 类型的隐式和显式转换重载,所以我们在使用可空类型时,可能不太容易感受出 Nullable 和 T 区别,比如可以在使用可空类型 T? 时,直接将 Nullable 与 T 类型隐式和显式转换,如:
  1. Nullable<int> value = 100
复制代码
但是在使用反射时,由于 T 和 T? 是两种不同的类型,因此我们编写转换器时必须留意到这种区别,否则会出现错误。
实现枚举转换器

本节示例代码在 Demo4.Console 中。
编写一个枚举字符串转换器代码示例如下:
  1. public class EnumStringConverter<TEnum> : JsonConverter<TEnum>
  2. {
  3.         private readonly bool _isNullable;
  4.         public EnumStringConverter(bool isNullType)
  5.         {
  6.                 _isNullable = isNullType;
  7.         }
  8.    
  9.     // 判断当前类型是否可以使用该转换器转换
  10.         public override bool CanConvert(Type objectType) => EnumStringConverterFactory.IsEnum(objectType);
  11.     // 从 json 中读取数据
  12.         // JSON => 值
  13.         // typeToConvert: 模型类属性/字段的类型
  14.         public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  15.         {
  16.         // 读取 json
  17.                 var value = reader.GetString();
  18.                 if (value == null)
  19.                 {
  20.                         if (_isNullable) return default;
  21.                         throw new ArgumentNullException(nameof(value));
  22.                 }
  23.                 // 是否为可空类型
  24.                 var sourceType = EnumStringConverterFactory.GetSourceType(typeof(TEnum));
  25.                 if (Enum.TryParse(sourceType, value.ToString(), out var result))
  26.                 {
  27.                         return (TEnum)result!;
  28.                 }
  29.                 throw new InvalidOperationException($"{value} 值不在枚举 {typeof(TEnum).Name} 范围中");
  30.         }
  31.         // 值 => JSON
  32.         public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)
  33.         {
  34.                 if (value == null) writer.WriteNullValue();
  35.                 else writer.WriteStringValue(Enum.GetName(value.GetType(), value));
  36.         }
  37. }
复制代码
由于 Utf8JsonReader 日常出行的机会不多,因此读者可能不太了解,在本章的末尾,笔者会简单介绍。
一般情况下,我们不会直接使用 EnumStringConverter ,为了能够适应所有枚举类型,还需要编写一个枚举转换工厂,通过工厂模式判断输入类型之后,再创建对应的转换器。
  1. public class EnumStringConverterFactory : JsonConverterFactory
  2. {
  3.         // 获取需要转换的类型
  4.         public static bool IsEnum(Type objectType)
  5.         {
  6.                 if (objectType.IsEnum) return true;
  7.                 var sourceType = Nullable.GetUnderlyingType(objectType);
  8.                 return sourceType is not null && sourceType.IsEnum;
  9.         }
  10.    
  11.     // 如果类型是可空类型,则获取原类型
  12.         public static Type GetSourceType(Type typeToConvert)
  13.         {
  14.                 if (typeToConvert.IsEnum) return typeToConvert;
  15.                 return Nullable.GetUnderlyingType(typeToConvert);
  16.         }
  17.     // 判断该类型是否属于枚举
  18.         public override bool CanConvert(Type typeToConvert) => IsEnum(typeToConvert);
  19.    
  20.     // 为该字段创建一个对应的类型转换器
  21.         public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
  22.         {
  23.                 var sourceType = GetSourceType(typeToConvert);
  24.                 var converter = typeof(EnumStringConverter<>).MakeGenericType(typeToConvert);
  25.                 return (JsonConverter)Activator.CreateInstance(converter, new object[] { sourceType != typeToConvert });
  26.         }
  27. }
复制代码
当 System.Text.Json 处理一个字段时,会调用 EnumStringConverterFactory 的  CanConvert 方法,如果返回 true,则会调用 EnumStringConverterFactory 的 CreateConverter 方法创转换器,最后调用转换器处理字段,这样一来,我们可以通过泛型类 EnumStringConverter 处理各种枚举。
然后定义特性注解,能够将模型类的属性字段绑定到一个转换器上。
  1.     [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
  2.     public class EnumConverterAttribute : JsonConverterAttribute
  3.     {
  4.         public override JsonConverter CreateConverter(Type typeToConvert)
  5.         {
  6.             return new EnumStringConverterFactory();
  7.         }
  8.     }
复制代码
如何使用类型转换器

使用自定义类型转换器有三种方法。
方法一,在枚举字段中使用自定义特性:
  1.     public class Model
  2.     {
  3.         public string Name { get; set; }
  4.         [EnumConverter]
  5.         public NetworkType Netwotk1 { get; set; }
  6.         
  7.         [EnumConverter]
  8.         public NetworkType? Netwotk2 { get; set; }
  9.     }
复制代码
方法二,使用 JsonConverter 特性。
  1. public class Model
  2. {
  3.         public string Name { get; set; }
  4.    
  5.         [JsonConverter(typeof(EnumConverter))]   
  6.         public NetworkType Netwotk1 { get; set; }
  7.    
  8.         [JsonConverter(typeof(EnumConverter))]
  9.         public NetworkType? Netwotk2 { get; set; }
  10. }
复制代码
方法三,在配置中添加转换器。
  1.                 jsonSerializerOptions.Converters.Add(new EnumStringConverterFactory());
  2.                 var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
复制代码
在模型类中使用转换器特性之后,我们可以通过字符串反序列化为枚举类型:
  1.         const string json =
  2.             """
  3.             {
  4.                 "Name": "工良",
  5.                 "Netwotk1": "IPV4",
  6.                 "Netwotk2": "IPV6"
  7.             }
  8.             """;
  9.         var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
复制代码
使用官方的转换器

System.Text.Json 中已经实现了很多转换器,可以在官方源码的 System/Text/Json/Serialization/Converters/Value 下找到所有自带的转换器,其中官方实现的枚举字符串转换器叫 JsonStringEnumConverter ,使用方法跟我们的自定义转换器一致。
这里我们可以使用官方的 JsonStringEnumConverter 转换器替代  EnumStringConverter:
  1.     public class Model
  2.     {
  3.         public string Name { get; set; }
  4.         public NetworkType Netwotk1 { get; set; }
  5.         public NetworkType? Netwotk2 { get; set; }
  6.     }
复制代码
  1.         JsonSerializerOptions jsonSerializerOptions = new();        jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());        const string json =
  2.             """
  3.             {
  4.                 "Name": "工良",
  5.                 "Netwotk1": "IPV4",
  6.                 "Netwotk2": "IPV6"
  7.             }
  8.             """;
  9.         var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
复制代码
字符串和值类型转换

很多情况下,会在模型类下使用数值类型,序列化到 json 时使用字符串。比如对应浮点型的数值,为了保证其准确性,我们会使用字符串形式保存到 json 中,这样可以避免传输时对浮点型处理而丢失其准确性。又比如前端处理超过 16 位数值时,数字会丢失精确度,16位数字存储毫秒格式的时间戳足够了,很多时候我们会使用分布式 id,雪花算法有很多种,其生成的 id 往往会超过 16 位。
JS 中处理超过 16 位数字时,会出现很精确度丢失的问题:
  1. console.log(11111111111111111);
  2. 输出: 11111111111111112
  3. console.log(111111111111111111);
  4. 输出: 111111111111111100
复制代码
有个最简单的方法是在 JsonSerializerOptions 中将所有数值字段转换为字符串:
  1.         new JsonSerializerOptions
  2.                 {
  3.                         NumberHandling = JsonNumberHandling.AllowReadingFromString
  4.                 };
复制代码
但是这样会导致所有值类型字段序列化为 json 时变成字符串,如果只需要处理几个字段而不是处理所有字段,那就需要我们自己编写类型转换器了。
要实现字符串转数值,需要考虑很多种数值类型,如 byte、int、double、long 等,从值类型转换为字符串是很简单的,但是要实现一个字符串转任意类型值类型,那就很麻烦,这也是我们编写转换器的重点。
编写 json 字符串和模型类值类型转换器的代码示例如下:
  1. public class StringNumberConverter<T> : JsonConverter<T>
  2. {
  3.         private static readonly TypeCode typeCode = Type.GetTypeCode(typeof(T));
  4.     // 从 json 中读取字符串,转换为对应的值类型
  5.         public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  6.         {
  7.                 switch (reader.TokenType)
  8.                 {
  9.                         case JsonTokenType.Number:
  10.                                 if (typeCode == TypeCode.Int32)
  11.                                 {
  12.                                         if (reader.TryGetInt32(out var value))
  13.                                         {
  14.                                                 return Unsafe.As<int, T>(ref value);
  15.                                         }
  16.                                 }
  17.                                 if (typeCode == TypeCode.Int64)
  18.                                 {
  19.                                         if (reader.TryGetInt64(out var value))
  20.                                         {
  21.                                                 return Unsafe.As<long, T>(ref value);
  22.                                         }
  23.                                 }
  24.                                 if (typeCode == TypeCode.Decimal)
  25.                                 {
  26.                                         if (reader.TryGetDecimal(out var value))
  27.                                         {
  28.                                                 return Unsafe.As<decimal, T>(ref value);
  29.                                         }
  30.                                 }
  31.                                 if (typeCode == TypeCode.Double)
  32.                                 {
  33.                                         if (reader.TryGetDouble(out var value))
  34.                                         {
  35.                                                 return Unsafe.As<double, T>(ref value);
  36.                                         }
  37.                                 }
  38.                                 if (typeCode == TypeCode.Single)
  39.                                 {
  40.                                         if (reader.TryGetSingle(out var value))
  41.                                         {
  42.                                                 return Unsafe.As<float, T>(ref value);
  43.                                         }
  44.                                 }
  45.                                 if (typeCode == TypeCode.Byte)
  46.                                 {
  47.                                         if (reader.TryGetByte(out var value))
  48.                                         {
  49.                                                 return Unsafe.As<byte, T>(ref value);
  50.                                         }
  51.                                 }
  52.                                 if (typeCode == TypeCode.SByte)
  53.                                 {
  54.                                         if (reader.TryGetSByte(out var value))
  55.                                         {
  56.                                                 return Unsafe.As<sbyte, T>(ref value);
  57.                                         }
  58.                                 }
  59.                                 if (typeCode == TypeCode.Int16)
  60.                                 {
  61.                                         if (reader.TryGetInt16(out var value))
  62.                                         {
  63.                                                 return Unsafe.As<short, T>(ref value);
  64.                                         }
  65.                                 }
  66.                                 if (typeCode == TypeCode.UInt16)
  67.                                 {
  68.                                         if (reader.TryGetUInt16(out var value))
  69.                                         {
  70.                                                 return Unsafe.As<ushort, T>(ref value);
  71.                                         }
  72.                                 }
  73.                                 if (typeCode == TypeCode.UInt32)
  74.                                 {
  75.                                         if (reader.TryGetUInt32(out var value))
  76.                                         {
  77.                                                 return Unsafe.As<uint, T>(ref value);
  78.                                         }
  79.                                 }
  80.                                 if (typeCode == TypeCode.UInt64)
  81.                                 {
  82.                                         if (reader.TryGetUInt64(out var value))
  83.                                         {
  84.                                                 return Unsafe.As<ulong, T>(ref value);
  85.                                         }
  86.                                 }
  87.                                 break;
  88.                         case JsonTokenType.String:
  89.                                 IConvertible str = reader.GetString() ?? "";
  90.                                 return (T)str.ToType(typeof(T), null);
  91.                 }
  92.                 throw new NotSupportedException($"无法将{reader.TokenType}转换为{typeToConvert}");
  93.         }
  94.     // 将值类型转换为 json 字符串
  95.         public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
  96.         {
  97.                 switch (typeCode)
  98.                 {
  99.                         case TypeCode.Int32:
  100.                                 writer.WriteNumberValue(Unsafe.As<T, int>(ref value));
  101.                                 break;
  102.                         case TypeCode.UInt32:
  103.                                 writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
  104.                                 break;
  105.                         case TypeCode.Decimal:
  106.                                 writer.WriteNumberValue(Unsafe.As<T, decimal>(ref value));
  107.                                 break;
  108.                         case TypeCode.Double:
  109.                                 writer.WriteNumberValue(Unsafe.As<T, double>(ref value));
  110.                                 break;
  111.                         case TypeCode.Single:
  112.                                 writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
  113.                                 break;
  114.                         case TypeCode.UInt64:
  115.                                 writer.WriteNumberValue(Unsafe.As<T, ulong>(ref value));
  116.                                 break;
  117.                         case TypeCode.Int64:
  118.                                 writer.WriteNumberValue(Unsafe.As<T, long>(ref value));
  119.                                 break;
  120.                         case TypeCode.Int16:
  121.                                 writer.WriteNumberValue(Unsafe.As<T, short>(ref value));
  122.                                 break;
  123.                         case TypeCode.UInt16:
  124.                                 writer.WriteNumberValue(Unsafe.As<T, ushort>(ref value));
  125.                                 break;
  126.                         case TypeCode.Byte:
  127.                                 writer.WriteNumberValue(Unsafe.As<T, byte>(ref value));
  128.                                 break;
  129.                         case TypeCode.SByte:
  130.                                 writer.WriteNumberValue(Unsafe.As<T, sbyte>(ref value));
  131.                                 break;
  132.                         default:
  133.                                 throw new NotSupportedException($"不支持非数字类型{typeof(T)}");
  134.                 }
  135.         }
  136. }
复制代码
编写字符串转换为各种类型的值类型,主要有一个难点泛型转换,我们使用 reader.TryGetInt32() 读取 int 值之后,明明知道泛型 T 是 int,但是我们却不能直接返回 int ,我们必须要有一个手段可以将值转换为泛型 T。如果使用反射,会带来很大的性能消耗,还可能伴随着装箱拆箱,所以这里使用了 Unsafe.As ,其作用是将转换类型的指针,使得相关的值类型可以转换为泛型 T。
实现字符串和值类型转换器之后,接着实现转换工厂:
  1. public class JsonStringToNumberConverter : JsonConverterFactory
  2. {
  3.         public static JsonStringToNumberConverter Default { get; } = new JsonStringToNumberConverter();
  4.         public override bool CanConvert(Type typeToConvert)
  5.         {
  6.                 var typeCode = Type.GetTypeCode(typeToConvert);
  7.                 return typeCode == TypeCode.Int32 ||
  8.                         typeCode == TypeCode.Decimal ||
  9.                         typeCode == TypeCode.Double ||
  10.                         typeCode == TypeCode.Single ||
  11.                         typeCode == TypeCode.Int64 ||
  12.                         typeCode == TypeCode.Int16 ||
  13.                         typeCode == TypeCode.Byte ||
  14.                         typeCode == TypeCode.UInt32 ||
  15.                         typeCode == TypeCode.UInt64 ||
  16.                         typeCode == TypeCode.UInt16 ||
  17.                         typeCode == TypeCode.SByte;
  18.         }
  19.         public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
  20.         {
  21.                 var type = typeof(StringNumberConverter<>).MakeGenericType(typeToConvert);
  22.                 var converter = Activator.CreateInstance(type);
  23.                 if (converter == null)
  24.                 {
  25.                         throw new InvalidOperationException($"无法创建 {type.Name} 类型的转换器");
  26.                 }
  27.                 return (JsonConverter)converter;
  28.         }
  29. }
复制代码
时间类型转换器

json 中规定了标准的时间格式,部分常用时间格式如下:
  1. YYYY-MM-DDTHH:mm:ss.sssZ
  2. YYYY-MM-DDTHH:mm:ss.sss+HH:mm
  3. YYYY-MM-DDTHH:mm:ss.sss-HH:mm
复制代码
示例:
  1. 2023-08-15T20:20:00+08:00
复制代码
但是在项目开发中,我们很多使用需要使用定制的格式,如 2023-02-15 20:20:20 ,那么就需要自行编写转换器,以便能够正确序列化或反序列化时间字段。
在 C# 中有一个指定 DateTtime 如何解析字符串时间的接口,即 DateTime.ParseExact(String, String, IFormatProvider),为了能够适应各种字符串时间格式,我们可以利用该接口将字符串转换为时间。
编写 json 字符串时间与 DateTime 互转的代码示例如下:
  1. public class CustomDateTimeConverter : JsonConverter<DateTime>
  2. {
  3.         private readonly string _format;
  4.     // format 参数是时间的字符串格式
  5.         public CustomDateTimeConverter(string format)
  6.         {
  7.                 _format = format;
  8.         }
  9.         public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options)
  10.         {
  11.                 writer.WriteStringValue(date.ToString(_format));
  12.         }
  13.         public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  14.         {
  15.                 var value = reader.GetString() ?? throw new FormatException("当前字段格式错误");
  16.                 return DateTime.ParseExact(value, _format, null);
  17.         }
  18. }
复制代码
转换器中不需要判断 json 字符串时间的各种,而是在使用时指定格式在构造函数中注入。使用示例:
  1. jsonSerializerOptions.Converters.Add(new CustomDateTimeConverter("yyyy/MM/dd HH:mm:ss"));
复制代码
其实,使用默认的 json 时间格式是一个很好的习惯。据笔者经验,在项目中修改默认的 json 时间格式,在后期项目开发和对接中,很有可能出现序列化问题。如果某些地方需要更高精细度,如需要毫秒、使用转换为时间戳、第三方系统对接需要特殊格式等,可以在需要的模型类上使用特性标记对应的时间转换器格式,最好不要全局修改 json 时间格式。
从底层处理 JSON

在本节中,笔者将会介绍如何使用 Utf8JsonReader 高性能地解析 json 文件,然后编写对 Utf8JsonReader 的性能测试,通过相关的示例让读者掌握 Utf8JsonReader 的使用,以及如何对代码进行性能测试。
Utf8JsonReader

Utf8JsonReader 和 Utf8JsonWriter 是 C# 中读取写入 json 的高性能 API,通过 Utf8JsonReader 和 Utf8JsonWriter 我们可以逐步读取 json 或写入 json。
Utf8JsonReader 使用比较广泛,例如官方的 JsonConfigurationProvider 便是使用 Utf8JsonReader 逐步读取 json 文件,生成 key/value 结构,而在后面的章节中,笔者也会介绍如何利用 Utf8JsonReader 实现 i18n 多语言的配置。由于 Utf8JsonReader 的使用最广泛,而 Utf8JsonWriter 并不常见,所以笔者只介绍 Utf8JsonReader  的使用方法。
Utf8JsonReader 和 Utf8JsonWriter 都是结构体,其定义如下:
  1. public ref struct Utf8JsonReader
  2. public ref struct Utf8JsonWriter
复制代码
由于其是 ref 结构体,因此使用上有较多限制,例如不能在异步中使用,不能作为类型参数在数组、 List、字典等中使用,只能被放到 ref struct 类型中当作字段或属性,或在函数参数中使用。使用 Utf8JsonReader  读取 json 时,开发者需要自行处理闭合括号 {}、[] 等,也需要自行判断处理 json 类型,因此读取过程也稍为复杂 。
下面,笔者来设定一个场景,就是使用 Utf8JsonReader 来实现读取 json 文件,将读取到的字段全都存到字典中,如果有多层结构,则使用 : 拼接层级,生成 IConfiguration 中的能够直接读取的 key/value 格式。
比如:
  1. // json
  2. {
  3.         "A": {
  4.                 "B": "test"
  5.         }
  6. }
  7. // C#
  8. new Dictionary<string, string>()
  9. {
  10.         {"A:B","test" }
  11. };
复制代码
新建一个静态类 ReadJsonHelper,在这个类型中编写解析 json 的代码。
  1. public static class ReadJsonHelper
  2. {
  3. }
复制代码
首先是读取字段值的代码,当从 json 读取字段时,如果字段不是对象或数组类型,则直接读取其值即可。
  1. // 读取字段值
  2. private static object? ReadObject(ref Utf8JsonReader reader)
  3. {
  4.         switch (reader.TokenType)
  5.         {
  6.                 case JsonTokenType.Null or JsonTokenType.None:
  7.                         return null;
  8.                 case JsonTokenType.False:
  9.                         return reader.GetBoolean();
  10.                 case JsonTokenType.True:
  11.                         return reader.GetBoolean();
  12.                 case JsonTokenType.Number:
  13.                         return reader.GetDouble();
  14.                 case JsonTokenType.String:
  15.                         return reader.GetString() ?? "";
  16.                 default: return null;
  17.         }
  18. }
复制代码
读取 json 字段时,我们会碰到复杂的嵌套结构,因此需要判断当前读取的是对象还是数组,而且两者可以相互嵌套,这就增加了我们的解析难度。
比如:
  1. {
  2.         ... ...
  3. }
  4. [... ...]
  5. [{...}, {...} ...]
复制代码
第一步是判断一个 json 的根结构是 {} 还是 [],然后逐步解析。
  1. // 解析 json 对象
  2. private static void BuildJsonField(ref Utf8JsonReader reader,
  3.                                    Dictionary<string, object> map,
  4.                                    string? baseKey)
  5. {
  6.         while (reader.Read())
  7.         {
  8.                 // 顶级数组 "[123,123]"
  9.                 if (reader.TokenType is JsonTokenType.StartArray)
  10.                 {
  11.                         ParseArray(ref reader, map, baseKey);
  12.                 }
  13.                 // 碰到 } 符号
  14.                 else if (reader.TokenType is JsonTokenType.EndObject) break;
  15.                 // 碰到字段
  16.                 else if (reader.TokenType is JsonTokenType.PropertyName)
  17.                 {
  18.                         var key = reader.GetString()!;
  19.                         var newkey = baseKey is null ? key : $"{baseKey}:{key}";
  20.                         // 判断字段是否为对象
  21.                         reader.Read();
  22.                         if (reader.TokenType is JsonTokenType.StartArray)
  23.                         {
  24.                                 ParseArray(ref reader, map, newkey);
  25.                         }
  26.                         else if (reader.TokenType is JsonTokenType.StartObject)
  27.                         {
  28.                                 BuildJsonField(ref reader, map, newkey);
  29.                         }
  30.                         else
  31.                         {
  32.                                 map[newkey] = ReadObject(ref reader);
  33.                         }
  34.                 }
  35.         }
  36. }
复制代码
json 数组有很多种情况,json 数组的元素可以是任意类型,因此处理起来稍微麻烦,所以针对数组类型,我们还应该支持解析元素,使用序号来访问对应位置的元素。
解析数组:
  1. // 解析数组
  2. private static void ParseArray(ref Utf8JsonReader reader, Dictionary<string, object> map, string? baseKey)
  3. {
  4.         int i = 0;
  5.         while (reader.Read())
  6.         {
  7.                 if (reader.TokenType is JsonTokenType.EndArray) break;
  8.                 var newkey = baseKey is null ? $"[{i}]" : $"{baseKey}[{i}]";
  9.                 i++;
  10.                 switch (reader.TokenType)
  11.                 {
  12.                         // [...,null,...]
  13.                         case JsonTokenType.Null:
  14.                                 map[newkey] = null;
  15.                                 break;
  16.                         // [...,123.666,...]
  17.                         case JsonTokenType.Number:
  18.                                 map[newkey] = reader.GetDouble();
  19.                                 break;
  20.                         // [...,"123",...]
  21.                         case JsonTokenType.String:
  22.                                 map[newkey] = reader.GetString();
  23.                                 break;
  24.                         // [...,true,...]
  25.                         case JsonTokenType.True:
  26.                                 map[newkey] = reader.GetBoolean();
  27.                                 break;
  28.                         case JsonTokenType.False:
  29.                                 map[newkey] = reader.GetBoolean();
  30.                                 break;
  31.                         // [...,{...},...]
  32.                         case JsonTokenType.StartObject:
  33.                                 BuildJsonField(ref reader, map, newkey);
  34.                                 break;
  35.                         // [...,[],...]
  36.                         case JsonTokenType.StartArray:
  37.                                 ParseArray(ref reader, map, newkey);
  38.                                 break;
  39.                         default:
  40.                                 map[newkey] = JsonValueKind.Null;
  41.                                 break;
  42.                 }
  43.         }
  44. }
复制代码
最后,我们编写一个解析 json 的入口,通过用户传递的 json 文件,解析出字典。
  1. public static Dictionary<string, object> Read(ReadOnlySequence<byte> sequence,
  2.                                               JsonReaderOptions jsonReaderOptions)
  3. {
  4.         var reader = new Utf8JsonReader(sequence, jsonReaderOptions);
  5.         var map = new Dictionary<string, object>();
  6.         BuildJsonField(ref reader, map, null);
  7.         return map;
  8. }
复制代码
JsonReaderOptions 用于配置 Utf8JsonReader 读取策略,其主要属性如下:
属性说明AllowTrailingCommasbool是否允许(和忽略)对象或数组成员末尾多余的逗号CommentHandlingJsonCommentHandling如何处理 JSON 注释MaxDepthint最大嵌套深度,默认最大 64 层读取文件生成字典示例:
  1. // 注意,不能直接 File.ReadAllBytes() 读取文件,因为文件有 bom 头
  2. var text = Encoding.UTF8.GetBytes(File.ReadAllText("read.json"));
  3. var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(text), new JsonReaderOptions { AllowTrailingCommas = true });
复制代码
在 Demo4.Console 示例项目中,有一个 read.json 文件,其内容较为复杂,可以使用这个 json 验证代码。
1.png

另外我们可以利用 Utf8JsonReader ,结合第三章中的自定义配置教程,将 json 文件解析到 IConfiguration 中。
  1. var config = new ConfigurationBuilder()
  2.         .AddInMemoryCollection(dic.ToDictionary(x => x.Key, x => x.Value.ToString()))
  3.         .Build();
复制代码
Utf8JsonReader 和 JsonNode 解析 JSON 性能测试

JsonNode 也是我们读取 json 常用的方法之一,在本节中,笔者会介绍如何使用 BenchmarkDotNet 编写性能测试,对比 Utf8JsonReader 和 JsonNode 读取 json 的性能。
在 Demo4.Benchmark 示例项目中,有三个存储了大量对象数组的 json 文件,这些文件使用工具批量生成,我们将会使用这三个 json 进行性能测试。
2.png

对象格式:
  1.   {
  2.     "a_tttttttttttt": 1001,
  3.     "b_tttttttttttt": "邱平",
  4.     "c_tttttttttttt": "Nancy Lee",
  5.     "d_tttttttttttt": "buqdu",
  6.     "e_tttttttttttt": 81.26,
  7.     "f_tttttttttttt": 60,
  8.     "g_tttttttttttt": "1990-04-18 10:52:59",
  9.     "h_tttttttttttt": "35812178",
  10.     "i_tttttttttttt": "18935330000",
  11.     "j_tttttttttttt": "w.nsliozye@mbwrxiyf.ug",
  12.     "k_tttttttttttt": "浙江省 金华市 兰溪市"
  13.   }
复制代码
首先安装 BenchmarkDotNet 框架,然后创建一个性能测试入口加载 json 文件。
  1. [SimpleJob(RuntimeMoniker.Net80)]
  2. [SimpleJob(RuntimeMoniker.NativeAot80)]
  3. [MemoryDiagnoser]
  4. [ThreadingDiagnoser]
  5. [MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter]
  6. public class ParseJson
  7. {
  8.     private ReadOnlySequence<byte> sequence;
  9.     [Params("100.json", "1000.json", "10000.json")]
  10.     public string FileName;
  11.     [GlobalSetup]
  12.     public async Task Setup()
  13.     {
  14.         var text = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"json/{FileName}"));
  15.         var bytes = Encoding.UTF8.GetBytes(text);
  16.         sequence = new ReadOnlySequence<byte>(bytes);
  17.     }
  18. }
复制代码
在 ParseJson 中添加相关的方法,使用 Utf8JsonReader 解析 json :
  1. [Benchmark]
  2. public void Utf8JsonReader()
  3. {
  4.         var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
  5.         U8Read(ref reader);
  6. }
  7. private static void U8Read(ref Utf8JsonReader reader)
  8. {
  9.         while (reader.Read())
  10.         {
  11.                 if (reader.TokenType is JsonTokenType.StartArray)
  12.                 {
  13.                         U8ReadArray(ref reader);
  14.                 }
  15.                 else if (reader.TokenType is JsonTokenType.EndObject) break;
  16.                 else if (reader.TokenType is JsonTokenType.PropertyName)
  17.                 {
  18.                         reader.Read();
  19.                         if (reader.TokenType is JsonTokenType.StartArray)
  20.                         {
  21.                                 // 进入数组处理
  22.                                 U8ReadArray(ref reader);
  23.                         }
  24.                         else if (reader.TokenType is JsonTokenType.StartObject)
  25.                         {
  26.                                 U8Read(ref reader);
  27.                         }
  28.                         else
  29.                         {
  30.                         }
  31.                 }
  32.         }
  33. }
  34. private static void U8ReadArray(ref Utf8JsonReader reader)
  35. {
  36.         while (reader.Read())
  37.         {
  38.                 if (reader.TokenType is JsonTokenType.EndArray) break;
  39.                 switch (reader.TokenType)
  40.                 {
  41.                         case JsonTokenType.StartObject:
  42.                                 U8Read(ref reader);
  43.                                 break;
  44.                         // [...,[],...]
  45.                         case JsonTokenType.StartArray:
  46.                                 U8ReadArray(ref reader);
  47.                                 break;
  48.                 }
  49.         }
  50. }
复制代码
在 ParseJson 中增加  JsonNode 解析 json 的代码:
  1.         [Benchmark]
  2.         public void JsonNode()
  3.         {
  4.                 var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
  5.                 var nodes = System.Text.Json.Nodes.JsonNode.Parse(ref reader, null);
  6.                 if (nodes is JsonObject o)
  7.                 {
  8.                         JNRead(o);
  9.                 }
  10.                 else if (nodes is JsonArray a)
  11.                 {
  12.                         JNArray(a);
  13.                 }
  14.         }
  15.         private static void JNRead(JsonObject obj)
  16.         {
  17.                 foreach (var item in obj)
  18.                 {
  19.                         var v = item.Value;
  20.                         if (v is JsonObject o)
  21.                         {
  22.                                 JNRead(o);
  23.                         }
  24.                         else if (v is JsonArray a)
  25.                         {
  26.                                 JNArray(a);
  27.                         }
  28.                         else if (v is JsonValue value)
  29.                         {
  30.                                 var el = value.GetValue<JsonElement>();
  31.                                 JNValue(el);
  32.                         }
  33.                 }
  34.         }
  35.         private static void JNArray(JsonArray obj)
  36.         {
  37.                 foreach (var v in obj)
  38.                 {
  39.                         if (v is JsonObject o)
  40.                         {
  41.                                 JNRead(o);
  42.                         }
  43.                         else if (v is JsonArray a)
  44.                         {
  45.                                 JNArray(a);
  46.                         }
  47.                         else if (v is JsonValue value)
  48.                         {
  49.                                 var el = value.GetValue<JsonElement>();
  50.                                 JNValue(el);
  51.                         }
  52.                 }
  53.         }
  54.         private static void JNValue(JsonElement obj){}
复制代码
然后在 Main 方法中启动性能 Benchmark 框架进行测试。
  1.                 static void Main()
  2.                 {
  3.                         var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
  4.                         Console.Read();
  5.                 }
复制代码
以 Release 模式编译项目后,启动程序进行性能测试。
笔者所用机器配置:
  1. AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
复制代码
可以看到两者的性能差异比较大,所以在需要高性能的场景下,我们使用 Utf8JsonReader 的性能会高一点,还可以降低内存的使用量。
MethodJobFileNameMeanGen0Gen1Gen2AllocatedUtf8JsonReader.NET 8.0100.json42.87 us----JsonNode.NET 8.0100.json237.57 us37.109424.4141-312624 BUtf8JsonReaderNativeAOT 8.0100.json49.81 us----JsonNodeNativeAOT 8.0100.json301.11 us37.109424.4141-312624 BUtf8JsonReader.NET 8.01000.json427.07 us----JsonNode.NET 8.01000.json2,699.76 us484.3750460.9375199.21883120511 BUtf8JsonReaderNativeAOT 8.01000.json494.87 us----JsonNodeNativeAOT 8.01000.json3,652.08 us484.3750464.8438199.21883120513 BUtf8JsonReader.NET 8.010000.json4,306.30 us---3 BJsonNode.NET 8.010000.json60,883.56 us4000.00003888.88891222.222231215842 BUtf8JsonReaderNativeAOT 8.010000.json4,946.71 us---3 BJsonNodeNativeAOT 8.010000.json62,864.68 us4125.00004000.00001250.000031216863 B
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册