HashSet允许插入重复项 - C#

46

这似乎是一个初学者的问题,但我无法找到一个具体回答这个问题的答案。

我有这个类:

public class Quotes{ 
    public string symbol; 
    public string extension
}

我正在使用这个:

HashSet<Quotes> values = new HashSet<Quotes>();

但是我可以多次添加相同的Quotes对象。例如,我的Quotes对象可能具有'symbol'等于'A'和'extension'等于'=n',并且此Quotes对象在HashSet中多次出现(通过调试模式查看HashSet)。我曾经认为在调用时会发生

values.Add(new Quotes(symb, ext));

如果符号和扩展名相同,则返回“false”,元素不会被添加。我有一种感觉,这与 HashSet 在添加新对象时比较 Quotes 对象有关。非常感谢您的帮助!


也许你想看看HashTable,或者更好的是Dictionary<string, string>。 - MethodMan
@jpints14,你使用什么进行哈希?是字符串内容还是内存位置?(或其他) - Adrian
“能够多次添加相同的Quotes对象”是指添加完全相同的实例,还是添加相同的实例? - James Michael Hare
7个回答

65

我猜测您正在创建一个具有相同值的新Quotes。在这种情况下,它们不相等。如果它们应该被视为相等,则需要重写Equals和GetHashCode方法。

public class Quotes{ 
    public string symbol; 
    public string extension

    public override bool Equals(object obj)
    {
        Quotes q = obj as Quotes;
        return q != null && q.symbol == this.symbol && q.extension == this.Extension;
    }

    public override int GetHashCode()
    {
        return this.symbol.GetHashCode() ^ this.extension.GetHashCode();
    }
}

19
请注意,如果符号或扩展可能为空,则 GetHashCode 必须处理该情况并且不会崩溃。 - Eric Lippert
我在比较之前已经做了检查,但还是谢谢你的提示。 - jpints14
6
注意,在字段类型不是 stringint 或其他值类型或密封类时,应该使用 q!= null && q.symbol.Equals(this.symbol)&& q.extension.Equals(this.extension) 而不是使用 ==,因为 == 不具备多态性(如果子类定义一个 operator ==,则基类的 operator == 仍将被使用,而子类可以 覆盖 .Equals() 方法,因此将使用子类的 .Equals())。另外,hash1 ^ hash2 是一个较差的哈希实现,因为 "a", "b""b", "a" 具有相同的哈希值。建议使用类似 (hash1 + 7 * 13) ^ hash2 的东西。 - Suzanne Soy
我刚刚像这个例子一样重写了Equals和GetHashCode,但在我的情况下,仍然会在HashSet中出现重复项。为什么? - Chris

21
我原本认为在使用相同的symb和ext调用values.Add(new Quotes(symb, ext));时,会返回'false'并且不会添加元素。

< p>但实际情况并非如此。

HashSet将使用GetHashCodeEquals来确定对象的相等性。目前,由于您没有在Quotes中重写这些方法,将使用默认的System.Object的引用相等性。每次添加新的Quote时,它都是一个唯一的对象实例,因此HashSet将其视为唯一的对象。

如果您重写Object.EqualsObject.GetHashCode,它将按照您的预期工作。


10

HashSet首先根据GetHashCode计算的哈希值比较条目。默认实现基于对象本身返回一个哈希码(每个实例之间不同)。

仅当哈希值相同时(对于基于实例的哈希值,这种情况非常罕见),才会调用Equals方法并用于明确比较两个对象。

你有两个选择:

  • 将Quotes更改为结构体
  • 在Quotes中重写GetHashCode和Equals方法

示例:

 public override int GetHashCode()
 {
    return (this.symbol == null ? 0 : this.symbol.GetHashCode())
       ^ (this.extension == null ? 0 : this.extension.GetHashCode());
 }
 public override bool Equals(object obj)
 {
    if (Object.ReferenceEquals(this, obj))
      return true;

    Quotes other = obj as Quotes;
    if (Object.ReferenceEquals(other, null))
      return false;

    return String.Equals(obj.symbol, this.symbol)
        && String.Equals(obj.extension, this.extension);
 }

2
你还需要重写 Object.Equals - 哈希值不能保证唯一,因此两种方法都要使用... - Reed Copsey
是的 - 我太专注于快速写出答案了 :-D 我刚刚添加了它,谢谢。 - Matthias
1
mmm - 我认为你的Object.ReferenceEquals检查不太对了... ;) 基本上,按照你的方式,只要“obj”是一个Quotes对象,你就会说它不相等(这也是它可能相等的唯一方式...) - Reed Copsey
啊!这种情况发生在我打字时两个if变成一个的时候...看来是时候休息一下了 :-) - Matthias
1
hash1 ^ hash2 是一种较差的哈希实现,因为 "a", "b""b", "a" 具有相同的哈希值。考虑使用类似 (hash1 + 7 * 13) ^ hash2 的方法。 - ErikE

7

我想更正Kendall的回答中的一些内容(由于某种奇怪的原因无法进行评论)。

return this.symbol.GetHashCode() ^ this.extension.GetHashCode();

请注意,异或函数是一种非常容易发生冲突的方式来组合两个哈希值,特别是当它们都是相同类型时(因为每个对象的符号==扩展将哈希到0)。即使它们不是相同类型或不太可能相等,这也是不好的实践,习惯于使用它可能会在不同的应用中导致问题。
相反,将一个哈希值乘以一个小质数,并加上第二个哈希值,例如:
return 3 * this.symbol.GetHashCode() + this.extension.GetHashCode();

3
我知道这有点晚了,但我遇到了相同的问题,并在实现所选答案时发现了不可接受的性能损失,特别是当你有很多记录时。
我发现使用Hashset和Tuple将其转换为两个步骤会更快,最后通过Select进行转换。
public class Quotes{ 
    public string symbol; 
    public string extension
}

var values = new HashSet<Tuple<string,string>>();

values.Add(new Tuple<string,string>("A","=n"));
values.Add(new Tuple<string,string>("A","=n"));

// values.Count() == 1

values.Select (v => new Quotes{ symbol = v.Item1, extension = v.Item2 });

尝试将其与接受的答案类似的方法进行比较,但同时实现IEquatable<Quotes>,你可能会获得更好的结果。通过进一步调整GetHashCode(),可能会获得更好的结果。 - Jon Hanna

2
Quotes q = new Quotes() { symbol = "GE", extension = "GElec" };
values.Add(q);
values.Add(q);

..正在添加相同的实例两次,第二次将返回false。

values.Add(new Quotes() { symbol = "GE", extension = "GElec" });
values.Add(new Quotes() { symbol = "GE", extension = "GElec" });

...是将两个不同的实例相加,这两个实例恰好具有公共字段相同的值。

如其他地方所述,覆盖Equals和GetHashCode方法将会解决此问题:

public class Quotes { 
    public string symbol; 
    public string extension;

    public override bool Equals(object obj) {
        if (!(obj is Quotes)) { return false; }
        return (this.symbol == ((Quotes)obj).symbol) && 
               (this.extension == ((Quotes)obj).extension);
    }

    public override int GetHashCode() {
        return (this.symbol.GetHashCode()) ^ (this.extension.GetHashCode());
    }
} 

如果您对代码进行逐步调试,您会发现 values.Add 调用了 Quotes.Equals 和 Quotes.GetHashCode。

在你的return (this.symbol.GetHashCode()) ^ (this.extension.GetHashCode());中,符号^是什么意思?这是我第一次看到,这是个打字错误吗? - Niklas

1

我被告知覆盖Equals()和GetHashCode()不是一个好的做法。

类是引用类型,结构体是值类型。改为使用结构体将允许按值进行相等比较,从而使相同的符号/扩展名相等。

public struct Quotes { 
    public string symbol; 
    public string extension;
}

public static void Main()
{
    var hashSet = new HashSet<Quotes>();

    hashSet.Add(new Quotes { symbol = "aaa", extension = "bbb" });
    hashSet.Add(new Quotes { symbol = "aaa", extension = "bbb" });

    Console.WriteLine(hashSet.Count);
}

输出结果为1。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接