【问题标题】:IEqualityComparer for Value Objects值对象的 IEqualityComparer
【发布时间】:2009-10-14 11:18:01
【问题描述】:

我有一个不可变的值对象,IPathwayModule,它的值定义为:

  • (int) 块;
  • (实体)模块,由(字符串)ModuleId 标识;
  • (枚举)状态;和
  • (实体)类,由(字符串)ClassId 标识 - 可以为 null。

这是我当前的 IEqualityComparer 实现,它似乎在一些单元测试中有效。但是,我认为我不了解自己做得好到足以知道自己是否做得对。以前的实现有时会在重复测试运行时失败。

private class StandardPathwayModuleComparer : IEqualityComparer<IPathwayModule>
{
    public bool Equals(IPathwayModule x, IPathwayModule y)
    {
        int hx = GetHashCode(x);
        int hy = GetHashCode(y);
        return hx == hy;
    }

    public int GetHashCode(IPathwayModule obj)
    {
        int h;
        if (obj.Class != null)
        {
            h = obj.Block.GetHashCode() + obj.Module.ModuleId.GetHashCode() + obj.Status.GetHashCode() + obj.Class.ClassId.GetHashCode();
        }
        else
        {
            h = obj.Block.GetHashCode() + obj.Module.ModuleId.GetHashCode() + obj.Status.GetHashCode() + "NOCLASS".GetHashCode();
        }
        return h;
    }
}

IPathwayModule 绝对是不可变的,具有相同值的不同实例应该相等并产生相同的 HashCode,因为它们被用作 HashSets 中的项目。

我想我的问题是:

  • 在这种情况下我是否正确使用了界面?
  • 是否存在我可能看不到所需行为的情况?
  • 有什么方法可以提高鲁棒性和性能吗?
  • 有没有我没有遵循的良好做法?

【问题讨论】:

    标签: c# equality hashset gethashcode


    【解决方案1】:

    不要根据哈希函数的结果做 Equals,它太脆弱了。而是对每个字段进行字段值比较。比如:

    return x != null && y != null && x.Name.Equals(y.Name) && x.Type.Equals(y.Type) ...
    

    此外,散列函数的结果并不适合相加。尝试改用^ 运算符。

    return obj.Name.GetHashCode() ^ obj.Type.GetHashCode() ...
    

    您不需要在 GetHashCode 中进行 null 检查。如果该值为 null,那么您将遇到更大的问题,尝试从您无法控制的事物中恢复是没有用的...

    【讨论】:

    • 你能再解释一下你的最后一点吗?我需要生成一个基于 .Class.ClassId 的 HashCode,但前提是 .Class 不为空。
    • 我是从道德权威的角度出发的,所以请随意忽略 :) 如果你想在你的集合中存储空值(在我看来这是一个糟糕的设计,这就是重点我含蓄地反对)那么你将不得不在这里做一个空检查。但是对于 null 的所有值,您应该返回相同的值,0 是最简单的。
    • 在我的领域中,null 似乎是有道理的。例如,如果 Dog 没有 Home,那么 Dog.Home == null 有什么问题?
    • Dog.Home 可能为空,是的,但是所有 Home 的集合中是否应该有一个空 Home?我谈论集合的原因是 IEqualityComparer 的主要用例是应用于集合,我认为更具体地应用于无类型集合。
    • 在我的域中,Pathway 是 IPathwayModule 的 (Hash)Set,而 Pathway 的集合中永远不会有 null 项。 HashSet 使用 IEqualityComparer 来确定其项目的相等性,从而防止将重复项添加到集合中。
    【解决方案2】:

    唯一的大问题是 Equals 的实现。哈希码不是唯一的,您可以为不同的对象获得相同的哈希码。您应该单独比较 IPathwayModule 的每个字段。

    GetHashCode() 可以稍微改进一下。您不需要在 int 上调用 GetHashCode()。 int 本身就是一个很好的哈希码。枚举值也是如此。然后可以像这样实现您的 GetHashCode:

    public int GetHashCode(IPathwayModule obj)
    {
        unchecked {
            int h = obj.Block + obj.Module.ModeleId.GetHashCode() + (int) obj.Status;
            if (obj.class != null)
               h += obj.Class.ClassId.GetHashCode();
            return h;
        }
    }
    

    'unchecked' 块是必要的,因为算术运算中可能存在溢出。

    【讨论】:

      【解决方案3】:

      您不应该使用 GetHashCode() 作为比较对象的主要方式。逐个进行比较。

      可能有多个对象具有相同的哈希码(这称为“哈希码冲突”)。

      另外,将多个整数值相加时要小心,因为很容易导致溢出异常。使用 'exclusive or' (^) 组合哈希码或将代码包装到 'unchecked' 块中。

      【讨论】:

        【解决方案4】:

        您应该实现更好的 Equals 和 GetHashCode 版本。

        例如,枚举的哈希码就是它们的数值。

        换句话说,使用这两个枚举:

        public enum A { x, y, z }
        public enum B { k, l, m }
        

        然后用你的实现,下面的值类型:

        public struct AB {
            public A;
            public B;
        }
        

        以下两个值将被视为相等:

        AB ab1 = new AB { A = A.x, B = B.m };
        AB ab2 = new AB { A = A.z, B = B.k };
        

        我假设你不想要那个。

        此外,将值类型作为接口传递会将它们装箱,这可能会带来性能问题,尽管可能并不多。您可以考虑让 IEqualityComparer 实现直接采用您的值类型。

        【讨论】:

        • IPathwayModule 和从它派生的那些类被实现为类,而不是出于性能原因的结构。在这种情况下,您对拳击的表现问题仍然有效吗?
        • 嗯,你说的是“价值对象”。我在想“值类型”,这意味着结构。
        • 我使用的是 DDD 意义上的“值对象”。将它们实现为类似乎是一种规范,前提是相等性覆盖以像值类型一样工作。
        【解决方案5】:
        1. 假设两个对象是相等的,因为它们的哈希码相等是错误的。您需要单独比较所有成员
        2. 使用 ^ 而不是 + 组合哈希码可能更好。

        【讨论】:

          【解决方案6】:

          如果我对您的理解很好,您希望听到一些关于您的代码的 cmets。这是我的评论:

          1. GetHashCode 应该一起异或,而不是添加。 XOR (^) 可以更好地防止碰撞
          2. 您比较哈希码。这很好,但只有在底层对象覆盖GetHashCode 时才这样做。如果没有,请使用属性及其哈希码并将它们组合起来。
          3. 哈希码很重要,它们可以进行快速比较。但是如果哈希码相等,对象仍然可以不同。这种情况很少发生。但如果哈希码相等,您需要比较对象的字段。
          4. 您说您的值类型是不可变的,但您引用的对象 (.Class) 不是不可变的
          5. 始终通过添加参考比较作为第一个测试来优化比较。引用不等,对象不等,结构不等。

          第 5 点取决于您是否希望您在值类型中引用的对象在不同引用时返回不相等。

          编辑:你比较了很多字符串。字符串比较在 C# 中进行了优化。正如其他人建议的那样,您可以在比较中更好地使用==。对于 GetHashCode,也可以按照其他人的建议使用 OR ^

          【讨论】:

          • 4.出于性能原因,IPathwayModule 被实现为一个类(引用类型)。是的,IPathwayModule 的值确实取决于引用对象(例如 Class、Module),但它取决于这些实体的身份字段,可以假定它们是不变的。 5.会做。由于 IPathwayModule 是作为一个类实现的,ReferenceEquals 将立即暗示相等。此外,具有相同值的两个不同引用应该是 .Equals() 并且理想情况下将共享相同的 HashCode 以确保任何 HashSet 不包含重复项。
          • 不,不要或哈希码。只有其中几个,你所有的哈希码都是 0xffffffff。使用异或
          • 这就是我的意思,感谢您指出。请参阅我的编辑。我会说得更清楚。
          【解决方案7】:

          感谢所有回复的人。我汇总了所有回复者的反馈,我改进后的IEqualityComparer 现在看起来像:

          private class StandardPathwayModuleComparer : IEqualityComparer<IPathwayModule>
          {
              public bool Equals(IPathwayModule x, IPathwayModule y)
              {
                  if (x == y) return true;
                  if (x == null || y == null) return false;
          
                  if ((x.Class == null) ^ (y.Class == null)) return false;
          
                  if (x.Class == null) //and implicitly y.Class == null
                  {
                      return x.Block.Equals(y.Block) && x.Status.Equals(y.Status) && x.Module.ModuleId.Equals(y.Module.ModuleId);
                  }
                  return x.Block.Equals(y.Block) && x.Status.Equals(y.Status) && x.Module.ModuleId.Equals(y.Module.ModuleId) && x.Class.ClassId.Equals(y.Class.ClassId);
              }
              public int GetHashCode(IPathwayModule obj)
              {
                  unchecked {
                      int h = obj.Block ^ obj.Module.ModuleId.GetHashCode() ^ (int) obj.Status;
                      if (obj.Class != null)
                      {
                         h ^= obj.Class.ClassId.GetHashCode();
                      }
                      return h;
                  }
              }
          }
          

          【讨论】:

          • 您可以将您的x/y.Class == null 测试重写为(x.Class ?? y.Class) == null,这可以稍微简洁和清晰(?? 是空合并运算符,如果不为空则返回左操作数,否则返回右操作数) .
          • 另外还有一个错误:你先测试x.Class,如果x为null就会抛出异常。稍后您测试x == null。如果x 确实是null,则永远不会到达此代码。在你做任何事情之前,测试null:如果其中一个是null,它们是不相等的。这也会重写您的 ReferenceEquals 测试:if(x == y) return true; elseif(x == null || y == null) return false;
          • 但是 x.Class 不为 null 并且 y.Class 为 null 的情况并没有被这个表达式捕获。
          • 谢谢 Abel,我已在您的第二条评论中进行了更改。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-23
          • 2017-10-13
          相关资源
          最近更新 更多