在 C# 中避免自定义类型的 HashSet 中的重复项
Avoiding duplicates in a HashSet of custom types in C#
我有以下来自 Tuple 的自定义 class:
public class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
}
和一个HashSet<CustomTuple>
。问题是当我将项目添加到集合中时,它们不会被识别为重复项。即输出 2,但它应该输出 1:
void Main()
{
HashSet<CustomTuple> set = new HashSet<CustomTuple>();
var a = new CustomTuple(new List<string>(), new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
Console.Write(set.Count); // Outputs 2
}
如何重写 Equals 和 GetHashCode 方法以使此代码输出设置计数 1?
您应该覆盖 System.Object class.
中定义的 GetHashCode 和 Equals 虚拟方法
请记住:
如果两个对象在逻辑上是 "equal" 那么它们必须具有相同的哈希码!
如果两个对象具有相同的散列码,则不强制要求对象相等。
此外,我注意到您的代码中存在架构问题:
List 是一种可变类型,但重写 Equals 和 GetHashCode 通常会使您的 class 在逻辑上表现得像一个值类型。因此,拥有 "Item1" 可变类型并表现得像值类型是非常危险的。我建议用 ReadOnlyCollection 替换您的 List 。然后你必须创建一个方法来检查两个 ReadOnlyCollections 是否相等。
对于 GetHashCode () 方法,只需将在 Item1 中找到的所有字符串项组成一个字符串,然后附加一个表示日期时间的哈希码的字符串,最后调用 "GetHashCode ()" 覆盖字符串的串联结果方法。所以通常你会:
override int GetHashCode () {
return (GetHashCodeForList (Item1) + (Item2 ?? DateTime.MinValue).GetHashCode ()).GetHashCode ();
}
GetHashCodeForList 方法将是这样的:
private string GetHashCodeForList (IEnumerable <string> lst) {
if (lst == null) return string.Empty;
StringBuilder sb = new StringBuilder ();
foreach (var item in lst) {
sb.Append (item);
}
return sb.ToString ();
}
最后说明:您可以缓存 GetHashCode 结果,因为获取它的成本相对较高,而且您的整个 class 将变得不可变(如果您将 List 替换为只读集合)。
A HashSet<T>
将首先调用 GetHashCode
,因此您需要先处理它。有关实现,请参阅此答案:
一个简单、幼稚的实现可能如下所示:
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + this.Item2.GetHashCode();
foreach (var s in this.Item1)
{
hash = hash * 23 + s.GetHashCode();
}
return hash;
}
}
但是,如果您的列表很长,那么这可能不够有效。因此,您必须根据您对碰撞的容忍度来决定在何处妥协。
如果 GetHashCode
的两个项目的结果相同,那么,只有这样,它才会调用 Equals
。 Equals
的实现将需要比较列表中的项目。像这样:
public override bool Equals(object o1)
{
var o = o1 as CustomTuple;
if (o == null)
{
return false;
}
if (Item2 != o.Item2)
{
return false;
}
if (Item1.Count() != o.Item1.Count())
{
return false;
}
for (int i=0; i < Item1.Count(); i++)
{
if (Item1[i] != o.Item1[i])
{
return false;
}
}
return true;
}
请注意,我们首先检查日期 (Item2
),因为这样比较便宜。如果日期不一样,我们就不用理会其他任何事情。接下来我们检查两个集合 (Item1
) 上的 Count
。如果它们不匹配,则迭代集合毫无意义。然后我们遍历两个集合并比较每个项目。一旦我们找到一个不匹配的,我们 return false
因为没有必要继续寻找。
正如 George 的回答中所指出的,您还存在列表可变的问题,这会导致您的 HashSet
出现问题,例如:
var a = new CustomTuple(new List<string>() {"foo"} , new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
// Hashset now has two entries
((List<string>)a.Item1).Add("foo");
// Hashset still has two entries, but they are now identical.
要解决这个问题,您需要强制 IEnumerable<string>
为只读。你可以这样做:
public class CustomTuple : Tuple<IReadOnlyList<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList().AsReadOnly(), time)
{
}
public override bool Equals(object o1)
{
// as above
}
public override int GetHashCode()
{
// as above
}
}
这就是我想要的,它根据需要输出 1:
private class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var that = (CustomTuple) obj;
if (Item1 == null && that.Item1 != null || Item1 != null && that.Item1 == null) return false;
if (Item2 == null && that.Item2 != null || Item2 != null && that.Item2 == null) return false;
if (!Item2.Equals(that.Item2)) return false;
if (that.Item1.Count != Item1.Count) return false;
for (int i = 0; i < Item1.Count; i++)
{
if (!Item1[i].Equals(that.Item1[i])) return false;
}
return true;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash*23 + Item2.GetHashCode();
return Item1.Aggregate(hash, (current, s) => current*23 + s.GetHashCode());
}
}
我有以下来自 Tuple 的自定义 class:
public class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
}
和一个HashSet<CustomTuple>
。问题是当我将项目添加到集合中时,它们不会被识别为重复项。即输出 2,但它应该输出 1:
void Main()
{
HashSet<CustomTuple> set = new HashSet<CustomTuple>();
var a = new CustomTuple(new List<string>(), new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
Console.Write(set.Count); // Outputs 2
}
如何重写 Equals 和 GetHashCode 方法以使此代码输出设置计数 1?
您应该覆盖 System.Object class.
中定义的 GetHashCode 和 Equals 虚拟方法请记住:
如果两个对象在逻辑上是 "equal" 那么它们必须具有相同的哈希码!
如果两个对象具有相同的散列码,则不强制要求对象相等。
此外,我注意到您的代码中存在架构问题: List 是一种可变类型,但重写 Equals 和 GetHashCode 通常会使您的 class 在逻辑上表现得像一个值类型。因此,拥有 "Item1" 可变类型并表现得像值类型是非常危险的。我建议用 ReadOnlyCollection 替换您的 List 。然后你必须创建一个方法来检查两个 ReadOnlyCollections 是否相等。
对于 GetHashCode () 方法,只需将在 Item1 中找到的所有字符串项组成一个字符串,然后附加一个表示日期时间的哈希码的字符串,最后调用 "GetHashCode ()" 覆盖字符串的串联结果方法。所以通常你会:
override int GetHashCode () {
return (GetHashCodeForList (Item1) + (Item2 ?? DateTime.MinValue).GetHashCode ()).GetHashCode ();
}
GetHashCodeForList 方法将是这样的:
private string GetHashCodeForList (IEnumerable <string> lst) {
if (lst == null) return string.Empty;
StringBuilder sb = new StringBuilder ();
foreach (var item in lst) {
sb.Append (item);
}
return sb.ToString ();
}
最后说明:您可以缓存 GetHashCode 结果,因为获取它的成本相对较高,而且您的整个 class 将变得不可变(如果您将 List 替换为只读集合)。
A HashSet<T>
将首先调用 GetHashCode
,因此您需要先处理它。有关实现,请参阅此答案:
一个简单、幼稚的实现可能如下所示:
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + this.Item2.GetHashCode();
foreach (var s in this.Item1)
{
hash = hash * 23 + s.GetHashCode();
}
return hash;
}
}
但是,如果您的列表很长,那么这可能不够有效。因此,您必须根据您对碰撞的容忍度来决定在何处妥协。
如果 GetHashCode
的两个项目的结果相同,那么,只有这样,它才会调用 Equals
。 Equals
的实现将需要比较列表中的项目。像这样:
public override bool Equals(object o1)
{
var o = o1 as CustomTuple;
if (o == null)
{
return false;
}
if (Item2 != o.Item2)
{
return false;
}
if (Item1.Count() != o.Item1.Count())
{
return false;
}
for (int i=0; i < Item1.Count(); i++)
{
if (Item1[i] != o.Item1[i])
{
return false;
}
}
return true;
}
请注意,我们首先检查日期 (Item2
),因为这样比较便宜。如果日期不一样,我们就不用理会其他任何事情。接下来我们检查两个集合 (Item1
) 上的 Count
。如果它们不匹配,则迭代集合毫无意义。然后我们遍历两个集合并比较每个项目。一旦我们找到一个不匹配的,我们 return false
因为没有必要继续寻找。
正如 George 的回答中所指出的,您还存在列表可变的问题,这会导致您的 HashSet
出现问题,例如:
var a = new CustomTuple(new List<string>() {"foo"} , new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
// Hashset now has two entries
((List<string>)a.Item1).Add("foo");
// Hashset still has two entries, but they are now identical.
要解决这个问题,您需要强制 IEnumerable<string>
为只读。你可以这样做:
public class CustomTuple : Tuple<IReadOnlyList<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList().AsReadOnly(), time)
{
}
public override bool Equals(object o1)
{
// as above
}
public override int GetHashCode()
{
// as above
}
}
这就是我想要的,它根据需要输出 1:
private class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var that = (CustomTuple) obj;
if (Item1 == null && that.Item1 != null || Item1 != null && that.Item1 == null) return false;
if (Item2 == null && that.Item2 != null || Item2 != null && that.Item2 == null) return false;
if (!Item2.Equals(that.Item2)) return false;
if (that.Item1.Count != Item1.Count) return false;
for (int i = 0; i < Item1.Count; i++)
{
if (!Item1[i].Equals(that.Item1[i])) return false;
}
return true;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash*23 + Item2.GetHashCode();
return Item1.Aggregate(hash, (current, s) => current*23 + s.GetHashCode());
}
}