C#自定義Key類型的字典無(wú)法序列化的解決方案詳解
一、問(wèn)題重現(xiàn)
我們先通過(guò)如下這個(gè)簡(jiǎn)單的例子來(lái)重現(xiàn)上述這個(gè)問(wèn)題。如代碼片段所示,我們定義了一個(gè)名為Point(代表二維坐標(biāo)點(diǎn))的只讀結(jié)構(gòu)體作為待序列化字典的Key。Point可以通過(guò)結(jié)構(gòu)化的表達(dá)式來(lái)表示,我們同時(shí)還定義了Parse方法將表達(dá)式轉(zhuǎn)換成Point對(duì)象。
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序列化多一個(gè)Dictionary<Point, int>類型的對(duì)象時(shí),會(huì)拋出一個(gè)NotSupportedException異常,如下所示的信息解釋了錯(cuò)誤的根源:Point類型不能作為被序列化字典對(duì)象的Key。順便說(shuō)一下,如果使用Newtonsoft.Json,這樣的字典可以序列化成功,但是反序列化會(huì)失敗。
二、自定義JsonConverter<Point>能解決嗎
遇到這樣的問(wèn)題我們首先想到的是:既然不執(zhí)行針對(duì)Point的序列化/反序列化,那么我們可以對(duì)應(yīng)相應(yīng)的JsonConverter自行完成序列化/反序列化工作。為此我們定義了如下這個(gè)PointConverter,將Point的表達(dá)式作為序列化輸出結(jié)果,同時(shí)調(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()); }
我們將這個(gè)PointConverter對(duì)象添加到創(chuàng)建的JsonSerializerOptions配置選項(xiàng)中,并將后者傳入序列化和反序列化方法中。
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);
不幸的是,這樣的解決方案無(wú)效,序列化時(shí)依然會(huì)拋出相同的異常。
三、自定義TypeConverter能解決問(wèn)題嗎
JsonConverter的目的本質(zhì)上就是希望將Point對(duì)象視為字符串進(jìn)行處理,既然自定義JsonConverter無(wú)法解決這個(gè)問(wèn)題,我們是否可以注冊(cè)相應(yīng)的類型轉(zhuǎn)換其來(lái)解決它呢?為此我們定義了如下這個(gè)PointTypeConverter 類型,使它來(lái)完成針對(duì)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注冊(cè)到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])); } }
實(shí)驗(yàn)證明,這種解決方案依然無(wú)效,序列化時(shí)還是會(huì)拋出相同的異常。順便說(shuō)一下,這種解決方案對(duì)于Newtonsoft.Json是適用的。
四、以鍵值對(duì)集合的形式序列化
為Point定義JsonConverter之所以不能解決我們的問(wèn)題,是因?yàn)楫惓2⒉皇窃谠噲D序列化Point對(duì)象時(shí)拋出來(lái)的,而是在在默認(rèn)的規(guī)則序列化字典對(duì)象時(shí),不合法的Key類型沒(méi)有通過(guò)驗(yàn)證。如果希望通過(guò)自定義JsonConverter的方式來(lái)解決,目標(biāo)類型不應(yīng)該時(shí)Point類型,而應(yīng)該時(shí)字典類型,為此我們定義了如下這個(gè)PointKeyedDictionaryConverter<TValue>類型。
我們知道字典本質(zhì)上就是鍵值對(duì)的集合,而集合針對(duì)元素類型并沒(méi)有特殊的約束,所以我們完全可以按照鍵值對(duì)集合的方式來(lái)進(jìn)行序列化和反序列化。如代碼把片段所示,用于序列化的Write方法中,我們利用作為參數(shù)的JsonSerializerOptions 得到針對(duì)IEnumerable<KeyValuePair<Point, TValue>>類型的JsonConverter,并利用它以鍵值對(duì)的形式對(duì)字典進(jìn)行序列化。
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方法中,我們采用相同的方式得到這個(gè)針對(duì)IEnumerable<KeyValuePair<Point, TValue>>類型的JsonConverter,并將其反序列化成鍵值對(duì)集合,在轉(zhuǎn)換成返回的字典。
var options = new JsonSerializerOptions { WriteIndented = true, Converters = { new PointConverter(), new PointKeyedDictionaryConverter<int>()} };
我們將PointKeyedDictionaryConverter<int>添加到創(chuàng)建的JsonSerializerOptions配置選項(xiàng)的JsonConverter列表中。從如下所示的輸出結(jié)果可以看出,我們創(chuàng)建的字典確實(shí)是以鍵值對(duì)集合的形式進(jìn)行序列化的。
五、轉(zhuǎn)換成合法的字典
既然作為字典Key的Point可以轉(zhuǎn)換成字符串,那么可以還有另一種解法,那就是將以Point為Key的字典轉(zhuǎn)換成以字符串為Key的字典,為此我們按照如下的方式重寫(xiě)的PointKeyedDictionaryConverter<TValue>。如代碼片段所示,重寫(xiě)的Writer方法利用傳入的JsonSerializerOptions配置選項(xiàng)得到針對(duì)Dictionary<string, TValue>的JsonConverter,然后將待序列化的Dictionary<Point, TValue> 對(duì)象轉(zhuǎn)換成Dictionary<string, TValue> 交給它進(jìn)行序列化。
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); } }
重寫(xiě)的Read方法采用相同的方式得到JsonConverter<Dictionary<string, TValue>>對(duì)象,并利用它執(zhí)行反序列化生成Dictionary<string, TValue> 對(duì)象。我們最終將它轉(zhuǎn)換成需要的Dictionary<Point, TValue> 對(duì)象。從如下所示的輸出可以看出,這次的序列化生成的JSON會(huì)更加精煉,因?yàn)檫@次是以字典類型輸出JSON字符串的。
六、自定義讀寫(xiě)
雖然以上兩種方式都能解決我們的問(wèn)題,而且從最終JSON字符串輸出的長(zhǎng)度來(lái)看,第二種具有更好的性能,但是它們都有一個(gè)問(wèn)題,那么就是需要?jiǎng)?chuàng)建中間對(duì)象。第一種方案需要?jiǎng)?chuàng)建一個(gè)鍵值對(duì)集合,第二種方案則需要?jiǎng)?chuàng)建一個(gè)Dictionary<string, TValue> 對(duì)象,如果對(duì)性能有更高的追求,它們都不是一種好的解決方案。既讓我們都已經(jīng)在自定義JsonConverter,完全可以自行可控制JSON內(nèi)容的讀寫(xiě),為此我們?cè)俅沃貙?xiě)了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(); } }
如上面的代碼片段所示,在重寫(xiě)的Write方法中,我們調(diào)用Utf8JsonWriter 的WriteStartObject和 WriteEndObject方法以對(duì)象的形式輸出字典。在這中間,我們便利字典的每個(gè)鍵值對(duì),并以“屬性”的形式對(duì)它們進(jìn)行輸出(Key和Value分別是屬性名和值)。在Read方法中,我們創(chuàng)建一個(gè)空的Dictionary<Point, TValue> 對(duì)象,在一個(gè)循環(huán)中利用Utf8JsonReader先后讀取作為Key的字符串和Value值,最終將Key轉(zhuǎn)換成Point類型,并添加到創(chuàng)建的字典中。從如下所示的輸出結(jié)果可以看出,這次生成的JSON具有與上面相同的結(jié)構(gòu)。
以上就是C#自定義Key類型的字典無(wú)法序列化的解決方案詳解的詳細(xì)內(nèi)容,更多關(guān)于C#字典無(wú)法序列化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
c#之OpenFileDialog解讀(打開(kāi)文件對(duì)話框)
這篇文章主要介紹了c#之OpenFileDialog(打開(kāi)文件對(duì)話框),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07C#使用Enum.TryParse()實(shí)現(xiàn)枚舉安全轉(zhuǎn)換
這篇文章介紹了C#使用Enum.TryParse()實(shí)現(xiàn)枚舉安全轉(zhuǎn)換的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Unity利用材質(zhì)自發(fā)光實(shí)現(xiàn)物體閃爍
這篇文章主要為大家詳細(xì)介紹了Unity利用材質(zhì)自發(fā)光實(shí)現(xiàn)物體閃爍,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04C#實(shí)現(xiàn)獲取Excel中圖片所在坐標(biāo)位置
本文以C#和vb.net代碼示例展示如何來(lái)獲取Excel工作表中圖片的坐標(biāo)位置,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-04-04c#中l(wèi)ist.FindAll與for循環(huán)的性能對(duì)比總結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于c#中l(wèi)ist.FindAll與for循環(huán)的性能,文中通過(guò)詳細(xì)的示例代碼給大家介紹了這兩者之間的性能,對(duì)大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10Unity實(shí)現(xiàn)大轉(zhuǎn)盤(pán)的簡(jiǎn)單筆記
這篇文章主要為大家分享了Unity實(shí)現(xiàn)大轉(zhuǎn)盤(pán)的簡(jiǎn)單筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02WPF實(shí)現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享
最近在網(wǎng)上看到了新版的360安全衛(wèi)士,感覺(jué)界面還不錯(cuò),于是用WPF制作了一個(gè),時(shí)間有限,一些具體的控件沒(méi)有制作,用圖片代替了。感興趣的朋友一起跟著小編學(xué)習(xí)WPF實(shí)現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享2015-09-09