views:

318

answers:

5

I have a list of objects, some of which can be null. I want it sorted by some property, with nulls at the end of list. However, List<T>.Sort() method seems to put nulls in the beginning no matter what the comparer returns. This is a little program that I used to test this:

class Program
 {
  class Foo {
   public int Bar;
   public Foo(int bar)
   {
    Bar = bar;
   }
  }
  static void Main(string[] args)
  {
   List<Foo> list = new List<Foo>{null, new Foo(1), new Foo(3), null, new Foo(100)};

   foreach (var foo in list)
   {
     Console.WriteLine("Foo: {0}", foo==null?"NULL":foo.Bar.ToString());
   }

   Console.WriteLine("Sorting:");
   list.Sort(new Comparer());
   foreach (var foo in list)
   {
    Console.WriteLine("Foo: {0}", foo == null ? "NULL" : foo.Bar.ToString());
   }
   Console.ReadKey();
  }
  class Comparer:IComparer<Foo>
  {
   #region Implementation of IComparer<in Foo>

   public int Compare(Foo x, Foo y)
   {
    int xbar = x == null ? int.MinValue : x.Bar;
    int ybar = y == null ? int.MinValue : y.Bar;
    return ybar - xbar;
   }

   #endregion
  }
 }

Try this yourself: sorted list is printed as

Foo: NULL
Foo: NULL
Foo: 100
Foo: 3
Foo: 1

nulls Are first, even though they're compared as int.Minvalue. Is the method buggy or what?

+12  A: 

Let’s say x is null and y is 1.

int xbar = x == null ? int.MinValue : x.Bar;
int ybar = y == null ? int.MinValue : y.Bar;
return ybar - xbar;

So now we have 1 - int.MinValue. This will be an overflow, so the end result will be negative, hence the null will be considered less than 1.

For that reason, the computation is fundamentally flawed.

public int Compare(Foo x, Foo y)
{
    int xbar = x == null ? int.MinValue : x.Bar;
    int ybar = y == null ? int.MinValue : y.Bar;
    return ybar.CompareTo(xbar);
}

(Thanks to the commenters for pointing out the flaw in my method.)

Konrad Rudolph
Very nice catch (+1)
Martin Milan
Oh. I'm feeling stupid. Yeah, that was it.
Nevermind
If Bar is comparable then I'd recommend using `x.Bar.CompareTo(y.Bar)`
Garry Shutler
So what if x == null and y == null?
Dave Van den Eynde
@Garry: rather the other way round, since the ordering should be inverted.
Konrad Rudolph
@Konrad Ah yes, missed that bit. My main point was to lean on the type's implementation of IComparable where possible rather than reimplementing it.
Garry Shutler
You will get a runtime error with this as it expects to get zero when the object is being compared to each other. There needs to be a if (x==null first
btlog
@Dave: unimportant for the sake of this sorting. But you’re right, and yours is a much simpler fix … (+1 on your answer).
Konrad Rudolph
A: 

int.MinValue is -2147483648.

Now, suppose you were comparing 3 and -2147483648 via subtraction, specifically 3 - (-2147483648?

Also you should consider using the null coalescing operator ??

tzenes
How would you use the null coalesce operator here? You can’t coalesce anything here, unless you define a null placeholder object for `Foo` – then you could use `(x ?? NullPlaceholder).Bar`.
Konrad Rudolph
Well, foo is essentially an integer with the option to be declared `null` so I'd replace `Foo` with `int?` and null coalesce them. Sorry if that wasn't clear.
tzenes
+2  A: 

Your implementation of IComparer.Compare is unnecessarily complex. Remember that all that matters about the return value is the sign - the magnitude is unimportant. To have nulls compare less than all real Foos, simply defer to Int32s implementation of CompareTo, after making your substitution:

return       (x == null ? int.MinValue : x.Bar)
  .CompareTo((y == null ? int.MinValue : y.Bar))
AakashM
+4  A: 

Replace this line:

return ybar - xbar;

with this line:

return ybar.CompareTo(xbar);

Because your arithmetic is overflowing.

Dave Van den Eynde
+13  A: 

Your implementation is incorrect because you failed to write the specification first, then write code that meets the specification.

So, start over. Write a specification:

  • FooCompare takes two references to Foo, x and y. Either may be null.
  • IComparer.Compare(x,y) is documented as returning zero if x and y are equal, negative if y is greater than x, and positive if y is smaller than x. We will follow this convention.
  • if x and y are both null, they are equal.
  • if one is null and the other one is not then the null one is the smaller.
  • if neither are null then the comparison is based on the value of the Bar property.

And now you can write code that you are guaranteed matches the spec:

// FooCompare takes two references to Foo, x and y. Either may be null.
// +1 means x is larger, -1 means y is larger, 0 means they are the same.

int FooCompare(Foo x, Foo y)
{
    // if x and y are both null, they are equal.
    if (x == null && y == null) return 0;
    // if one is null and the other one is not then the non-null one is larger.
    if (x != null && y == null) return 1; 
    if (y != null && x == null) return -1; 
    // if neither are null then the comparison is 
    // based on the value of the Bar property.
    var xbar = x.Bar; // only calculate x.Bar once
    var ybar = y.Bar; // only calculate y.Bar once
    if (xbar > ybar) return 1;
    if (ybar > xbar) return -1;
    return 0;
}

// The original poster evidently wishes the list to be sorted from
// largest to smallest, where null is the smallest.
// Reverse the polarity of the neutron flow:
int Compare(Foo x, Foo y)
{
    return FooCompare(y, x);
}

Don't mess around with clever integer arithmetic tricks; as you've seen, clever equals buggy. Correctness first.

Eric Lippert
Funny thing is, that's exactly what I did. But it wasn't working correctly. Then I tried to change it to find out what's wrong, and made the version I put here, and it still didn't work. I was working with Mono in Unity3d; befuddled, I decided to try to reproduce this behavior in .NET. And it "reproduced" because I forgot about overflow.Your code is in fact correct and works in .NET like it should. But it doesn't work in Unity3d's Mono, which is probably a bug (and I'll go to their site now and ask there).
Nevermind
Eric, a buddy of mine pointed out that you may have mixed up the return values when one reference is null and one is not. Based upon your specifications `else if (x == null ` should return `1` and `else if (x != null ` should return `-1` if null values should appear at the end of the sorted list.
Brian Hasden
Eric Lippert
@Eric: +1, very well said, though you did [mix your signs](http://msdn.microsoft.com/en-us/library/system.collections.icomparer.compare%28VS.71%29.aspx)
BlueRaja - Danny Pflughoeft
@BlueRaja and @Brian, ah, thank you, yes, you are right. The problem here is that the *spec* is wrong. The reason that the spec is wrong is because I was matching the original poster's design. Notice how it returns ybar - xbar. If ybar is 10 and xbar is 3 then it returns 7, which indicates that x is *greater* than y according to the docs, but in fact 10 is greater than 3. This is because the original poster is attempting to sort the list into *reverse* order. Hence my confusion.
Eric Lippert
I've rewritten the code to follow the conventions of IComparer.
Eric Lippert