C# 實現一個基於值相等性比較的字典
阿新 • • 發佈:2021-01-31
# C# 實現一個基於值相等性比較的字典
## Intro
今天在專案裡遇到一個需求,大概是這樣的我要比較兩個 JSON 字串是不是相等,JSON 字串其實是一個 `Dictionary` 但是順序可能不同,和上一篇 `record 使用場景` 中的第一個需求類似,前面我們介紹過使用 `record` 可以比較方便的解決,但是我們的專案是 `.netcoreapp3.1` 的,不能使用 `record`,如何比較方便的比較呢?我們能否自己實現一個類似於 `record` 的型別,基於值去比較呢?於是就有了本文的探索
## StringValueDictioanry
實現了一個基於值進行比較的字典,實現程式碼如下,實現的比較簡單,涉及到一些簡單的知識點,平時不怎麼用已經忘了怎麼寫了,通過寫下面的程式碼又學習了一下
先來看測試程式碼吧,測試程式碼如下:
``` csharp
[Fact]
public void EqualsTest()
{
var abc = new { Id = 1, Name = "Tom" };
var dic1 = StringValueDictionary.FromObject(abc);
var dic2 = StringValueDictionary.FromObject(new Dictionary()
{
{"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()
{
{"Id", 1},
{"Name", "Tom" },
});
var set = new HashSet();
set.Add(dic1);
set.Add(dic2);
Assert.Single(set);
}
[Fact]
public void CloneTest()
{
var dic1 = StringValueDictionary.FromObject(new Dictionary()
{
{"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 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` 的隱式轉換,來看下面的實現程式碼:
``` csharp
public sealed class StringValueDictionary : IEquatable
{
private readonly Dictionary _dictionary = new();
private StringValueDictionary(IDictionary 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 dictionary)
{
return new StringValueDictionary(dictionary);
}
if (obj is IDictionary 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>()
.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.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(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/StringValueDictionary