【问题标题】:Implementing GetHashCode for IEqualityComparer<T> with conditional equality使用条件相等为 IEqualityComparer<T> 实现 GetHashCode
【发布时间】:2012-10-23 01:15:35
【问题描述】:

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

我将 intersect 和 except (Linq) 与自定义 IEqualityComparer 一起使用,以查询 ISyncableUser 的两个序列的设置差异和设置交集。

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

两个 ISyncableUser 是否相等背后的逻辑是有条件的。条件围绕 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()?

欢迎提出任何想法!

【问题讨论】:

  • 是否有意使用 string.IsNullOrEmpty 而不是 string.IsNullOrWhiteSpace 检查 userB.UserPrincipalName,就像对 userA.UserPrincipalName 一样?检查应该相同才能有正确的 Equals 方法。
  • 不,抱歉,我直接输入了 SO,那是我的错误。我现在已经更正了。

标签: c# linq set hashcode


【解决方案1】:

如果我没看错,你的等式关系不是传递的。图片如下三个ISyncableUsers:

A { Guid: "1", UserPrincipalName: "2" }
B { Guid: "2", UserPrincipalName: "2" }
C { Guid: "2", UserPrincipalName: "1" }
  • A == B 因为他们有相同的UserPrincipalName
  • B == C 因为他们有相同的Guid
  • A != C 因为他们也不分享。

来自the spec

Equals 方法具有自反性、对称性和传递性。也就是说,如果用于将对象与自身进行比较,则返回truetrue 表示 xy 两个对象,如果 yxtrue ;和 true 对于两个对象 xz 如果它是 true 对于 xy 并且也是 true 对于 @ 987654337@和z

如果你的相等关系不一致,你就无法实现一个哈希码来支持它。

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

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

这样G(g) == U(u) == P(g, u) 对应所有gu。这只有在您完全忽略 gu 时才有可能。

【讨论】:

  • 问题是是否有可能拥有这样的ISyncableUser's,我们不确定它们的名称和 guid 是否可以分配任意值。
  • 嗯,这是一个很好的观点。不过,即使“技术上”不可能得到不一致的结果,我也会很担心。
  • 同意,否则这个问题就太无聊了。 :)
  • 就我要处理的业务逻辑而言,我的 Equal 实现是正确的。确实不一致。这是上下文的。这就是我想的问题。我认为我正在将 Intersect、Except 和 IEqualityComparer 硬塞到一个没有明确设计的场景中。感谢大家的帮助。您刚刚确认了我的想法,但希望我遗漏了什么。
【解决方案2】:

如果我们假设您的 Equals 实现是正确的,即它具有反射性、传递性和对称性,那么您的 GetHashCode 函数的基本实现应该如下所示:

public int GetHashCode(ISyncableUser obj) { if (obj == null) { return SOME_CONSTANT; } if (!string.IsNullOrWhiteSpace(obj.UserPrincipalName) && <can have user object with different guid and the same name>) { 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 是否为空/空的情况?我不确定这是否可行。
  • @SamShiles:我已经编辑了我的答案,您的哈希码的问题是,对于每个对象,您需要知道是否有可能获得另一个具有不同 guid 但名称相同的对象。否则它就行不通了。
【解决方案3】:

一种方法是维护用户名和 GUIDS 的哈希码字典。

  • 您可以在开始时为所有用户生成此字典一次,这可能是最简洁的解决方案。

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

  • 或者,您可以在 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 实现具有相同的限制)

【讨论】:

    猜你喜欢
    • 2011-08-20
    • 1970-01-01
    • 2014-11-08
    • 1970-01-01
    • 2014-09-16
    • 1970-01-01
    • 2012-06-30
    • 2016-10-10
    相关资源
    最近更新 更多