欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C#自定義Key類型的字典無法序列化的解決方案詳解

 更新時間:2024年03月19日 11:13:53   作者:Artech  
當我們使用System.Text.Json.JsonSerializer對一個字典對象進行序列化的時候,默認情況下字典的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&lt;TValue&gt; : JsonConverter&lt;Dictionary&lt;Point, TValue&gt;&gt;
{
    public override Dictionary&lt;Point, TValue&gt;? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        JsonConverter&lt;TValue&gt;? valueConverter = null;
        Dictionary&lt;Point, TValue&gt;? dictionary = null;
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                return dictionary;
            }
            valueConverter ??= (JsonConverter&lt;TValue&gt;)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&lt;Point, TValue&gt; value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        JsonConverter&lt;TValue&gt;? valueConverter = null;
        foreach (var (k, v) in value)
        {
            valueConverter ??= (JsonConverter&lt;TValue&gt;)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#之OpenFileDialog解讀(打開文件對話框)

    c#之OpenFileDialog解讀(打開文件對話框)

    這篇文章主要介紹了c#之OpenFileDialog(打開文件對話框),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • C#使用Enum.TryParse()實現(xiàn)枚舉安全轉換

    C#使用Enum.TryParse()實現(xiàn)枚舉安全轉換

    這篇文章介紹了C#使用Enum.TryParse()實現(xiàn)枚舉安全轉換的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-08-08
  • Unity利用材質自發(fā)光實現(xiàn)物體閃爍

    Unity利用材質自發(fā)光實現(xiàn)物體閃爍

    這篇文章主要為大家詳細介紹了Unity利用材質自發(fā)光實現(xiàn)物體閃爍,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-04-04
  • C#實現(xiàn)解壓GZip文件的方法

    C#實現(xiàn)解壓GZip文件的方法

    這篇文章主要介紹了C#實現(xiàn)解壓GZip文件的方法,涉及C#操作壓縮文件的技巧,需要的朋友可以參考下
    2015-05-05
  • C#實現(xiàn)獲取Excel中圖片所在坐標位置

    C#實現(xiàn)獲取Excel中圖片所在坐標位置

    本文以C#和vb.net代碼示例展示如何來獲取Excel工作表中圖片的坐標位置,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下
    2022-04-04
  • c#中l(wèi)ist.FindAll與for循環(huán)的性能對比總結

    c#中l(wèi)ist.FindAll與for循環(huán)的性能對比總結

    這篇文章主要給大家總結介紹了關于c#中l(wèi)ist.FindAll與for循環(huán)的性能,文中通過詳細的示例代碼給大家介紹了這兩者之間的性能,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。
    2017-10-10
  • 詳解C#App.config和Web.config加密

    詳解C#App.config和Web.config加密

    本篇文章給大家分享了C#App.config和Web.config加密的相關知識點以及具體代碼步驟,有興趣的朋友參考學習下。
    2018-05-05
  • Unity實現(xiàn)大轉盤的簡單筆記

    Unity實現(xiàn)大轉盤的簡單筆記

    這篇文章主要為大家分享了Unity實現(xiàn)大轉盤的簡單筆記,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-02-02
  • C#編程總結(一)序列化總結

    C#編程總結(一)序列化總結

    本篇主要介紹了C#序列化總結,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-12-12
  • WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享

    WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享

    最近在網(wǎng)上看到了新版的360安全衛(wèi)士,感覺界面還不錯,于是用WPF制作了一個,時間有限,一些具體的控件沒有制作,用圖片代替了。感興趣的朋友一起跟著小編學習WPF實現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享
    2015-09-09

最新評論