最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

.NET?中的序列化和反序列化問題示例詳解

 更新時間:2026年04月07日 08:48:45   作者:癡者工良  
文章主要介紹了.NET中的序列化和反序列化機制,重點講解了System.Text.Json的使用,包括配置選項、類型轉(zhuǎn)換器(如枚舉轉(zhuǎn)換器、字符串和值類型轉(zhuǎn)換器、時間類型轉(zhuǎn)換器)、性能測試(使用Utf8JsonReader和JsonNode),感興趣的朋友跟隨小編一起看看吧

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

在 ASP.NET Core 應(yīng)用中,框架會屏蔽了很多實現(xiàn)序列化和反序列化的細節(jié),我們只需要定義參數(shù)模型,ASP.NET Core 會自動將 http 請求的 Body 反序列化為模型對象。但是日常開發(fā)中我們會對序列化和反序列化做許多定制配置,比如忽略值為 null 的字段、時間格式處理、忽略大小寫、字段類型轉(zhuǎn)換等各種情況。因此筆者單獨使用一章講解序列化框架的使用以及如何進行定制,深入了解 .NET 中序列化和反序列化機制。

System.Text.Json 是 .NET 框架自帶的序列化框架,簡單易用并且性能也很出色,使用 System.Text.Json 反序列化字符串為對象是很簡單的,示例如下:

// 自定義序列化配置
static JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
	PropertyNameCaseInsensitive = true,
	WriteIndented = true
};
public static void Main()
{
	const string json =
		"""
            {
                "Name": "工良"
            }
            """;
	var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
}
public class Model
{
	public string Name { get; set; }
}

JsonSerializerOptions 的屬性定義了如何序列化和反序列化,其常用屬性如下:

屬性類型說明
AllowTrailingCommasbool忽略 JSON 中多余的逗號
ConvertersIList<JsonConverter>轉(zhuǎn)換器列表
DefaultBufferSizeint默認(rèn)緩沖區(qū)大小
DefaultIgnoreConditionJsonIgnoreCondition當(dāng)字段/屬性的值為默認(rèn)值時,是否忽略
DictionaryKeyPolicyJsonNamingPolicy字典 Key 重命名規(guī)則,如首字母生成小寫
IgnoreNullValuesbool忽略 JSON 中值為 null 的字段/屬性
IgnoreReadOnlyFieldsbool忽略只讀字段
IgnoreReadOnlyPropertiesbool忽略只讀屬性
IncludeFieldsbool是否處理字段,默認(rèn)只處理屬性
MaxDepthint最大嵌套深度,默認(rèn)最大深度為 64
NumberHandlingJsonNumberHandling如何處理數(shù)字類型
PropertyNameCaseInsensitivebool忽略大小寫
PropertyNamingPolicyJsonNamingPolicy重命名規(guī)則,如首字母生成小寫
ReadCommentHandlingJsonCommentHandling處理注釋
WriteIndentedbool序列化時格式化 JSON,如換行、空格、縮進

接下來筆者將會列舉一些常用的定制場景和編碼方法,為了避免混肴,在本章中所指的 “字段” 或 “屬性”,等同于類型的“字段和屬性”。

編寫類型轉(zhuǎn)換器

類型轉(zhuǎn)換器的作用是當(dāng) json 對象字段和模型類字段類型不一致時,可以自動轉(zhuǎn)換對應(yīng)的類型,下面筆者介紹常用的幾種類型轉(zhuǎn)換器。

枚舉轉(zhuǎn)換器

.NET 是如何序列化枚舉

編寫 WebAPI 的模型類時常常會用到枚舉,枚舉類型默認(rèn)會以數(shù)值的形式輸出到 json 中。

C# 代碼示例如下:

// 枚舉
public enum NetworkType
    {
        Unknown = 0,
        IPV4 = 1,
        IPV6 = 2
    }
// 類型
public class Model
    {
        public string Name { get; set; }
        public NetworkType Netwotk1 { get; set; }
        public NetworkType? Netwotk2 { get; set; }
    }
var model = new Model
		{
			Name = "工良",
			Netwotk1 = NetworkType.IPV4,
			Netwotk2 = NetworkType.IPV6
		}

當(dāng)我們序列化對象時,會得到這樣的結(jié)果:

{
	"Name": "工良",
	"Netwotk1": 1,
	"Netwotk2": 2
}

但是這樣會在閱讀上帶來難題,數(shù)字記憶比較困難,并且后期需要擴展枚舉字段時,可能會導(dǎo)致對應(yīng)數(shù)值的變化,那么已經(jīng)對接的代碼都需要修改,如果枚舉涉及的范圍比較廣,那么要做出修改就會變得十分困難。

比如說突然出現(xiàn)了一個 IPV5,那么我們除了改代碼,可能還要修改以及對接的其它應(yīng)用。

public enum NetworkType
{
        Unknown = 0,
        IPV4 = 1,
        IPV5 = 2,
        IPV6 = 3
}

因此,我們需要一種方法,能夠讓枚舉序列化后使用對應(yīng)的名稱表示,以及能夠使用這個字符串轉(zhuǎn)化為對應(yīng)的枚舉類型,后期需要擴展或中間插入時,對以前的代碼和數(shù)據(jù)庫完全沒有影響。

比如反序列化時,得到的是這樣的 json:

"Netwotk1": "IPV4"
"Netwotk2": "IPV6"

即使后來中間插入一個 IPV5,生成新的字符串即可,完全不需要重新排序枚舉值。

"Netwotk1": "IPV4"
"Netwotk2": "IPV6"
"Netwotk3": "IPV5"

在 C# 模型類中使用枚舉而 json 中使用字符串,要實現(xiàn)這種形式的枚舉轉(zhuǎn)換,有兩種方法。

  • 在模型類的枚舉字段或?qū)傩陨戏胖靡粋€特性注解,序列化反序列化時從這個特性注解中獲取轉(zhuǎn)換器。
  • 使用 JsonSerializerOptions 添加轉(zhuǎn)換器,在反序列化或序列化時傳遞自定義配置。

無論哪種方法,我們都需要實現(xiàn)一個轉(zhuǎn)換器,能夠?qū)⒛P皖愔械拿杜e使用對應(yīng)的名稱序列化到 json 中。在實現(xiàn)自定義轉(zhuǎn)換器示例之前,我們來了解相關(guān)的一些知識。

自定義轉(zhuǎn)換器需要繼承 JsonConverterJsonConverter<T>,當(dāng)反序列化 json 的字段或序列化對象的字段屬性時,框架會自動調(diào)用轉(zhuǎn)換器。

JsonConverter<T> 為例,里面有好幾個抽象接口,我們一般只需要實現(xiàn)轉(zhuǎn)換器的兩個抽象接口即可:

// json 值 => 對象字段
public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
// 對象字段 => json 值
public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);

不過我們一定要注意 C# 中的可空類型,比如 NetworkTypeNetworkType? 實際上是兩種類型,可空類型本質(zhì)是使用 Nullable<T> 包裝的類型。

Nullable<T> 的定義如下:

public struct Nullable<T> where T : struct

另外 Nullable<T> 實現(xiàn)了和 T 類型的隱式和顯式轉(zhuǎn)換重載,所以我們在使用可空類型時,可能不太容易感受出 Nullable<T> 和 T 區(qū)別,比如可以在使用可空類型 T? 時,直接將 Nullable<T>T 類型隱式和顯式轉(zhuǎn)換,如:

Nullable<int> value = 100

但是在使用反射時,由于 TT? 是兩種不同的類型,因此我們編寫轉(zhuǎn)換器時必須留意到這種區(qū)別,否則會出現(xiàn)錯誤。

實現(xiàn)枚舉轉(zhuǎn)換器

本節(jié)示例代碼在 Demo4.Console 中。

編寫一個枚舉字符串轉(zhuǎn)換器代碼示例如下:

public class EnumStringConverter<TEnum> : JsonConverter<TEnum>
{
	private readonly bool _isNullable;
	public EnumStringConverter(bool isNullType)
	{
		_isNullable = isNullType;
	}
    // 判斷當(dāng)前類型是否可以使用該轉(zhuǎn)換器轉(zhuǎn)換
	public override bool CanConvert(Type objectType) => EnumStringConverterFactory.IsEnum(objectType);
    // 從 json 中讀取數(shù)據(jù)
	// JSON => 值
	// typeToConvert: 模型類屬性/字段的類型
	public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
        // 讀取 json
		var value = reader.GetString();
		if (value == null)
		{
			if (_isNullable) return default;
			throw new ArgumentNullException(nameof(value));
		}
		// 是否為可空類型
		var sourceType = EnumStringConverterFactory.GetSourceType(typeof(TEnum));
		if (Enum.TryParse(sourceType, value.ToString(), out var result))
		{
			return (TEnum)result!;
		}
		throw new InvalidOperationException($"{value} 值不在枚舉 {typeof(TEnum).Name} 范圍中");
	}
	// 值 => JSON
	public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)
	{
		if (value == null) writer.WriteNullValue();
		else writer.WriteStringValue(Enum.GetName(value.GetType(), value));
	}
}

由于 Utf8JsonReader 日常出行的機會不多,因此讀者可能不太了解,在本章的末尾,筆者會簡單介紹。

一般情況下,我們不會直接使用 EnumStringConverter ,為了能夠適應(yīng)所有枚舉類型,還需要編寫一個枚舉轉(zhuǎn)換工廠,通過工廠模式判斷輸入類型之后,再創(chuàng)建對應(yīng)的轉(zhuǎn)換器。

public class EnumStringConverterFactory : JsonConverterFactory
{
	// 獲取需要轉(zhuǎn)換的類型
	public static bool IsEnum(Type objectType)
	{
		if (objectType.IsEnum) return true;
		var sourceType = Nullable.GetUnderlyingType(objectType);
		return sourceType is not null && sourceType.IsEnum;
	}
    // 如果類型是可空類型,則獲取原類型
	public static Type GetSourceType(Type typeToConvert)
	{
		if (typeToConvert.IsEnum) return typeToConvert;
		return Nullable.GetUnderlyingType(typeToConvert);
	}
    // 判斷該類型是否屬于枚舉
	public override bool CanConvert(Type typeToConvert) => IsEnum(typeToConvert);
    // 為該字段創(chuàng)建一個對應(yīng)的類型轉(zhuǎn)換器
	public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
	{
		var sourceType = GetSourceType(typeToConvert);
		var converter = typeof(EnumStringConverter<>).MakeGenericType(typeToConvert);
		return (JsonConverter)Activator.CreateInstance(converter, new object[] { sourceType != typeToConvert });
	}
}

當(dāng) System.Text.Json 處理一個字段時,會調(diào)用 EnumStringConverterFactory 的 CanConvert 方法,如果返回 true,則會調(diào)用 EnumStringConverterFactory 的 CreateConverter 方法創(chuàng)轉(zhuǎn)換器,最后調(diào)用轉(zhuǎn)換器處理字段,這樣一來,我們可以通過泛型類 EnumStringConverter<TEnum> 處理各種枚舉。

然后定義特性注解,能夠?qū)⒛P皖惖膶傩宰侄谓壎ǖ揭粋€轉(zhuǎn)換器上。

    [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class EnumConverterAttribute : JsonConverterAttribute
    {
        public override JsonConverter CreateConverter(Type typeToConvert)
        {
            return new EnumStringConverterFactory();
        }
    }

如何使用類型轉(zhuǎn)換器

使用自定義類型轉(zhuǎn)換器有三種方法。

方法一,在枚舉字段中使用自定義特性:

    public class Model
    {
        public string Name { get; set; }
        [EnumConverter]
        public NetworkType Netwotk1 { get; set; }
        [EnumConverter]
        public NetworkType? Netwotk2 { get; set; }
    }

方法二,使用 JsonConverter 特性。

public class Model
{
	public string Name { get; set; }
	[JsonConverter(typeof(EnumConverter))]   
	public NetworkType Netwotk1 { get; set; }
	[JsonConverter(typeof(EnumConverter))]
	public NetworkType? Netwotk2 { get; set; }
}

方法三,在配置中添加轉(zhuǎn)換器。

		jsonSerializerOptions.Converters.Add(new EnumStringConverterFactory());
		var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

在模型類中使用轉(zhuǎn)換器特性之后,我們可以通過字符串反序列化為枚舉類型:

        const string json =
            """
            {
                "Name": "工良",
                "Netwotk1": "IPV4",
                "Netwotk2": "IPV6"
            }
            """;
        var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

使用官方的轉(zhuǎn)換器

System.Text.Json 中已經(jīng)實現(xiàn)了很多轉(zhuǎn)換器,可以在官方源碼的 System/Text/Json/Serialization/Converters/Value 下找到所有自帶的轉(zhuǎn)換器,其中官方實現(xiàn)的枚舉字符串轉(zhuǎn)換器叫 JsonStringEnumConverter ,使用方法跟我們的自定義轉(zhuǎn)換器一致。

這里我們可以使用官方的 JsonStringEnumConverter 轉(zhuǎn)換器替代 EnumStringConverter<TEnum>

    public class Model
    {
        public string Name { get; set; }
        public NetworkType Netwotk1 { get; set; }
        public NetworkType? Netwotk2 { get; set; }
    }
        JsonSerializerOptions jsonSerializerOptions = new();
        jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        const string json =
            """
            {
                "Name": "工良",
                "Netwotk1": "IPV4",
                "Netwotk2": "IPV6"
            }
            """;
        var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

字符串和值類型轉(zhuǎn)換

很多情況下,會在模型類下使用數(shù)值類型,序列化到 json 時使用字符串。比如對應(yīng)浮點型的數(shù)值,為了保證其準(zhǔn)確性,我們會使用字符串形式保存到 json 中,這樣可以避免傳輸時對浮點型處理而丟失其準(zhǔn)確性。又比如前端處理超過 16 位數(shù)值時,數(shù)字會丟失精確度,16位數(shù)字存儲毫秒格式的時間戳足夠了,很多時候我們會使用分布式 id,雪花算法有很多種,其生成的 id 往往會超過 16 位。

JS 中處理超過 16 位數(shù)字時,會出現(xiàn)很精確度丟失的問題:

console.log(11111111111111111);
輸出: 11111111111111112
console.log(111111111111111111);
輸出: 111111111111111100

有個最簡單的方法是在 JsonSerializerOptions 中將所有數(shù)值字段轉(zhuǎn)換為字符串:

        new JsonSerializerOptions
		{
			NumberHandling = JsonNumberHandling.AllowReadingFromString
		};

但是這樣會導(dǎo)致所有值類型字段序列化為 json 時變成字符串,如果只需要處理幾個字段而不是處理所有字段,那就需要我們自己編寫類型轉(zhuǎn)換器了。

要實現(xiàn)字符串轉(zhuǎn)數(shù)值,需要考慮很多種數(shù)值類型,如 byte、int、double、long 等,從值類型轉(zhuǎn)換為字符串是很簡單的,但是要實現(xiàn)一個字符串轉(zhuǎn)任意類型值類型,那就很麻煩,這也是我們編寫轉(zhuǎn)換器的重點。

編寫 json 字符串和模型類值類型轉(zhuǎn)換器的代碼示例如下:

public class StringNumberConverter<T> : JsonConverter<T>
{
	private static readonly TypeCode typeCode = Type.GetTypeCode(typeof(T));
    // 從 json 中讀取字符串,轉(zhuǎn)換為對應(yīng)的值類型
	public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		switch (reader.TokenType)
		{
			case JsonTokenType.Number:
				if (typeCode == TypeCode.Int32)
				{
					if (reader.TryGetInt32(out var value))
					{
						return Unsafe.As<int, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Int64)
				{
					if (reader.TryGetInt64(out var value))
					{
						return Unsafe.As<long, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Decimal)
				{
					if (reader.TryGetDecimal(out var value))
					{
						return Unsafe.As<decimal, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Double)
				{
					if (reader.TryGetDouble(out var value))
					{
						return Unsafe.As<double, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Single)
				{
					if (reader.TryGetSingle(out var value))
					{
						return Unsafe.As<float, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Byte)
				{
					if (reader.TryGetByte(out var value))
					{
						return Unsafe.As<byte, T>(ref value);
					}
				}
				if (typeCode == TypeCode.SByte)
				{
					if (reader.TryGetSByte(out var value))
					{
						return Unsafe.As<sbyte, T>(ref value);
					}
				}
				if (typeCode == TypeCode.Int16)
				{
					if (reader.TryGetInt16(out var value))
					{
						return Unsafe.As<short, T>(ref value);
					}
				}
				if (typeCode == TypeCode.UInt16)
				{
					if (reader.TryGetUInt16(out var value))
					{
						return Unsafe.As<ushort, T>(ref value);
					}
				}
				if (typeCode == TypeCode.UInt32)
				{
					if (reader.TryGetUInt32(out var value))
					{
						return Unsafe.As<uint, T>(ref value);
					}
				}
				if (typeCode == TypeCode.UInt64)
				{
					if (reader.TryGetUInt64(out var value))
					{
						return Unsafe.As<ulong, T>(ref value);
					}
				}
				break;
			case JsonTokenType.String:
				IConvertible str = reader.GetString() ?? "";
				return (T)str.ToType(typeof(T), null);
		}
		throw new NotSupportedException($"無法將{reader.TokenType}轉(zhuǎn)換為{typeToConvert}");
	}
    // 將值類型轉(zhuǎn)換為 json 字符串
	public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
	{
		switch (typeCode)
		{
			case TypeCode.Int32:
				writer.WriteNumberValue(Unsafe.As<T, int>(ref value));
				break;
			case TypeCode.UInt32:
				writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
				break;
			case TypeCode.Decimal:
				writer.WriteNumberValue(Unsafe.As<T, decimal>(ref value));
				break;
			case TypeCode.Double:
				writer.WriteNumberValue(Unsafe.As<T, double>(ref value));
				break;
			case TypeCode.Single:
				writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
				break;
			case TypeCode.UInt64:
				writer.WriteNumberValue(Unsafe.As<T, ulong>(ref value));
				break;
			case TypeCode.Int64:
				writer.WriteNumberValue(Unsafe.As<T, long>(ref value));
				break;
			case TypeCode.Int16:
				writer.WriteNumberValue(Unsafe.As<T, short>(ref value));
				break;
			case TypeCode.UInt16:
				writer.WriteNumberValue(Unsafe.As<T, ushort>(ref value));
				break;
			case TypeCode.Byte:
				writer.WriteNumberValue(Unsafe.As<T, byte>(ref value));
				break;
			case TypeCode.SByte:
				writer.WriteNumberValue(Unsafe.As<T, sbyte>(ref value));
				break;
			default:
				throw new NotSupportedException($"不支持非數(shù)字類型{typeof(T)}");
		}
	}
}

編寫字符串轉(zhuǎn)換為各種類型的值類型,主要有一個難點泛型轉(zhuǎn)換,我們使用 reader.TryGetInt32() 讀取 int 值之后,明明知道泛型 T 是 int,但是我們卻不能直接返回 int ,我們必須要有一個手段可以將值轉(zhuǎn)換為泛型 T。如果使用反射,會帶來很大的性能消耗,還可能伴隨著裝箱拆箱,所以這里使用了 Unsafe.As ,其作用是將轉(zhuǎn)換類型的指針,使得相關(guān)的值類型可以轉(zhuǎn)換為泛型 T。

實現(xiàn)字符串和值類型轉(zhuǎn)換器之后,接著實現(xiàn)轉(zhuǎn)換工廠:

public class JsonStringToNumberConverter : JsonConverterFactory
{
	public static JsonStringToNumberConverter Default { get; } = new JsonStringToNumberConverter();
	public override bool CanConvert(Type typeToConvert)
	{
		var typeCode = Type.GetTypeCode(typeToConvert);
		return typeCode == TypeCode.Int32 ||
			typeCode == TypeCode.Decimal ||
			typeCode == TypeCode.Double ||
			typeCode == TypeCode.Single ||
			typeCode == TypeCode.Int64 ||
			typeCode == TypeCode.Int16 ||
			typeCode == TypeCode.Byte ||
			typeCode == TypeCode.UInt32 ||
			typeCode == TypeCode.UInt64 ||
			typeCode == TypeCode.UInt16 ||
			typeCode == TypeCode.SByte;
	}
	public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
	{
		var type = typeof(StringNumberConverter<>).MakeGenericType(typeToConvert);
		var converter = Activator.CreateInstance(type);
		if (converter == null)
		{
			throw new InvalidOperationException($"無法創(chuàng)建 {type.Name} 類型的轉(zhuǎn)換器");
		}
		return (JsonConverter)converter;
	}
}

時間類型轉(zhuǎn)換器

json 中規(guī)定了標(biāo)準(zhǔn)的時間格式,部分常用時間格式如下:

YYYY-MM-DDTHH:mm:ss.sssZ
YYYY-MM-DDTHH:mm:ss.sss+HH:mm
YYYY-MM-DDTHH:mm:ss.sss-HH:mm

示例:

2023-08-15T20:20:00+08:00

但是在項目開發(fā)中,我們很多使用需要使用定制的格式,如 2023-02-15 20:20:20 ,那么就需要自行編寫轉(zhuǎn)換器,以便能夠正確序列化或反序列化時間字段。

在 C# 中有一個指定 DateTtime 如何解析字符串時間的接口,即 DateTime.ParseExact(String, String, IFormatProvider),為了能夠適應(yīng)各種字符串時間格式,我們可以利用該接口將字符串轉(zhuǎn)換為時間。

編寫 json 字符串時間與 DateTime 互轉(zhuǎn)的代碼示例如下:

public class CustomDateTimeConverter : JsonConverter<DateTime>
{
	private readonly string _format;
    // format 參數(shù)是時間的字符串格式
	public CustomDateTimeConverter(string format)
	{
		_format = format;
	}
	public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options)
	{
		writer.WriteStringValue(date.ToString(_format));
	}
	public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		var value = reader.GetString() ?? throw new FormatException("當(dāng)前字段格式錯誤");
		return DateTime.ParseExact(value, _format, null);
	}
}

轉(zhuǎn)換器中不需要判斷 json 字符串時間的各種,而是在使用時指定格式在構(gòu)造函數(shù)中注入。使用示例:

jsonSerializerOptions.Converters.Add(new CustomDateTimeConverter("yyyy/MM/dd HH:mm:ss"));

其實,使用默認(rèn)的 json 時間格式是一個很好的習(xí)慣。據(jù)筆者經(jīng)驗,在項目中修改默認(rèn)的 json 時間格式,在后期項目開發(fā)和對接中,很有可能出現(xiàn)序列化問題。如果某些地方需要更高精細度,如需要毫秒、使用轉(zhuǎn)換為時間戳、第三方系統(tǒng)對接需要特殊格式等,可以在需要的模型類上使用特性標(biāo)記對應(yīng)的時間轉(zhuǎn)換器格式,最好不要全局修改 json 時間格式。

從底層處理 JSON

在本節(jié)中,筆者將會介紹如何使用 Utf8JsonReader 高性能地解析 json 文件,然后編寫對 Utf8JsonReader 的性能測試,通過相關(guān)的示例讓讀者掌握 Utf8JsonReader 的使用,以及如何對代碼進行性能測試。

Utf8JsonReader

Utf8JsonReader 和 Utf8JsonWriter 是 C# 中讀取寫入 json 的高性能 API,通過 Utf8JsonReader 和 Utf8JsonWriter 我們可以逐步讀取 json 或?qū)懭?json。

Utf8JsonReader 使用比較廣泛,例如官方的 JsonConfigurationProvider 便是使用 Utf8JsonReader 逐步讀取 json 文件,生成 key/value 結(jié)構(gòu),而在后面的章節(jié)中,筆者也會介紹如何利用 Utf8JsonReader 實現(xiàn) i18n 多語言的配置。由于 Utf8JsonReader 的使用最廣泛,而 Utf8JsonWriter 并不常見,所以筆者只介紹 Utf8JsonReader 的使用方法。

Utf8JsonReader 和 Utf8JsonWriter 都是結(jié)構(gòu)體,其定義如下:

public ref struct Utf8JsonReader
public ref struct Utf8JsonWriter

由于其是 ref 結(jié)構(gòu)體,因此使用上有較多限制,例如不能在異步中使用,不能作為類型參數(shù)在數(shù)組、 List<>、字典等中使用,只能被放到 ref struct 類型中當(dāng)作字段或?qū)傩?,或在函?shù)參數(shù)中使用。使用 Utf8JsonReader 讀取 json 時,開發(fā)者需要自行處理閉合括號 {}[] 等,也需要自行判斷處理 json 類型,因此讀取過程也稍為復(fù)雜 。

下面,筆者來設(shè)定一個場景,就是使用 Utf8JsonReader 來實現(xiàn)讀取 json 文件,將讀取到的字段全都存到字典中,如果有多層結(jié)構(gòu),則使用 : 拼接層級,生成 IConfiguration 中的能夠直接讀取的 key/value 格式。
比如:

// json
{
	"A": {
		"B": "test"
	}
}
// C#
new Dictionary<string, string>()
{
	{"A:B","test" }
};

新建一個靜態(tài)類 ReadJsonHelper,在這個類型中編寫解析 json 的代碼。

public static class ReadJsonHelper
{
}

首先是讀取字段值的代碼,當(dāng)從 json 讀取字段時,如果字段不是對象或數(shù)組類型,則直接讀取其值即可。

// 讀取字段值
private static object? ReadObject(ref Utf8JsonReader reader)
{
	switch (reader.TokenType)
	{
		case JsonTokenType.Null or JsonTokenType.None:
			return null;
		case JsonTokenType.False:
			return reader.GetBoolean();
		case JsonTokenType.True:
			return reader.GetBoolean();
		case JsonTokenType.Number:
			return reader.GetDouble();
		case JsonTokenType.String:
			return reader.GetString() ?? "";
		default: return null;
	}
}

讀取 json 字段時,我們會碰到復(fù)雜的嵌套結(jié)構(gòu),因此需要判斷當(dāng)前讀取的是對象還是數(shù)組,而且兩者可以相互嵌套,這就增加了我們的解析難度。

比如:

{
	... ...
}
[... ...]
[{...}, {...} ...]

第一步是判斷一個 json 的根結(jié)構(gòu)是 {} 還是 [],然后逐步解析。

// 解析 json 對象
private static void BuildJsonField(ref Utf8JsonReader reader, 
                                   Dictionary<string, object> map, 
                                   string? baseKey)
{
	while (reader.Read())
	{
		// 頂級數(shù)組 "[123,123]"
		if (reader.TokenType is JsonTokenType.StartArray)
		{
			ParseArray(ref reader, map, baseKey);
		}
		// 碰到 } 符號
		else if (reader.TokenType is JsonTokenType.EndObject) break;
		// 碰到字段
		else if (reader.TokenType is JsonTokenType.PropertyName)
		{
			var key = reader.GetString()!;
			var newkey = baseKey is null ? key : $"{baseKey}:{key}";
			// 判斷字段是否為對象
			reader.Read();
			if (reader.TokenType is JsonTokenType.StartArray)
			{
				ParseArray(ref reader, map, newkey);
			}
			else if (reader.TokenType is JsonTokenType.StartObject)
			{
				BuildJsonField(ref reader, map, newkey);
			}
			else
			{
				map[newkey] = ReadObject(ref reader);
			}
		}
	}
}

json 數(shù)組有很多種情況,json 數(shù)組的元素可以是任意類型,因此處理起來稍微麻煩,所以針對數(shù)組類型,我們還應(yīng)該支持解析元素,使用序號來訪問對應(yīng)位置的元素。

解析數(shù)組:

// 解析數(shù)組
private static void ParseArray(ref Utf8JsonReader reader, Dictionary<string, object> map, string? baseKey)
{
	int i = 0;
	while (reader.Read())
	{
		if (reader.TokenType is JsonTokenType.EndArray) break;
		var newkey = baseKey is null ? $"[{i}]" : $"{baseKey}[{i}]";
		i++;
		switch (reader.TokenType)
		{
			// [...,null,...]
			case JsonTokenType.Null:
				map[newkey] = null;
				break;
			// [...,123.666,...]
			case JsonTokenType.Number:
				map[newkey] = reader.GetDouble();
				break;
			// [...,"123",...]
			case JsonTokenType.String:
				map[newkey] = reader.GetString();
				break;
			// [...,true,...]
			case JsonTokenType.True:
				map[newkey] = reader.GetBoolean();
				break;
			case JsonTokenType.False:
				map[newkey] = reader.GetBoolean();
				break;
			// [...,{...},...]
			case JsonTokenType.StartObject:
				BuildJsonField(ref reader, map, newkey);
				break;
			// [...,[],...]
			case JsonTokenType.StartArray:
				ParseArray(ref reader, map, newkey);
				break;
			default:
				map[newkey] = JsonValueKind.Null;
				break;
		}
	}
}

最后,我們編寫一個解析 json 的入口,通過用戶傳遞的 json 文件,解析出字典。

public static Dictionary<string, object> Read(ReadOnlySequence<byte> sequence, 
                                              JsonReaderOptions jsonReaderOptions)
{
	var reader = new Utf8JsonReader(sequence, jsonReaderOptions);
	var map = new Dictionary<string, object>();
	BuildJsonField(ref reader, map, null);
	return map;
}

JsonReaderOptions 用于配置 Utf8JsonReader 讀取策略,其主要屬性如下:

屬性說明
AllowTrailingCommasbool是否允許(和忽略)對象或數(shù)組成員末尾多余的逗號
CommentHandlingJsonCommentHandling如何處理 JSON 注釋
MaxDepthint最大嵌套深度,默認(rèn)最大 64 層

讀取文件生成字典示例:

// 注意,不能直接 File.ReadAllBytes() 讀取文件,因為文件有 bom 頭
var text = Encoding.UTF8.GetBytes(File.ReadAllText("read.json"));
var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(text), new JsonReaderOptions { AllowTrailingCommas = true });

在 Demo4.Console 示例項目中,有一個 read.json 文件,其內(nèi)容較為復(fù)雜,可以使用這個 json 驗證代碼。

另外我們可以利用 Utf8JsonReader ,結(jié)合第三章中的自定義配置教程,將 json 文件解析到 IConfiguration 中。

var config = new ConfigurationBuilder()
	.AddInMemoryCollection(dic.ToDictionary(x => x.Key, x => x.Value.ToString()))
	.Build();

Utf8JsonReader 和 JsonNode 解析 JSON 性能測試

JsonNode 也是我們讀取 json 常用的方法之一,在本節(jié)中,筆者會介紹如何使用 BenchmarkDotNet 編寫性能測試,對比 Utf8JsonReader 和 JsonNode 讀取 json 的性能。

在 Demo4.Benchmark 示例項目中,有三個存儲了大量對象數(shù)組的 json 文件,這些文件使用工具批量生成,我們將會使用這三個 json 進行性能測試。

對象格式:

  {
    "a_tttttttttttt": 1001,
    "b_tttttttttttt": "邱平",
    "c_tttttttttttt": "Nancy Lee",
    "d_tttttttttttt": "buqdu",
    "e_tttttttttttt": 81.26,
    "f_tttttttttttt": 60,
    "g_tttttttttttt": "1990-04-18 10:52:59",
    "h_tttttttttttt": "35812178",
    "i_tttttttttttt": "18935330000",
    "j_tttttttttttt": "w.nsliozye@mbwrxiyf.ug",
    "k_tttttttttttt": "浙江省 金華市 蘭溪市"
  }

首先安裝 BenchmarkDotNet 框架,然后創(chuàng)建一個性能測試入口加載 json 文件。

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.NativeAot80)]
[MemoryDiagnoser]
[ThreadingDiagnoser]
[MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter]
public class ParseJson
{
    private ReadOnlySequence<byte> sequence;
    [Params("100.json", "1000.json", "10000.json")]
    public string FileName;
    [GlobalSetup]
    public async Task Setup()
    {
        var text = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"json/{FileName}"));
        var bytes = Encoding.UTF8.GetBytes(text);
        sequence = new ReadOnlySequence<byte>(bytes);
    }
}

在 ParseJson 中添加相關(guān)的方法,使用 Utf8JsonReader 解析 json :

[Benchmark]
public void Utf8JsonReader()
{
	var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
	U8Read(ref reader);
}
private static void U8Read(ref Utf8JsonReader reader)
{
	while (reader.Read())
	{
		if (reader.TokenType is JsonTokenType.StartArray)
		{
			U8ReadArray(ref reader);
		}
		else if (reader.TokenType is JsonTokenType.EndObject) break;
		else if (reader.TokenType is JsonTokenType.PropertyName)
		{
			reader.Read();
			if (reader.TokenType is JsonTokenType.StartArray)
			{
				// 進入數(shù)組處理
				U8ReadArray(ref reader);
			}
			else if (reader.TokenType is JsonTokenType.StartObject)
			{
				U8Read(ref reader);
			}
			else
			{
			}
		}
	}
}
private static void U8ReadArray(ref Utf8JsonReader reader)
{
	while (reader.Read())
	{
		if (reader.TokenType is JsonTokenType.EndArray) break;
		switch (reader.TokenType)
		{
			case JsonTokenType.StartObject:
				U8Read(ref reader);
				break;
			// [...,[],...]
			case JsonTokenType.StartArray:
				U8ReadArray(ref reader);
				break;
		}
	}
}

在 ParseJson 中增加 JsonNode 解析 json 的代碼:

[Benchmark]
	public void JsonNode()
	{
		var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
		var nodes = System.Text.Json.Nodes.JsonNode.Parse(ref reader, null);
		if (nodes is JsonObject o)
		{
			JNRead(o);
		}
		else if (nodes is JsonArray a)
		{
			JNArray(a);
		}
	}
	private static void JNRead(JsonObject obj)
	{
		foreach (var item in obj)
		{
			var v = item.Value;
			if (v is JsonObject o)
			{
				JNRead(o);
			}
			else if (v is JsonArray a)
			{
				JNArray(a);
			}
			else if (v is JsonValue value)
			{
				var el = value.GetValue<JsonElement>();
				JNValue(el);
			}
		}
	}
	private static void JNArray(JsonArray obj)
	{
		foreach (var v in obj)
		{
			if (v is JsonObject o)
			{
				JNRead(o);
			}
			else if (v is JsonArray a)
			{
				JNArray(a);
			}
			else if (v is JsonValue value)
			{
				var el = value.GetValue<JsonElement>();
				JNValue(el);
			}
		}
	}
	private static void JNValue(JsonElement obj){}

然后在 Main 方法中啟動性能 Benchmark 框架進行測試。

		static void Main()
		{
			var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
			Console.Read();
		}

以 Release 模式編譯項目后,啟動程序進行性能測試。

筆者所用機器配置:

AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores

可以看到兩者的性能差異比較大,所以在需要高性能的場景下,我們使用 Utf8JsonReader 的性能會高一點,還可以降低內(nèi)存的使用量。

MethodJobFileNameMeanGen0Gen1Gen2Allocated
Utf8JsonReader.NET 8.0100.json42.87 us----
JsonNode.NET 8.0100.json237.57 us37.109424.4141-312624 B
Utf8JsonReaderNativeAOT 8.0100.json49.81 us----
JsonNodeNativeAOT 8.0100.json301.11 us37.109424.4141-312624 B
Utf8JsonReader.NET 8.01000.json427.07 us----
JsonNode.NET 8.01000.json2,699.76 us484.3750460.9375199.21883120511 B
Utf8JsonReaderNativeAOT 8.01000.json494.87 us----
JsonNodeNativeAOT 8.01000.json3,652.08 us484.3750464.8438199.21883120513 B
Utf8JsonReader.NET 8.010000.json4,306.30 us---3 B
JsonNode.NET 8.010000.json60,883.56 us4000.00003888.88891222.222231215842 B
Utf8JsonReaderNativeAOT 8.010000.json4,946.71 us---3 B
JsonNodeNativeAOT 8.010000.json62,864.68 us4125.00004000.00001250.000031216863 B

到此這篇關(guān)于.NET 中的序列化和反序列化的文章就介紹到這了,更多相關(guān).NET 序列化和反序列化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

南阳市| 乐昌市| 朝阳县| 莱芜市| 榕江县| 台东市| 寻乌县| 嘉善县| 濉溪县| 阿尔山市| 化州市| 铜鼓县| 巴彦淖尔市| 寿光市| 阿荣旗| 永胜县| 永泰县| 海原县| 金川县| 五指山市| 永州市| 厦门市| 乌拉特中旗| 略阳县| 林周县| 怀安县| 汕尾市| 天等县| 丰台区| 苍山县| 白河县| 罗田县| 云南省| 高淳县| 临沂市| 西乌| 连云港市| 马山县| 岐山县| 红河县| 定远县|