C# 實現一個基於值相等性比較的字典

C# 實現一個基於值相等性比較的字典

Intro

今天在項目里遇到一個需求,大概是這樣的我要比較兩個 JSON 字元串是不是相等,JSON 字元串其實是一個 Dictionary<string, string> 但是順序可能不同,和上一篇 record 使用場景 中的第一個需求類似,前面我們介紹過使用 record 可以比較方便的解決,但是我們的項目是 .netcoreapp3.1 的,不能使用 record,如何比較方便的比較呢?我們能否自己實現一個類似於 record 的類型,基於值去比較呢?於是就有了本文的探索

StringValueDictioanry

實現了一個基於值進行比較的字典,實現程式碼如下,實現的比較簡單,涉及到一些簡單的知識點,平時不怎麼用已經忘了怎麼寫了,通過寫下面的程式碼又學習了一下

先來看測試程式碼吧,測試程式碼如下:

[Fact]
public void EqualsTest()
{
    var abc = new { Id = 1, Name = "Tom" };
    var dic1 = StringValueDictionary.FromObject(abc);
    var dic2 = StringValueDictionary.FromObject(new Dictionary<string, object>()
    {
        {"Name", "Tom" },
        {"Id", 1},
    });

    Assert.True(dic1 == dic2);
    Assert.Equal(dic1, dic2);
}

[Fact]
public void DistinctTest()
{
    var abc = new { Id = 1, Name = "Tom" };
    var dic1 = StringValueDictionary.FromObject(abc);
    var dic2 = StringValueDictionary.FromObject(new Dictionary<string, object>()
    {
        {"Id", 1},
        {"Name", "Tom" },
    });
    var set = new HashSet<StringValueDictionary>();
    set.Add(dic1);
    set.Add(dic2);

    Assert.Single(set);
}

[Fact]
public void CloneTest()
{
    var dic1 = StringValueDictionary.FromObject(new Dictionary<string, object>()
    {
        {"Id", 1},
        {"Name", "Tom" }
    });
    var dic2 = dic1.Clone();
    Assert.False(ReferenceEquals(dic1, dic2));
    Assert.True(dic1 == dic2);
}

[Fact]
public void ImplicitConvertTest()
{
    var abc = new { Id = 1, Name = "Tom" };
    var stringValueDictionary = StringValueDictionary.FromObject(abc);
    Dictionary<string, string> dictionary = stringValueDictionary;
    Assert.Equal(stringValueDictionary.Count, dictionary.Count);

    var dic2 = StringValueDictionary.FromObject(dictionary);

    Assert.Equal(dic2, stringValueDictionary);
    Assert.True(dic2 == stringValueDictionary);
}

從上面的程式碼可能大概能看出一些實現,重寫了默認的 EqualsGetHashCode,並重載了「==」 運算符,並且實現了一個從 StringValueDictionaryDictionary 的隱式轉換,來看下面的實現程式碼:

public sealed class StringValueDictionary : IEquatable<StringValueDictionary>
{
    private readonly Dictionary<string, string?> _dictionary = new();

    private StringValueDictionary(IDictionary<string, string?> dictionary)
    {
        foreach (var pair in dictionary)
        {
            _dictionary[pair.Key] = pair.Value;
        }
    }

    private StringValueDictionary(StringValueDictionary dictionary)
    {
        foreach (var key in dictionary.Keys)
        {
            _dictionary[key] = dictionary[key];
        }
    }

    public static StringValueDictionary FromObject(object obj)
    {
        if (obj is null) throw new ArgumentNullException(nameof(obj));
        if (obj is IDictionary<string, string?> dictionary)
        {
            return new StringValueDictionary(dictionary);
        }
        if (obj is IDictionary<string, object?> dictionary2)
        {
            return new StringValueDictionary(dictionary2.ToDictionary(p => p.Key, p => p.Value?.ToString()));
        }
        if (obj is StringValueDictionary dictionary3)
        {
            return new StringValueDictionary(dictionary3);
        }
        return new StringValueDictionary(obj.GetType().GetProperties()
            .ToDictionary(p => p.Name, p => p.GetValue(obj)?.ToString()));
    }

    public static StringValueDictionary FromJson(string json)
    {
        Guard.NotNull(json, nameof(json));
        var dic = json.JsonToObject<Dictionary<string, object?>>()
            .ToDictionary(x => x.Key, x => x.Value?.ToString());
        return new StringValueDictionary(dic);
    }

    public StringValueDictionary Clone() => new(this);

    public int Count => _dictionary.Count;

    public bool ContainsKey(string key) => _dictionary.ContainsKey(key) ? _dictionary.ContainsKey(key) : throw new ArgumentOutOfRangeException(nameof(key));

    public string? this[string key] => _dictionary[key];

    public Dictionary<string, string>.KeyCollection Keys => _dictionary.Keys!;

    public bool Equals(StringValueDictionary? other)
    {
        if (other is null) return false;
        if (other.Count != Count) return false;
        foreach (var key in _dictionary.Keys)
        {
            if (!other.ContainsKey(key))
            {
                return false;
            }
            if (_dictionary[key] != other[key])
            {
                return false;
            }
        }
        return true;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as StringValueDictionary);
    }

    public override int GetHashCode()
    {
        var stringBuilder = new StringBuilder();
        foreach (var pair in _dictionary)
        {
            stringBuilder.Append($"{pair.Key}={pair.Value}_");
        }
        return stringBuilder.ToString().GetHashCode();
    }

    public static bool operator ==(StringValueDictionary? current, StringValueDictionary? other)
    {
        return current?.Equals(other) == true;
    }

    public static bool operator !=(StringValueDictionary? current, StringValueDictionary? other)
    {
        return current?.Equals(other) != true;
    }

    public static implicit operator Dictionary<string, string?>(StringValueDictionary dictionary)
    {
        return dictionary._dictionary;
    }
}

More

上述程式碼實現的有點粗糙,可能會有一些問題,僅供參考

以上程式碼基本實現了基於想要的值的相等性比較以及 Clone(複製、克隆)的目標

實現相等性比較的時候,EqualsGetHashCode 方法也要重寫,如果沒有重寫 GetHashCode,編譯器也會給出警告,如果沒有重寫 GetHashCode 在實際在 HashSet 或者 Dictionary 里可能會出現重複 key

重載運算符的時候需要一個靜態方法,”==” 和 “!=” 是一對操作運算符,如果要實現兩個都要實現,不能只實現其中一個

implicit 也算是一個特殊的運算符,巧妙的使用隱式轉換可以大大簡化程式碼的寫法,StackExchange.Redis 中就使用了 implicit 來實現 RedisValue 和 string 等其他常用類型的隱式轉換

References

Tags: