C#自定義Key類型的字典無法序列化的解決方案詳解
一、問題重現(xiàn)
我們先通過如下這個簡單的例子來重現(xiàn)上述這個問題。如代碼片段所示,我們定義了一個名為Point(代表二維坐標點)的只讀結構體作為待序列化字典的Key。Point可以通過結構化的表達式來表示,我們同時還定義了Parse方法將表達式轉換成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])); } }
當我們使用JsonSerializer序列化多一個Dictionary<Point, int>類型的對象時,會拋出一個NotSupportedException異常,如下所示的信息解釋了錯誤的根源:Point類型不能作為被序列化字典對象的Key。順便說一下,如果使用Newtonsoft.Json,這樣的字典可以序列化成功,但是反序列化會失敗。
二、自定義JsonConverter<Point>能解決嗎
遇到這樣的問題我們首先想到的是:既然不執(zhí)行針對Point的序列化/反序列化,那么我們可以對應相應的JsonConverter自行完成序列化/反序列化工作。為此我們定義了如下這個PointConverter,將Point的表達式作為序列化輸出結果,同時調用Parse方法生成反序列化的結果。
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的目的本質上就是希望將Point對象視為字符串進行處理,既然自定義JsonConverter無法解決這個問題,我們是否可以注冊相應的類型轉換其來解決它呢?為此我們定義了如下這個PointTypeConverter 類型,使它來完成針對Point和字符串之間的類型轉換。
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()!; }
我們利用標注的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的方式來解決,目標類型不應該時Point類型,而應該時字典類型,為此我們定義了如下這個PointKeyedDictionaryConverter<TValue>類型。
我們知道字典本質上就是鍵值對的集合,而集合針對元素類型并沒有特殊的約束,所以我們完全可以按照鍵值對集合的方式來進行序列化和反序列化。如代碼把片段所示,用于序列化的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,并將其反序列化成鍵值對集合,在轉換成返回的字典。
var options = new JsonSerializerOptions { WriteIndented = true, Converters = { new PointConverter(), new PointKeyedDictionaryConverter<int>()} };
我們將PointKeyedDictionaryConverter<int>添加到創(chuàng)建的JsonSerializerOptions配置選項的JsonConverter列表中。從如下所示的輸出結果可以看出,我們創(chuàng)建的字典確實是以鍵值對集合的形式進行序列化的。
五、轉換成合法的字典
既然作為字典Key的Point可以轉換成字符串,那么可以還有另一種解法,那就是將以Point為Key的字典轉換成以字符串為Key的字典,為此我們按照如下的方式重寫的PointKeyedDictionaryConverter<TValue>。如代碼片段所示,重寫的Writer方法利用傳入的JsonSerializerOptions配置選項得到針對Dictionary<string, TValue>的JsonConverter,然后將待序列化的Dictionary<Point, TValue> 對象轉換成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> 對象。我們最終將它轉換成需要的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方法中,我們調用Utf8JsonWriter 的WriteStartObject和 WriteEndObject方法以對象的形式輸出字典。在這中間,我們便利字典的每個鍵值對,并以“屬性”的形式對它們進行輸出(Key和Value分別是屬性名和值)。在Read方法中,我們創(chuàng)建一個空的Dictionary<Point, TValue> 對象,在一個循環(huán)中利用Utf8JsonReader先后讀取作為Key的字符串和Value值,最終將Key轉換成Point類型,并添加到創(chuàng)建的字典中。從如下所示的輸出結果可以看出,這次生成的JSON具有與上面相同的結構。
以上就是C#自定義Key類型的字典無法序列化的解決方案詳解的詳細內(nèi)容,更多關于C#字典無法序列化的資料請關注腳本之家其它相關文章!
相關文章
C#使用Enum.TryParse()實現(xiàn)枚舉安全轉換
這篇文章介紹了C#使用Enum.TryParse()實現(xiàn)枚舉安全轉換的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08c#中l(wèi)ist.FindAll與for循環(huán)的性能對比總結
這篇文章主要給大家總結介紹了關于c#中l(wèi)ist.FindAll與for循環(huán)的性能,文中通過詳細的示例代碼給大家介紹了這兩者之間的性能,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。2017-10-10WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享
最近在網(wǎng)上看到了新版的360安全衛(wèi)士,感覺界面還不錯,于是用WPF制作了一個,時間有限,一些具體的控件沒有制作,用圖片代替了。感興趣的朋友一起跟著小編學習WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享2015-09-09