如何识别GetHashCode的错误实现
本文关键字:GetHashCode 错误 实现 识别 何识别 | 更新日期: 2023-09-27 18:21:37
我有一个GetHashCode的实现,我认为它相当健壮,但老实说,我是从互联网深处挖掘出来的,虽然我理解所写的内容,但我觉得没有资格将其描述为GetHashCode"好"或"坏"的实现。
我在StackOverflow上读了很多关于GetHashCode的文章。有没有一个例子说明为什么在NHibernate中应该覆盖Equals/GetHashCode?我认为这个帖子可能是最好的信息来源,但它仍然让我感到疑惑。
考虑以下实体及其Equals和GetHashCode的给定实现:
public class Playlist : IAbstractDomainEntity
{
public Guid Id { get; set; }
public string Title { get; set;
public Stream Stream { get; set; }
// Use interfaces so NHibernate can inject with its own collection implementation.
public IList<PlaylistItem> Items { get; set; }
public PlaylistItem FirstItem { get; set; }
public Playlist NextPlaylist { get; set; }
public Playlist PreviousPlaylist { get; set; }
private int? _oldHashCode;
public override int GetHashCode()
{
// Once we have a hash code we'll never change it
if (_oldHashCode.HasValue)
return _oldHashCode.Value;
bool thisIsTransient = Equals(Id, Guid.Empty);
// When this instance is transient, we use the base GetHashCode()
// and remember it, so an instance can NEVER change its hash code.
if (thisIsTransient)
{
_oldHashCode = base.GetHashCode();
return _oldHashCode.Value;
}
return Id.GetHashCode();
}
public override bool Equals(object obj)
{
Playlist other = obj as Playlist;
if (other == null)
return false;
// handle the case of comparing two NEW objects
bool otherIsTransient = Equals(other.Id, Guid.Empty);
bool thisIsTransient = Equals(Id, Guid.Empty);
if (otherIsTransient && thisIsTransient)
return ReferenceEquals(other, this);
return other.Id.Equals(Id);
}
}
在这个实施方案中吹嘘的安全检查数量似乎太多了。它激发了我的信心——假设写这篇文章的人比我了解更多的角落案例——但也让我想知道为什么我看到这么多简单的实现。
为什么在重写Equals方法时重写GetHashCode很重要?看看所有这些不同的实现。以下是一个简单但评价很高的实现:
public override int GetHashCode()
{
return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}
这个实现会比我提供的更好还是更差?为什么?
两者同等有效吗?在实现GetHashCode时,是否应该遵循标准的"准则"?上述实施是否存在明显缺陷?如何创建测试用例来验证GetHashCode的实现?
GetHashCode
应该与类/环境的"相等"概念相匹配(除了在容器中保持恒定和快速)。
在正常情况下,"相等"是比较相应对象的所有字段(值类型比较)。在这种情况下,以某种方式合并所有字段的哈希代码的简单实现就足够了。
我的理解是,在NHibernate的情况下,"相等"明显更棘手,因此您会看到复杂的实现。我认为这主要是因为一些对象属性可能还不可用——在这种情况下,比较对象的"身份"就足够了。
不幸的是,尽管等式测试方法可以对任何一对对象引用X
和Y
提出两个不同的问题,但只有一个Equals
方法和一个GetHashCode
方法。
-
假设
X
和Y
是相同的类型(*),那么X
的所有成员的行为是否总是与Y
的相应方法相同?在这个定义下,对不同数组的两个引用将被报告为不相等,即使它们包含匹配的元素,因为即使它们的元素在某个时刻是相同的,也可能并不总是如此。 -
假设
X
和Y
属于同一类型(*),是否会同时将对对象X
的所有引用替换为对对象Y
的引用,反之亦然,从而影响除基于身份的GetHashCode
函数之外的任何其他成员?在此定义下,对元素匹配的两个不同数组的引用将被报告为相等。
(*)一般而言,不同类型的对象应报告不平等。在某些情况下,如果所有可以访问私有类的代码都只在匹配的公共类型中存储引用,那么从同一个公共类继承的不同私有类的对象应该被视为相等的,但这最多只是一个非常狭窄的例外。
有些情况需要问第一个问题,有些情况则需要问第二个问题;Equals
和GetHashCode
的默认Object
实现回答第一个,而默认ValueType
实现回答第二个。不幸的是,选择哪种比较方法适合给定的引用是如何使用引用的函数,而不是引用实例类型的函数。如果两个对象持有对集合的引用,而这些引用既不会发生变异,也不会暴露给可能发生变异的代码,为了封装其中的内容,持有这些引用的对象的相等性应该取决于集合的内容,而不是它们的身份。
看起来代码有时会以第一个问题更合适的方式使用PlayList
类型的实例,有时则以第二个问题更适合的方式使用。虽然这可能是可行的,但我认为最好有一个通用的数据持有者对象,如果需要,它可以被一个等式检查方法适合一种或另一种用途的对象包装(例如,有一个PlaylistData
对象,它可以由MutablePlaylist
或ImmutablePlaylist
包装)。包装器类可以具有InvalidateAndMakeImmutable
或InvalidateAndMakeMutable
方法,这些方法将使包装器无效并返回对象周围的新包装器(使用包装器将确保系统知道给定的Playlist
引用是否可以暴露给可能使其发生变异的代码)。