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);
}
从上面的代码可能大概能看出一些实现,重写了默认的 Equals
和 GetHashCode
,并重载了“==” 运算符,并且实现了一个从 StringValueDictionary
到 Dictionary
的隐式转换,来看下面的实现代码:
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(复制、克隆)的目标
实现相等性比较的时候,
Equals
和GetHashCode
方法也要重写,如果没有重写GetHashCode
,编译器也会给出警告,如果没有重写GetHashCode
在实际在HashSet
或者Dictionary
里可能会出现重复key
重载运算符的时候需要一个静态方法,"==" 和 "!=" 是一对操作运算符,如果要实现两个都要实现,不能只实现其中一个
implicit 也算是一个特殊的运算符,巧妙的使用隐式转换可以大大简化代码的写法,
StackExchange.Redis
中就使用了 implicit 来实现RedisValue
和 string 等其他常用类型的隐式转换
References
https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Models/StringValueDictionary.cs
https://github.com/WeihanLi/WeihanLi.Common/blob/dev/test/WeihanLi.Common.Test/ModelsTest/StringValueDictionaryTest.cs