避免在C#中自定义类型的HashSet中出现重复

时间:2015-08-03 15:08:56

标签: c# tuples equals hashset gethashcode

我有以下来自元组的自定义类:

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?

3 个答案:

答案 0 :(得分:1)

您应该覆盖在System.Object类中定义的GetHashCode和Equals虚拟方法。

请记住:

  • 如果两个对象在逻辑上相等&#34;然后他们必须有相同的哈希码!

  • 如果两个对象具有相同的哈希码,则不一定要使对象相等。

另外,我发现代码中存在架构问题: List是一个可变类型,但重写Equals和GetHashCode通常会使您的类在逻辑上表现得像值类型。所以&#34; Item1&#34;一个可变类型,表现得像一个值类型是非常危险的。我建议用ReadOnlyCollection替换你的List。然后你必须创建一个方法来检查两个ReadOnlyCollections是否为Equal。

对于GetHashCode()方法,只需从Item1中找到的所有字符串项组成一个字符串,然后附加一个表示日期时间的哈希代码的字符串,然后最终调用连接结果&#34; GetHashCode()&#34 ;覆盖了字符串方法。通常你会有:

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结果,因为它相对昂贵,并且您的整个类将变为不可变(如果您使用只读集合替换List)。

答案 1 :(得分:0)

HashSet<T>首先会调用GetHashCode,因此您需要首先处理该问题。有关实施,请参阅以下答案:https://stackoverflow.com/a/263416/1250301

所以一个简单,天真的实现可能看起来像这样:

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),因为这很便宜。如果日期不一样,我们不打扰其他任何事情。接下来,我们检查两个集合Count上的Item1。如果它们不匹配,那么迭代集合是没有意义的。然后我们遍历两个集合并比较每个项目。一旦我们找到一个不匹配的,我们就会返回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
    }

}

答案 2 :(得分:0)

这就是我的目标,根据需要输出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());
  }
}