2012-08-07 65 views
5

阿罗哈,HashSet的<T> .RemoveWhere()和GetHashCode()

这里有一个简单的类,它覆盖的GetHashCode:

class OverridesGetHashCode 
{ 
    public string Text { get; set; } 

    public override int GetHashCode() 
    { 
     return (Text != null ? Text.GetHashCode() : 0); 
    } 
    // overriding Equals() doesn't change anything, so I'll leave it out for brevity 
} 

当我创建一个类的实例,将其添加到HashSet,然后改变它的Text属性,例如:

var hashset = new HashSet<OverridesGetHashCode>(); 
var oghc = new OverridesGetHashCode { Text = "1" }; 
hashset.Add(oghc); 
oghc.Text = "2"; 

那么这不起作用:

var removedCount = hashset.RemoveWhere(c => ReferenceEquals(c, oghc)); 
// fails, nothing is removed 
Assert.IsTrue(removedCount == 1); 

而且也没有这样的:

// this line works, i.e. it does find a single item matching the predicate 
var existing = hashset.Single(c => ReferenceEquals(c, oghc)); 
// but this fails; nothing is removed again 
var removed = hashset.Remove(existing); 
Assert.IsTrue(removed); 

我猜它在内部使用时,项目被插入,如果这是真的,这是 理解的hashset.Contains(oghc)不工作时产生的哈希值。 我也猜测它通过它的哈希代码查找项目,如果它找到匹配项,只有它检查谓词,这可能是第一次测试失败的原因(再次,我只是在这里猜测)。 但是为什么最后一次测试失败,我只是从哈希集合中取出那个对象?我是否错过了某些东西,这是从HashSet中移除某些东西的错误方法吗?

感谢您花时间阅读本文。

UPDATE:为了避免混淆,这里的equals()方法:

protected bool Equals(OverridesGetHashCode other) 
    { 
     return string.Equals(Text, other.Text); 
    } 

public override bool Equals(object obj) 
    { 
     if (ReferenceEquals(null, obj)) return false; 
     if (ReferenceEquals(this, obj)) return true; 
     if (obj.GetType() != this.GetType()) return false; 
     return Equals((OverridesGetHashCode) obj); 
    } 
+0

您应该看看Eric Lippert的[GetHashCode准则和规则](http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for- gethashcode.aspx)特别是规则* GetHashCode返回的整数必须永远不会更改,而对象包含在依赖于哈希码保持稳定*的数据结构中。 – 2012-08-07 15:12:47

+0

我首先想到这是一个很好的问题,现在我觉得我问了一些非常愚蠢的东西:)一段时间后,这一切都有意义,它刚开始时感觉不符合直觉。 '我以前从未使用HashSet'是我能想出的最佳借口:D谢谢你。 – 2012-08-07 15:26:44

回答

2

这里有很好的答案,只是想补充一点。如果你看一下反编译代码HashSet<T>,你会看到Add(value)执行以下操作:

  1. 呼吁IEqualityComparer<T>.GetHashCode()以获取价值的哈希码。对于默认的比较器,这可以归结为GetHashCode()
  2. 使用该散列码来计算应该存储哪个“存储桶”和“槽”值(参考值)。
  3. 存储参考。

当您拨打Remove(value)时,它会执行步骤1.和2.再次查找引用的位置。然后它调用IEqualityComparer<T>.Equals()以确保它确实找到了正确的值。但是,由于您已更改GetHashCode()返回的内容,因此它会计算不同的存储桶/插槽位置,该位置无效。因此,它找不到该对象。

所以,请注意,Equals()这里并不真正起作用,因为如果哈希码更改,它甚至不会到达正确的桶/槽位置。

4

通过改变你的对象的哈希码,而对象是在HashSet使用是违反HashSet的合同。

无法删除对象在这里不是问题。 您不允许首先更改哈希码。

让我从MSDN引述如下:

GetHashCode方法的对象必须一致只要没有修改的对象状态 确定返回值返回相同 散列码对象的Equals方法。请注意, 只适用于当前执行的应用程序和 ,如果应用程序再次运行 ,则可以返回不同的散列码。

他们讲故事有点不同,但本质是一样的。他们说,哈希码从来没有的变化。在实践中,只要确保没有人再使用旧的哈希码,就可以对其进行更改。这不是好的做法,但它有效。

+1

它可能会被认为是对对象状态进行了*修改,以确定对象的Equals方法的返回值* – 2012-08-07 14:56:46

+0

编辑后的引用在这里是一个完全不同的问题。具有相同数据的对象应该返回相同的哈希码,但由于对象现在具有不同的数据,因此有权返回不同的哈希码(它*不应该在变异后返回相同的哈希码)。 – Servy 2012-08-07 14:56:48

+0

@Usr“,只要没有修改确定对象Equals的返回值的对象状态。假定,如果基于Text的值比较对象是否相等,则GetHashCode()以返回一个基于Text值的值,即使Text可能会发生变化 – drch 2012-08-07 14:58:19

4

添加到基于哈希的表(HashSet,Dictionary等)中的任何项目一旦插入到结构中(至少在它们被移除之前),都不会被修改。

要在数据结构中查找一个对象,它会计算它的哈希码,然后根据该哈希码找到一个位置。如果你改变了那个对象,那么它返回的哈希代码不再反映它在数据结构中的当前位置(除非你非常非常幸运,它恰好是一个哈希碰撞)。

MSDN page for Dictionary是说:

只要一个对象被用作Dictionary<TKey, TValue>的关键,它必须在不影响其散列值任何方式更改。

这个相同的断言也适用于HashSet,因为它们都是使用散列表实现的。

+0

是的在上面的例子中,如果你做了hashset.RemoveWhere(x => true),它仍然不会删除任何东西。谓词是真实的,但哈希集找不到该对象。 – drch 2012-08-07 15:04:03