Why do I have to overload operators when implementing CompareTo?
Asked Answered
M

2

40

Let's say I have a type that implements IComparable.

I would have thought it's reasonable to expect that the operators ==, !=, >, <, >= and <= would "just work" automatically by calling CompareTo but instead I have to override them all if I want to use them.

From the language design perspective is there a good reason it was done this way? Are there any cases when you it's genuinely useful for A>B to behave differently to Compare(A,B)>0?

Mellifluous answered 15/12, 2013 at 11:1 Comment(1)
You don't call Compare(A, B), you call A.CompareTo(B). And that means it cannot ever work if A is null, which you might want to support with an overloaded operator (x == null and null == x should check the same thing). I'm not at all convinced this is the primary reason, though.Encephalo
U
33

The whole situation is vexing. C# has too many ways to express equality and inequality:

  • the == != > < >= <= operators (which are logically static methods)
  • the Equals static method (which calls the virtual method), the Equals virtual method, the ReferenceEquals method
  • The IComparable and IEquatable interfaces

They all have subtly different semantics and with the exception of static Equals, none automatically uses the other, and none actually has the behavior that I want. Static methods are dispatched based on the compile-time type of both operands; the virtual methods / interface methods are dispatched based on the run-time type of one of the operands, which makes the operation asymmetric; the type of one side matters more than the type of the other.

I can't imagine that anyone thinks that the situation we're in is great; given no constraints, this is not what would have evolved. But managed language designers do have constraints: the CLR does not implement static methods in interface contracts or double-virtual dispatch, or the ability to put an operator constraint on a generic type parameter. And therefore multiple solutions have evolved to solve the equality/inequality problem.

I think that were the CLR and C# designers to go back in time and tell their past selves what features ought to be in v1 of the CLR, some form of static methods in interfaces would be high on the list. If there were static methods in interface then we can define:

interface IComparable<in T, in U> 
{
    static bool operator <(T t, U u);
    static bool operator >(T t, U u);
    ... etc

And then if you have:

static void Sort<T>(T[] array) where T : IComparable<T, T>

You could then use the < and == and so on operators to compare elements.

Unfold answered 15/12, 2013 at 16:55 Comment(6)
This wouldn't be an interface in the current meaning of the term, would it? I don't see how it would be possible to meaningfully use your suggested IComparable<T, T> outside of a generic type constraint. It seems more like C++'s proposed concepts to me.Encephalo
@hvd: The proposed feature is not currently implemented so by definition it changes the meaning of "interface". Perhaps a different term, like "concept" would be better; I don't know. Trying to design the feature in this 500 character comment seems like a bad idea.Unfold
Oh, I'm certainly not asking for that. I just think that what you could do with that interface is fundamentally different from what you can do in the current C# with interfaces, and what you can do with them in other languages that had them before C# did. I'm probably just over-thinking this.Encephalo
Thanks Eric this is a really great discussion. As usual the answer is that it's more complicated than I thought :)Mellifluous
The idea of providing static methods on an interface, maybe renamed a contract, would be unbelievably helpful. For example, when creating an architecture where you truly want static classes to implement the same contract, you end up having to build Singleton's that implement an interface. Thus making it so they expose an Instance property, or eek, building a set of static methods to mimic the instance methods which leverage the Instance property on their own. At any rate Eric, this would be a phenomenal feature but with your explanation I can see the hurdles they'd now have to overcome.Roncesvalles
Different ways of comparing things are useful in different contexts. I would posit that in a good language calls to ReferenceEquals shouldn't ever be necessary (there should be an operator which serves that purpose in all cases where both operands are reference types, and is forbidden in all others) but otherwise it's better to have more ways of comparing things, each of which can have a consistent behavior whenever it's compile-time legal (whether or not it's "useful" in all contexts), than to try to cram more meanings into fewer ways of comparison.Trapshooting
S
12

Two main reasons:

  1. It is a general structure for all operators. While the comparison operators may never have alternative semantics, there is great utility in a structure that allows very different semantics for some of the other operators. Implementing a separate structure for just the comparison operators would have required omitting some other, probably much more useful, feature. Check out this elegant implementation of BNF within C# for an example.
  2. The default implementations, for the case of value types that have it, relies of necessity on Reflection, and thus is horribly inefficient. Only you actually know the most efficient way to implement these operators for your classes. In many cases not all fields of a struct need to be compared to test equality, nor do all fields always need to be combined in a suitable GetHashCode implementation. No default implementation can ever determine that for all types, because it is reducible to the Halting Problem.

Update as per Eric Lippert among others, the following is the appropriate standard implementation of the comparison operators in C# for a type UDT:

public int  CompareTo(UDT x) { return CompareTo(this, x); }
public bool Equals(UDT x)    { return CompareTo(this, x) == 0; }
public static bool operator  < (UDT x, UDT y) { return CompareTo(x, y)  < 0; }
public static bool operator  > (UDT x, UDT y) { return CompareTo(x, y)  > 0; }
public static bool operator <= (UDT x, UDT y) { return CompareTo(x, y) <= 0; }
public static bool operator >= (UDT x, UDT y) { return CompareTo(x, y) >= 0; }
public static bool operator == (UDT x, UDT y) { return CompareTo(x, y) == 0; }
public static bool operator != (UDT x, UDT y) { return CompareTo(x, y) != 0; }
public override bool Equals(object obj)
{
    return (obj is UDT) && (CompareTo(this, (UDT)obj) == 0);
}

Just add the custom definition for private static int CompareTo(UDT x, UDT y) and stir.

Semela answered 15/12, 2013 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.