在 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 的两个项目的结果相同,那么,只有这样,它才会调用 EqualsEquals 的实现将需要比较列表中的项目。像这样:

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());
  }
}