C#自定義Key類型的字典無法序列化的解決方案詳解
一、問題重現(xiàn)
我們先通過如下這個簡單的例子來重現(xiàn)上述這個問題。如代碼片段所示,我們定義了一個名為Point(代表二維坐標(biāo)點)的只讀結(jié)構(gòu)體作為待序列化字典的Key。Point可以通過結(jié)構(gòu)化的表達式來表示,我們同時還定義了Parse方法將表達式轉(zhuǎn)換成Point對象。
using System.Diagnostics;
using System.Text.Json;
var dictionary = new Dictionary<Point, int>
{
{ new Point(1.0, 1.0), 1 },
{ new Point(2.0, 2.0), 2 },
{ new Point(3.0, 3.0), 3 }
};
try
{
var json = JsonSerializer.Serialize(dictionary);
Console.WriteLine(json);
var dictionary2 = JsonSerializer.Deserialize<Dictionary<Point, int>>(json)!;
Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
public readonly record struct Point(double X, double Y)
{
public override string ToString()=> $"({X}, {Y})";
public static Point Parse(string s)
{
var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
if (tokens.Length != 2)
{
throw new FormatException("Invalid format");
}
return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
}
}當(dāng)我們使用JsonSerializer序列化多一個Dictionary<Point, int>類型的對象時,會拋出一個NotSupportedException異常,如下所示的信息解釋了錯誤的根源:Point類型不能作為被序列化字典對象的Key。順便說一下,如果使用Newtonsoft.Json,這樣的字典可以序列化成功,但是反序列化會失敗。

二、自定義JsonConverter<Point>能解決嗎
遇到這樣的問題我們首先想到的是:既然不執(zhí)行針對Point的序列化/反序列化,那么我們可以對應(yīng)相應(yīng)的JsonConverter自行完成序列化/反序列化工作。為此我們定義了如下這個PointConverter,將Point的表達式作為序列化輸出結(jié)果,同時調(diào)用Parse方法生成反序列化的結(jié)果。
public class PointConverter : JsonConverter<Point>
{
public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)=> Point.Parse(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
}我們將這個PointConverter對象添加到創(chuàng)建的JsonSerializerOptions配置選項中,并將后者傳入序列化和反序列化方法中。
var options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new PointConverter() }
};
var json = JsonSerializer.Serialize(dictionary, options);
Console.WriteLine(json);
var dictionary2 = JsonSerializer.Deserialize<Dictionary<Point, int>>(json, options)!;
Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);不幸的是,這樣的解決方案無效,序列化時依然會拋出相同的異常。

三、自定義TypeConverter能解決問題嗎
JsonConverter的目的本質(zhì)上就是希望將Point對象視為字符串進行處理,既然自定義JsonConverter無法解決這個問題,我們是否可以注冊相應(yīng)的類型轉(zhuǎn)換其來解決它呢?為此我們定義了如下這個PointTypeConverter 類型,使它來完成針對Point和字符串之間的類型轉(zhuǎn)換。
public class PointTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string);
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string);
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => Point.Parse((string)value);
public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value?.ToString()!;
}我們利用標(biāo)注的TypeConverterAttribute特性將PointTypeConverter注冊到Point類型上。
[TypeConverter(typeof(PointTypeConverter))]
public readonly record struct Point(double X, double Y)
{
public override string ToString() => $"({X}, {Y})";
public static Point Parse(string s)
{
var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
if (tokens.Length != 2)
{
throw new FormatException("Invalid format");
}
return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
}
}實驗證明,這種解決方案依然無效,序列化時還是會拋出相同的異常。順便說一下,這種解決方案對于Newtonsoft.Json是適用的。

四、以鍵值對集合的形式序列化
為Point定義JsonConverter之所以不能解決我們的問題,是因為異常并不是在試圖序列化Point對象時拋出來的,而是在在默認的規(guī)則序列化字典對象時,不合法的Key類型沒有通過驗證。如果希望通過自定義JsonConverter的方式來解決,目標(biāo)類型不應(yīng)該時Point類型,而應(yīng)該時字典類型,為此我們定義了如下這個PointKeyedDictionaryConverter<TValue>類型。
我們知道字典本質(zhì)上就是鍵值對的集合,而集合針對元素類型并沒有特殊的約束,所以我們完全可以按照鍵值對集合的方式來進行序列化和反序列化。如代碼把片段所示,用于序列化的Write方法中,我們利用作為參數(shù)的JsonSerializerOptions 得到針對IEnumerable<KeyValuePair<Point, TValue>>類型的JsonConverter,并利用它以鍵值對的形式對字典進行序列化。
public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var enumerableConverter = (JsonConverter<IEnumerable<KeyValuePair<Point, TValue>>>)options.GetConverter(typeof(IEnumerable<KeyValuePair<Point, TValue>>));
return enumerableConverter.Read(ref reader, typeof(IEnumerable<KeyValuePair<Point, TValue>>), options)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
{
var enumerableConverter = (JsonConverter<IEnumerable<KeyValuePair<Point, TValue>>>)options.GetConverter(typeof(IEnumerable<KeyValuePair<Point, TValue>>));
enumerableConverter.Write(writer, value, options);
}
}用于反序列化的Read方法中,我們采用相同的方式得到這個針對IEnumerable<KeyValuePair<Point, TValue>>類型的JsonConverter,并將其反序列化成鍵值對集合,在轉(zhuǎn)換成返回的字典。
var options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new PointConverter(), new PointKeyedDictionaryConverter<int>()}
};我們將PointKeyedDictionaryConverter<int>添加到創(chuàng)建的JsonSerializerOptions配置選項的JsonConverter列表中。從如下所示的輸出結(jié)果可以看出,我們創(chuàng)建的字典確實是以鍵值對集合的形式進行序列化的。

五、轉(zhuǎn)換成合法的字典
既然作為字典Key的Point可以轉(zhuǎn)換成字符串,那么可以還有另一種解法,那就是將以Point為Key的字典轉(zhuǎn)換成以字符串為Key的字典,為此我們按照如下的方式重寫的PointKeyedDictionaryConverter<TValue>。如代碼片段所示,重寫的Writer方法利用傳入的JsonSerializerOptions配置選項得到針對Dictionary<string, TValue>的JsonConverter,然后將待序列化的Dictionary<Point, TValue> 對象轉(zhuǎn)換成Dictionary<string, TValue> 交給它進行序列化。
public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var converter = (JsonConverter<Dictionary<string, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
return converter.Read(ref reader, typeof(Dictionary<string, TValue>), options)
?.ToDictionary(kv => Point.Parse(kv.Key), kv=> kv.Value);
}
public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
{
var converter = (JsonConverter<Dictionary<string, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
converter.Write(writer, value.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options);
}
}重寫的Read方法采用相同的方式得到JsonConverter<Dictionary<string, TValue>>對象,并利用它執(zhí)行反序列化生成Dictionary<string, TValue> 對象。我們最終將它轉(zhuǎn)換成需要的Dictionary<Point, TValue> 對象。從如下所示的輸出可以看出,這次的序列化生成的JSON會更加精煉,因為這次是以字典類型輸出JSON字符串的。

六、自定義讀寫
雖然以上兩種方式都能解決我們的問題,而且從最終JSON字符串輸出的長度來看,第二種具有更好的性能,但是它們都有一個問題,那么就是需要創(chuàng)建中間對象。第一種方案需要創(chuàng)建一個鍵值對集合,第二種方案則需要創(chuàng)建一個Dictionary<string, TValue> 對象,如果對性能有更高的追求,它們都不是一種好的解決方案。既讓我們都已經(jīng)在自定義JsonConverter,完全可以自行可控制JSON內(nèi)容的讀寫,為此我們再次重寫了PointKeyedDictionaryConverter<TValue>。
public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
JsonConverter<TValue>? valueConverter = null;
Dictionary<Point, TValue>? dictionary = null;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return dictionary;
}
valueConverter ??= (JsonConverter<TValue>)options.GetConverter(typeof(TValue))!;
dictionary ??= [];
var key = Point.Parse(reader.GetString()!);
reader.Read();
var value = valueConverter.Read(ref reader, typeof(TValue), options)!;
dictionary.Add(key, value);
}
return dictionary;
}
public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
JsonConverter<TValue>? valueConverter = null;
foreach (var (k, v) in value)
{
valueConverter ??= (JsonConverter<TValue>)options.GetConverter(typeof(TValue))!;
writer.WritePropertyName(k.ToString());
valueConverter.Write(writer, v, options);
}
writer.WriteEndObject();
}
}如上面的代碼片段所示,在重寫的Write方法中,我們調(diào)用Utf8JsonWriter 的WriteStartObject和 WriteEndObject方法以對象的形式輸出字典。在這中間,我們便利字典的每個鍵值對,并以“屬性”的形式對它們進行輸出(Key和Value分別是屬性名和值)。在Read方法中,我們創(chuàng)建一個空的Dictionary<Point, TValue> 對象,在一個循環(huán)中利用Utf8JsonReader先后讀取作為Key的字符串和Value值,最終將Key轉(zhuǎn)換成Point類型,并添加到創(chuàng)建的字典中。從如下所示的輸出結(jié)果可以看出,這次生成的JSON具有與上面相同的結(jié)構(gòu)。

以上就是C#自定義Key類型的字典無法序列化的解決方案詳解的詳細內(nèi)容,更多關(guān)于C#字典無法序列化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#使用Enum.TryParse()實現(xiàn)枚舉安全轉(zhuǎn)換
這篇文章介紹了C#使用Enum.TryParse()實現(xiàn)枚舉安全轉(zhuǎn)換的方法,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08
Unity利用材質(zhì)自發(fā)光實現(xiàn)物體閃爍
這篇文章主要為大家詳細介紹了Unity利用材質(zhì)自發(fā)光實現(xiàn)物體閃爍,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04
C#實現(xiàn)獲取Excel中圖片所在坐標(biāo)位置
本文以C#和vb.net代碼示例展示如何來獲取Excel工作表中圖片的坐標(biāo)位置,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-04-04
c#中l(wèi)ist.FindAll與for循環(huán)的性能對比總結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于c#中l(wèi)ist.FindAll與for循環(huán)的性能,文中通過詳細的示例代碼給大家介紹了這兩者之間的性能,對大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享
最近在網(wǎng)上看到了新版的360安全衛(wèi)士,感覺界面還不錯,于是用WPF制作了一個,時間有限,一些具體的控件沒有制作,用圖片代替了。感興趣的朋友一起跟著小編學(xué)習(xí)WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享2015-09-09

