【问题标题】:Avoiding duplicates in a HashSet of custom types in C#避免 C# 中自定义类型的 HashSet 中的重复项
【发布时间】:2015-08-03 15:08:56
【问题描述】:

我有以下从 Tuple 派生的自定义类:

public class CustomTuple : Tuple<List<string>, DateTime?>
{
  public CustomTuple(IEnumerable<string> strings, DateTime? time)
      : base(strings.OrderBy(x => x).ToList(), time)
  {
  }
}

HashSet&lt;CustomTuple&gt;。问题是当我将项目添加到集合时,它们不会被识别为重复项。即这输出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?

【问题讨论】:

  • 您覆盖它们,以便当它们应该相等时,该方法返回 true。
  • 我知道 - 但我尝试的方法不起作用。
  • 你认为什么是平等的?列表中的所有字符串都匹配并且日期匹配?所以在你的equals 方法中这样做。 (我会先检查日期以提高效率)。
  • 是的,我认为这是平等的。
  • 那么你的代码呢?请注意,GetHashCode 的结果必须在 Equals 被调用之前匹配。

标签: c# tuples equals hashset gethashcode


【解决方案1】:

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

请记住:

  • 如果两个对象在逻辑上“相等”,那么它们必须具有相同的哈希码!

  • 如果两个对象具有相同的哈希码,则不必让您的对象相等。

另外,我注意到您的代码中存在架构问题: List 是一种可变类型,但重写 Equals 和 GetHashCode 通常会使您的类在逻辑上表现得像值类型。所以让“Item1”成为一个可变类型并且表现得像一个值类型是非常危险的。我建议用 ReadOnlyCollection 替换您的 List 。然后你必须创建一个方法来检查两个 ReadOnlyCollections 是否相等。

对于 GetHashCode () 方法,只需从 Item1 中找到的所有字符串项组成一个字符串,然后附加一个表示 datetime 哈希码的字符串,然后最后调用在字符串方法上覆盖的“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 结果,因为获取它的成本相对较高,并且您的整个类将变得不可变(如果您将 List 替换为只读集合)。

【讨论】:

  • +1 表示可变性。他们在构造函数中复制列表,但由于它是List&lt;string&gt;,您始终可以将其转换回并添加更多项目。例如 ((List&lt;string&gt;)a.Item1).Add("foo"); 这是你不应该做的事情。
【解决方案2】:

HashSet&lt;T&gt; 将首先调用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,因为继续查找没有意义。

正如乔治的回答中所指出的,你还有一个问题是你的列表是可变的,这会导致你的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&lt;string&gt; 为只读。你可以这样做:

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
    }

}

【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2015-09-12
      • 1970-01-01
      • 1970-01-01
      • 2014-12-30
      • 1970-01-01
      • 2016-12-21
      • 1970-01-01
      • 2020-10-25
      • 1970-01-01
      相关资源
      最近更新 更多