Sorting IComparable objects some of which are null
Asked Answered
T

4

7

Most people, when writing a refence type (class) which implements IComparable<T>, use the convention that null is LESS than any actual object. But if you try to use the opposite convention, something interesting happens:

using System;
using System.Collections.Generic;

namespace SortingNulls
{
  internal class Child : IComparable<Child>
  {
    public int Age;
    public string Name;

    public int CompareTo(Child other)
    {
      if (other == null)
        return -1; // what's your problem?

      return this.Age.CompareTo(other.Age);
    }

    public override string ToString()
    {
      return string.Format("{0} ({1} years)", this.Name, this.Age);
    }
  }

  internal static class Program
  {
    private static void Main()
    {
      var listOfChilds = new List<Child>
      {
        null,
        null,
        null,
        null,
        new Child { Age = 5, Name = "Joe" },
        new Child { Age = 6, Name = "Sam" },
        new Child { Age = 3, Name = "Jude" },
        new Child { Age = 7, Name = "Mary" },
        null,
        null,
        null,
        null,
        new Child { Age = 7, Name = "Pete" },
        null,
        new Child { Age = 3, Name = "Bob" },
        new Child { Age = 4, Name = "Tim" },
        null,
        null,
      };

      listOfChilds.Sort();

      Console.WriteLine("Sorted list begins here");
      for (int i = 0; i < listOfChilds.Count; ++i)
        Console.WriteLine("{0,2}: {1}", i, listOfChilds[i]);
      Console.WriteLine("Sorted list ends here");
    }
  }
}

When running the above code, you see that the null references are not sorted as expected. Apparently, when comparing A to B, if A is an object and B is null, the user-defined comparision is used, but if conversely A is null and B is an object, some BCL comparision is used instead.

Is this a bug?

Tonry answered 31/5, 2011 at 15:14 Comment(0)
C
11

No, this is not a bug. Your CompareTo method which implements IComparable<Child> is defined on your Child class. In other words, if you have to invoke a method on one of your types in order to make a comparison.

If one of the Child items being compared is null, how can you invoke CompareTo on it?

Note that from the definition of IComparable:

By definition, any object compares greater than (or follows) a null reference (Nothing in Visual Basic), and two null references compare equal to each other.

Which explains the results you observe.

The solution is to delegate to some other class to perform the comparison. See the IComparer interface.

Corkage answered 31/5, 2011 at 15:20 Comment(1)
Nice answers. I hadn't noticed that the documentation said an object must compare greater than null. When A is null and B is not null, the framework could have swapped A and B, called my method, and then flipped the sign of the returned Int32. I think that sometimes happens with the old nongeneric IComparable. Of course with an IComparer<T> or a Comparision<T> delegate it is easy to handle null as the first parameter.Tonry
P
1

What would expect to happen if you tried to evaluate this.Age.CompareTo(other.Age); if this is null? In fact, this can never be null in C#.

As for asking if it's a bug, see this blog post.

Polynuclear answered 31/5, 2011 at 15:21 Comment(0)
V
1

No, its your code that has a "bug" as its not following the standards that define IComparable.CompareTo(): IComparable

Specifically: By definition, any object compares greater than (or follows) null, and two null references compare equal to each other.

In your example you are defining your object to compare smaller than (or precedes) null, which is precisely the opposite of how it should be done.

Vaporizer answered 31/5, 2011 at 15:24 Comment(0)
C
1

The default Comparer<T> for a type T has to take into account the scenario where the first element (let's call it A) is null. Let's say it looks something like this:

if (ReferenceEquals(a, null))
{
    return -1;
}

return a.CompareTo(b);

This is based on the documentation of List<T>.Sort:

This method uses the default comparer Comparer(Of T).Default for type T to determine the order of list elements.

Arguably, the top step could only return 0 if both elements are null, and otherwise use the opposite of b.CompareTo(a).

I wouldn't really call it a bug, though. It's just something to be aware of.

Combatant answered 31/5, 2011 at 15:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.