使用条件相等性为IEqualityComparer<T>实现GetHashCode

6

我想知道有没有人对这个问题有任何建议。

我正在使用intersect和except(Linq)以及自定义的IEqualityComparer来查询两个ISyncableUsers序列的集合差异和集合交集。

public interface ISyncableUser
{
    string Guid { get; }
    string UserPrincipalName { get; }
}

判断两个ISyncableUsers是否相等的逻辑是有条件的。这些条件主要围绕着Guid和UserPrincipalName这两个属性是否有值。最好的方法是通过代码来解释这个逻辑。下面是我客户端IEqualityComparer Equals方法的实现:

public bool Equals(ISyncableUser userA, ISyncableUser userB)
{
    if (userA == null && userB == null)
    {
        return true;
    }

    if (userA == null)
    {
        return false;
    }

    if (userB == null)
    {
        return false;
    }

    if ((!string.IsNullOrWhiteSpace(userA.Guid) && !string.IsNullOrWhiteSpace(userB.Guid)) &&
        userA.Guid == userB.Guid)
    {
        return true;
    }

    if (UsersHaveUpn(userA, userB))
    {
        if (userB.UserPrincipalName.Equals(userA.UserPrincipalName, StringComparison.InvariantCultureIgnoreCase))
        {
            return true;
        }
    }
    return false;
}

private bool UsersHaveUpn(ISyncableUser userA, ISyncableUser userB)
{
    return !string.IsNullOrWhiteSpace(userA.UserPrincipalName)
            && !string.IsNullOrWhiteSpace(userB.UserPrincipalName);
}

我遇到的问题是实现GetHashCode,使得上面表示的条件相等被尊重。目前我唯一能让intersect和except方法按预期工作的方法是始终从GetHashCode()返回相同的值,强制调用Equals。

 public int GetHashCode(ISyncableUser obj)
 {
     return 0;
 }

这个方法能够实现,但是性能惩罚非常大,这是意料之中的。我测试过使用非条件相等时的情况,当两个包含50000个对象的集合,用一个合适的哈希码实现可以在约40毫秒内执行拦截和除外操作。而一个始终返回0的哈希码实现需要大约144000毫秒(是的,2.4分钟!)。
那么,在上述场景中,我应该如何实现GetHashCode()呢?
欢迎提出任何想法!

用户B的UserPrincipalName属性使用了string.IsNullOrEmpty进行检查,而不像用户A的UserPrincipalName属性那样使用string.IsNullOrWhiteSpace。这个检查应该是一致的,以正确实现Equals方法。 - Konstantin Oznobihin
不好意思,我直接在SO上输入了,那是我的错误。我已经纠正了。 - Sam Shiles
3个回答

2
如果我理解正确的话,您的相等关系不是传递的。请想象以下三个ISyncableUser
A { Guid: "1", UserPrincipalName: "2" }
B { Guid: "2", UserPrincipalName: "2" }
C { Guid: "2", UserPrincipalName: "1" }
  • A == B,因为它们具有相同的 UserPrincipalName
  • B == C,因为它们具有相同的 Guid
  • A != C,因为它们没有共享的内容。

来自规范文档,

Equals 方法是自反的、对称的和传递的。也就是说,如果使用该方法将对象与自身进行比较,则返回 true;如果 yx 为真,则 xy 的结果也为真;如果 xy 的结果为真,并且 yz 的结果也为真,则 xz 的结果也为真。

如果您的相等关系不一致,那么您无法实现支持该关系的哈希代码。

从另一个角度看:您实际上正在寻找三个功能:

  • G 将 GUID 映射到整数(如果您知道 GUID 但 UPN 为空)
  • U 将 UPN 映射到整数(如果您知道 UPN 但 GUID 为空)
  • P 将(guid,upn)对映射到整数(如果您两者都知道)

使得对于所有的 guG(g) == U(u) == P(g,u)。只有当您完全忽略 gu 时才可能实现这一点。


问题是是否可能拥有这样的ISyncableUser,我们不确定他们的名称和GUID是否可以分配任意值。 - Konstantin Oznobihin
嗯,这是一个非常好的观点。即使在“技术上”不可能得到不一致的结果,我也会感到担忧。 - Rawling
同意,否则这将是一个太无聊的问题 :) - Konstantin Oznobihin
我的Equal实现在涉及到的业务逻辑方面是正确的。但它并不一致,而是具有上下文关联性。这就是问题所在。我想我正在把Intersect、Except和IEqualityComparer<T>强行塞入一个它并没有明确设计的场景中。感谢大家的帮助。你们刚刚证实了我的想法,但我还是希望我错了。 - Sam Shiles

2
如果我们假设你的Equals实现是正确的,即它是反射性的、传递性的和对称的,那么你的GetHashCode函数的基本实现应该如下所示:
``` public int GetHashCode(ISyncableUser obj) { if (obj == null) { return SOME_CONSTANT; }
if (!string.IsNullOrWhiteSpace(obj.UserPrincipalName) && ) { return GetHashCode(obj.UserPrincipalName); }
return GetHashCode(obj.Guid); } ```
你还应该了解到你的对象之间存在相当复杂的依赖关系。
事实上,让我们考虑两个ISyncableUser对象:'u1'和'u2',使得u1.Guid != u2.Guid,但u1.UserPrincipalName == u2.UserPrincipalName并且名称不为空。相等性的要求强制对于任何ISyncableUser对象'u',使得u.Guid == u1.Guid,条件u.UserPrincipalName == u1.UserPrincipalName也应该为真。这种推理决定了GetHashCode的实现,对于每个用户对象,它应该基于它的名称或guid。

我已经更新了我的OP,包含了我的UsersHaveUpn方法。很抱歉,但我不明白如何实现你的HaveUpnWithSomeUser方法。只是检查UPN是否为空吗?我不确定那样会起作用。 - Sam Shiles
@SamShiles:我已经编辑了我的答案,你哈希码的问题在于对于每个对象,你需要知道是否有可能得到另一个具有相同名称但不同 guid 的对象。否则它就行不通。 - Konstantin Oznobihin

0
一种方法是维护一个用户名和GUID的哈希码字典。
  • 你可以在开始时为所有用户生成这个字典,这可能是最清晰的解决方案。

  • 你可以在每个用户的构造函数中添加或更新条目。

  • 或者,你可以在GetHashCode函数内部维护该字典。这意味着你的GetHashCode函数需要更多的工作,并且不是没有副作用的。让它与多个线程或并行linq一起工作需要更加小心的处理。因此,我不知道是否推荐这种方法。

尽管如此,这是我的尝试:

private Dictionary<string, int> _guidHash = 
     new Dictionary<string, int>();

private Dictionary<string, int> _nameHash = 
     new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

public int GetHashCode(ISyncableUser obj)
{
    int hash = 0;

    if (obj==null) return hash;

    if (!String.IsNullOrWhiteSpace(obj.Guid) 
        && _guidHash.TryGetValue(obj.Guid, out hash))
        return hash;

    if (!String.IsNullOrWhiteSpace(obj.UserPrincipalName) 
        && _nameHash.TryGetValue(obj.UserPrincipalName, out hash))
        return hash;

    hash = RuntimeHelpers.GetHashCode(obj); 
    // or use some other method to generate an unique hashcode here

    if (!String.IsNullOrWhiteSpace(obj.Guid)) 
         _guidHash.Add(obj.Guid, hash);

    if (!String.IsNullOrWhiteSpace(obj.UserPrincipalName)) 
         _nameHash.Add(obj.UserPrincipalName, hash);

    return hash;
}

请注意,如果ISyncableUser对象不友好并展示像Rawling答案中的情况,这将失败。我假设具有相同GUID的用户将具有相同的名称或根本没有名称,并且具有相同principalName的用户具有相同的GUID或根本没有GUID。(我认为给定的Equals实现具有相同的限制)

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