Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do immutable ID properties affect .NET equality?

Tags:

c#

equality

(Apologies for the long setup. There is a question in here, I promise.)

Consider a class Node that has an immutable unique ID that is assigned at construction time. This ID is used for serialization when persisting the object graph, among other things. For example, when an object is deserialized, it gets tested against the main object graph by ID to look for a collision and reject it.

Also, a Node is only instantiated by a private system, and all public accesses to them are done via the INode interface.

So we have something like this common pattern:

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();
    }
}

My questions are around these comparison/equality-related features of .NET:

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

Here are the questions, finally. When implementing a class like Node:

  • Which of the above interfaces/operators do you implement as well? Why?
  • Do you extend IEquatable<T> from INode or Node? Given that IEquatable<T> only seems to get used (mostly in the BCL) through runtime type checks, is there a point to/not to extend from INode?
  • For those that you do implement, do you do them just on the class itself, or do you additionally do it on the ID as well?
    • For example, is IEquatable<NodeID> also a part of the Node?
    • Do you test for obj as NodeID in Equals?

After working in C# for over a decade, I'm embarrassed not to have a thorough understanding of these interfaces and best practices related to them.

(Note that our .NET systems are built in 95% C#, 5% C++/CLI, 0% VB (if it makes a difference).)

like image 681
scobi Avatar asked Jun 06 '11 17:06

scobi


3 Answers

Which of IEquatable<T>, IComparable / IComparable<T>, operator == / operator != do you implement as well? Why?

I implement IEquatable<T> for circumstances as you are describing. Since dictionaries will make use of this interface, it could be used in a number of hidden places, especially if LInQ is involved. It will be more effecient than the overridden Equals(object), because of type-safety and (for value types) lack of boxing.

Note that GetHashCode must also be overridden as well if you are modifying the equality semantics for your class.

I don't normally override operator == unless my type has some kind of numeric interpretation. That is to say, == / != are hardly ever the only operators that I override on a class.

Do you extend IEquatable from INode or Node? Given that IEquatable only seems to get used (mostly in the BCL) through runtime type checks, is there a point to/not to extend from INode?

I would add it to INode if I expected to ever compare nodes for equality. If not, then I wouldn't bother. It may be worthwhile, though, if collections of INodes are what you are working with.

For those that you do implement, do you do them just on the class itself, or do you additionally do it on the ID as well?

If the ID is a simple type, like an integer, then clearly this is unnecessary. If it is a complex type, then separation-of-concerns would require that equality be implemented on the ID's type. This is how I always think about it: what design will allow me to make the biggest changes with the least amount of effort? If you someday decide to change the format of the complex ID object, then do you want to go around the code fixing all the things that this will break? Or do you want to encapsulate all of that logic into the ID class itself. I would certainly choose the latter. (This goes for ToString(), serialization code, and everything else. A Node has no buisness poking around the internals of its complex ID type.)

For example, is IEquatable<NodeID> also a part of the Node?

No, it wouldn't be part of the node. Node would implement IEquatable<Node> and NodeId would implement IEquatable<NodeId>.

Do you test for obj as NodeID in Equals?

I am not exactly sure what you are asking, but I always implement the object.Equals(object) override on IEquatable<T> classes like this:

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.
}
like image 88
Jeffrey L Whitledge Avatar answered Nov 13 '22 02:11

Jeffrey L Whitledge


My understanding of it may also be imperfect, but here goes...

I never do IEquatable, might be a mistake on my part but I figure overriding Equals(obj) is good enough and I can do my type checking in there (or in private helper methods) as needed. I honestly don't see the point in IEquatable, unless you're just trying to be explicit about be equatable w/ another type.

IComprable is used for sorting/ordering type operations, and is useful if there's some business logic around sorting that can't be captured by overriding GetHashCode. Again, most of the time I will either do default sorting using GetHashCode or use a linq OrderBy() so there's not much point here.

I always override ==/!= when I override Equals. It makes the behavior a lot more consistent.

like image 1
Paul Avatar answered Nov 13 '22 02:11

Paul


Your Node.Equals method is really just a wrapper for NodeID.Equals, so that's where you need to implement the real comparison logic (which I assume would be something like myIntId == someOtherInt). Otherwise you will just be using the default .Equals behavior that you inherit from object.

If I am correct in assuming that your id at the lowest level is just an int, you can implement IComparable<NodeID> and IEquatable<NodeID> which will simply be wrappers around your int id field as Int32 already implements IComparable<int> and IEquatable<int>.

like image 1
Ed S. Avatar answered Nov 13 '22 00:11

Ed S.