不可变ID属性如何影响.NET相等?

时间:2011-06-06 17:00:41

标签: c# equality

为长时间设置道歉。我在这里有一个问题,我保证。

考虑具有在构造时分配的不可变唯一ID的类Node。除了其他方面,此ID在持久化对象图时用于序列化。例如,当一个对象被反序列化时,它会通过ID对主对象图进行测试,以查找冲突并拒绝它。

此外,Node仅由私有系统实例化,所有对它们的公共访问都是通过INode接口完成的。

所以我们有类似这种常见模式的东西:

interface INode
{
    NodeID ID { get; }

    // ... other awesome stuff
}

class Node : INode
{
    readonly NodeID _id = NodeID.CreateNew();

    NodeID INode.ID { get { return _id; } }

    // ... implement other awesome stuff

    public override bool Equals(object obj)
    {
        var node = obj as INode;
        return ReferenceEquals(node, null) ? false : _id.Equals(node.ID);
    }

    public override int GetHashCode()
    {
        return _id.GetHashCode();
    }
}

我的问题围绕.NET的这些比较/与平等相关的特性:

  • IEquatable<T>
  • IComparable / IComparable<T>
  • operator == / operator !=

最后是问题。实现像Node这样的类时:

  • 您实现了以上哪些接口/运营商? 为什么?
  • 您是否IEquatable<T>INode延长了Node?鉴于IEquatable<T>似乎只是通过运行时类型检查来使用(主要是在BCL中),是否指向/不延伸INode
  • 对于那些你实现的人,你是在课堂上做的,还是在ID上另外做?
    • 例如,IEquatable<NodeID>也是Node
    • 的一部分
    • 您是否在obj as NodeID
    • 中测试Equals

在C#工作了十多年之后,我很尴尬,没有彻底了解这些界面和与之相关的最佳实践。

(请注意,我们的.NET系统内置95%C#,5%C ++ / CLI,0%VB(如果它有所不同)。)

4 个答案:

答案 0 :(得分:1)

您的Node.Equals方法实际上只是NodeID.Equals的包装器,因此您需要实现真正的比较逻辑(我假设它类似于myIntId == someOtherInt)。否则,您将只使用从.Equals继承的默认object行为。

如果我认为你在最低级别的id只是一个int是正确的,你可以实现IComparable<NodeID>IEquatable<NodeID>,它只是你的int id字段的包装器{{1}已经实现了Int32IComparable<int>

答案 1 :(得分:1)

我对它的理解也可能不完美,但这里有......

我从不做IEquatable,可能是我的错误,但我认为重写Equals(obj)已经足够好了,我可以根据需要在那里(或在私人帮助方法中)进行类型检查。老实说,我没有在IEquatable中看到这一点,除非你只是想明确与其他类型相等。

IComprable用于排序/排序类型操作,如果有一些业务逻辑围绕排序而无法通过覆盖GetHashCode捕获,则它非常有用。同样,大多数时候我会使用GetHashCode进行默认排序或使用linq OrderBy(),所以这里没有太多意义。

当我覆盖Equals时,我总是覆盖== /!=。它使行为更加一致。

答案 2 :(得分:1)

正如Ed所说,如果你通过NodeID比较相等而NodeID不是值类型,那么NodeID类也必须实现IEquatable<NodeID>(仅因为你的语法为_id.Equals(node.ID)

也就是说,如果您在接口级别声明interface INode : IEquatable<INode>,则仍需要在实例级别实际实现:例如对于节点:public bool Equals(INode other)

  1. 只要您只对原始界面中声明的属性进行比较,这就有效。如果你有一个带有新属性的派生类,并且你也想用它来比较相等(比如NodeID和NodeDate),那么接口签名将不会给你的Equals方法提供它对该属性所需的访问权。
  2. 从调用者的角度来看(API的消费者想要将一个节点与另一个节点进行比较),调用者必须将所有实例强制转换为接口。这可以是原始意图,只要所有派生类都没有引入调用者需要但未在界面中声明的内容。
  3. 如果您发现您正在使用其他派生类属性,或者您不希望将消费者强制转换为INode,那么只需从派生类(节点)扩展IEquatable<Node>
  4. Joe Albhari对C# in a Nutshell

    中的两个界面都有一个非常容易理解和详细的解释

    使用IEquatable<T>的最重要原因是声明为您的类的使用者的合同,您保证非默认的相等实现。从框架的角度来看,您声明IEquatable<T>,以便使用哈希算法的框架类可以根据您的Equals和GetHashCode的实现进行比较。例如,HashSet集合,以及LINQ扩展,例如&#39; Distinct&#39;。那说:

    1. 如果您不同时覆盖Equals()GetHashCode();

    2. ,则散列算法将无法正常工作
    3. 如果您的覆盖IEquatableEquals(object);

    4. ,则
    5. 或者,如果您确实声明了,那么如果您实施GetHashCode(),则可省略Equals(object);

答案 3 :(得分:1)

  

您实施了哪IEquatable<T>IComparable / IComparable<T>operator == / operator !=?为什么呢?

我正如您所描述的那样实施IEquatable<T>。由于字典将使用此接口,因此可以在许多隐藏位置使用,特别是如果涉及LInQ。它会比被覆盖的Equals(object)更有效,因为类型安全和(对于值类型)缺乏拳击。

请注意,如果要修改类的相等语义,也必须覆盖GetHashCode

除非我的类型有某种数字解释,否则我通常不会覆盖operator ==。也就是说,== /!=几乎不是我在类上覆盖的唯一运算符。

  

您是从INode还是Node扩展IEquatable?鉴于IEquatable似乎只是通过运行时类型检查来使用(主要是在BCL中),是否指向/不从INode扩展?

如果我希望比较节点的相等性,我会将它添加到INode。如果没有,那我就不会打扰了。但是,如果INode的集合是您正在使用的集合,则可能是值得的。

  

对于那些你实现的人,你是在课堂上做的,还是在ID上另外做?

如果ID是一个简单类型,如整数,那么显然这是不必要的。如果它是复杂类型,那么关注点分离将要求在ID的类型上实现相等性。这就是我一直在思考的问题:什么设计可以让我以最少的努力做出最大的改变?如果你有一天决定改变复杂ID对象的格式,那么你是否想绕过代码修复所有会破坏的东西?或者您想将所有逻辑封装到ID类本身中。我当然会选择后者。 (这适用于ToString(),序列化代码以及其他所有内容。节点没有任何商业攻击其复杂ID类型的内部。)

  

例如,IEquatable<NodeID>是节点的一部分吗?

不,它不会是节点的一部分。 Node将实施IEquatable<Node>NodeId将实施IEquatable<NodeId>

  

您是否在Equals中测试obj为NodeID?

我不确定你在问什么,但我总是在object.Equals(object)类上实现IEquatable<T>覆盖,如下所示:

public override bool Equals(object obj)
{
    return Equals(obj as Node);
}

public bool Equals(Node node)
{
    if (node == null)
        return false;
    // ... do the type-specific comparison.
}