C# arguably could have just have some baked in notion in System.Object about how you order objects like it did with Equals
to compare them for identity.
Unfortunately, this leads to concerns about intensionality vs. extensionality, localizationm etc.
There is an IComparable<T>
interface, but the built-in value types can't implement interfaces like that.
Therefore, there is no good way to just look at a type at compile time and know definitively if it has a meaningful ordering. =(
The mechanism that has evolved in C# is to use the IComparer<T>
instance that is returned by Comparer<T>.Default
and get a runtime error when you try to sort something which lacks an ordering.
By allowing multiple IComparer
s and IComparer<T>
s, you can have a notion of multiple alternative orderings that work on the same type, so that much is good, but all is not well.
Internally, c# uses a bunch of rules to find Comparer<T>.Default
, which it handles differently if T is an instance of IComparable<T>
or is of the form Nullable<T>
, etc.
e.g. the code from system/collections/generic/comparer.cs:
public static Comparer<T> Default {
get {
Comparer<T> comparer = defaultComparer;
if (comparer == null) {
comparer = CreateComparer();
defaultComparer = comparer;
}
return comparer;
}
}
private static Comparer<T> CreateComparer() {
Type t = typeof(T);
// If T implements IComparable<T> return a GenericComparer<T>
if (typeof(IComparable<T>).IsAssignableFrom(t)) {
//return (Comparer<T>)Activator.CreateInstance(typeof(GenericComparer<>).MakeGenericType(t));
return (Comparer<T>)(typeof(GenericComparer<int>).TypeHandle.CreateInstanceForAnotherGenericParameter(t));
}
// If T is a Nullable<U> where U implements IComparable<U> return a NullableComparer<U>
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) {
Type u = t.GetGenericArguments()[0];
if (typeof(IComparable<>).MakeGenericType(u).IsAssignableFrom(u)) {
//return (Comparer<T>)Activator.CreateInstance(typeof(NullableComparer<>).MakeGenericType(u));
return (Comparer<T>)(typeof(NullableComparer<int>).TypeHandle.CreateInstanceForAnotherGenericParameter(u));
}
}
// Otherwise return an ObjectComparer<T>
return new ObjectComparer<T>();
}
In essence, this lets Microsoft gradually add special cases for their own code, but unfortunately the code that results from just using Comparer<T>.Default
for a custom IComparable<T>
instance is pretty abysmal.
The Sort method on IList<T>
, presumes that Comparer<T>.Default
will come up with something it can use to compare your objects, but it has no way to look at the type T and tell that it does, so it assumes it is safe and only realizes later at runtime that MyClass
isn't playing along.