We all use <, <=, >, and >= with integers and floating point values all the time. It just works and it’s built into basically every programming language. These simple operators suddenly become quite a pain when you start wanting to compare other objects. IComparable seems to make it easier, but there’s some trickiness when you start dealing with null objects. Today’s article explores this and ends up with some handy utility functions to take some of the gotchas out of comparing.

Let’s start with a simple class:

class Player
{
	public string Name { get; set; }
	public int Score { get; set; }
}

If we try to compare two Player objects, we get a compiler error:

var mario = new Player { Name = "Mario", Score = 100 };
var luigi = new Player { Name = "Luigi", Score = 50 };
if (mario > luigi)
{
	Debug.Log("Mario wins");
}
else if (mario < luigi)
{
	Debug.Log("Luigi wins");
}
else
{
	Debug.Log("Tie");
}
Error CS0019: Operator `>' cannot be applied to operands of type `Player' and `Player' (CS0019)

C# provides two main solutions to this issue. First are a pair of interfaces designed to make objects comparable, aptly named IComparable and IComparable<T>. All we have to do is make our Player class implement one or both of their CompareTo methods. Both take another object to compare to and return an int that’s less than zero if this is less than the parameter, greater than zero if it’s greater, and zero if it’s equal. Here’s what Player looks like with both of them implemented:

using System;
 
class Player : IComparable, IComparable<Player>
{
	public string Name { get; set; }
	public int Score { get; set; }
 
	public int CompareTo(object other)
	{
		return Score - ((Player)other).Score;
	}
 
	public int CompareTo(Player player)
	{
		return Score - player.Score;
	}
}

Now we have to change from using the > and < operators to calling CompareTo:

if (mario.CompareTo(luigi) > 0)
{
	Debug.Log("Mario wins");
}
else if (mario.CompareTo(luigi) < 0)
{
	Debug.Log("Luigi wins");
}
else
{
	Debug.Log("Tie");
}

Already we have three problems. First is that the non-generic IComparable takes any object as its parameter. That’s great if we can compare to anything, but it’s likely we can only compare to specific types. The generic version that takes a type is usually a much better fit for this reason. If we want to use the non-generic interface, we’re almost always forced to cast or otherwise check the type of the parameter so we can make a comparison.

The second problem is related to the first: what if the parameter to compare against is null? Both of the CompareTo implementations would throw an exception trying to access the Score field. So let’s fix up those functions so they always return 1 if the parameter is null. This means that any non-null object is greater than any null object, which makes sense because “something is better than nothing”.

public int CompareTo(object other)
{
	return other == null ? 1 : Score - ((Player)other).Score;
}
 
public int CompareTo(Player player)
{
	return other == null ? 1 : Score - player.Score;
}

The third problem is trickier: what if this is null? Yes, it’s true that you can’t even get into the method if the object you’re calling it on is null, but what if you still want to compare the objects? Should you change all of the calling code like this?

if (mario == null)
{
	if (luigi == null)
	{
		Debug.Log("Tie");
	}
	else
	{
		Debug.Log("Luigi wins");
	}
}
else
{
	if (luigi == null)
	{
		Debug.Log("Mario wins");
	}
	else
	{
		if (mario.CompareTo(luigi) > 0)
		{
			Debug.Log("Mario wins");
		}
		else if (mario.CompareTo(luigi) < 0)
		{
			Debug.Log("Luigi wins");
		}
		else
		{
			Debug.Log("Tie");
		}
	}
}

Yikes! That got out of hand fast! Since our CompareTo can’t even be entered if this is null, it seems like we’re forced to bloat up the calling code like this. However, there is another option. We could use a static utility method to do the comparison:

public static int NullSafeCompareTo<T>(IComparable<T> a, T b)
{
	return a == null
		? b == null
			? 0
			: -1
		: b == null
			? 1
			: a.CompareTo(b);
}

This handles the cases where one or both of the objects to compare is null with 0, 1, and -1 values and then hands off to the real CompareTo function when both are not null. Now we can use it instead of directly using CompareTo like this:

if (NullSafeCompareTo(mario, luigi) > 0)
{
	Debug.Log("Mario wins");
}
else if (NullSafeCompareTo(mario, luigi) < 0)
{
	Debug.Log("Luigi wins");
}
else
{
	Debug.Log("Tie");
}

This has been really successful. The code is now just about as terse as it was before, but we can do even better. C# features “extension methods” which allow us to tack on methods to existing classes and interfaces just like they were always there. It’s really handy in a case like this because we can use it to make the comparison look even more like we were just using CompareTo. Here are the extension methods:

/// <summary>
/// Null-safe extension methods for IComparable and IComparable{T}
/// </summary>
/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3268</author>
public static class IComparableExtensions
{
	/// <summary>
	/// Compare an IComparable{T} with a {T}. If both are null, zero is returned. If this object is
	/// null, -1 is returned. If the object to compare with is null, 1 is returned. Otherwise, this
	/// object's <c>CompareTo</c> is called with the object to compare to as its parameter.
	/// </summary>
	/// <returns>The comparison of this object with the parameter</returns>
	/// <param name="a">The object to compare against <c>b</c></param>
	/// <param name="b">The object to compare with</param>
	/// <typeparam name="T">The type of object to compare with</typeparam>
	public static int NullSafeCompareTo<T>(this IComparable<T> a, T b)
	{
		return a == null
			? b == null
			? 0
				: -1
				: b == null
			? 1
				: a.CompareTo(b);
	}
 
	/// <summary>
	/// Compare an IComparable with an object. If both are null, zero is returned. If this object is
	/// null, -1 is returned. If the object to compare with is null, 1 is returned. Otherwise, this
	/// object's <c>CompareTo</c> is called with the object to compare to as its parameter.
	/// </summary>
	/// <returns>The comparison of this object with the parameter</returns>
	/// <param name="a">The object to compare against <c>b</c></param>
	/// <param name="b">The object to compare with</param>
	public static int NullSafeCompareTo(this IComparable a, object b)
	{
		return a == null
			? b == null
			? 0
				: -1
				: b == null
			? 1
				: a.CompareTo(b);
	}
}

Now let’s look at the comparison code:

if (mario.NullSafeCompareTo(luigi) > 0)
{
	Debug.Log("Mario wins");
}
else if (mario.NullSafeCompareTo(luigi) < 0)
{
	Debug.Log("Luigi wins");
}
else
{
	Debug.Log("Tie");
}

The only difference between this and the version that directly used CompareTo is that it seems to be calling a NullSafeCompareTo method instead. We know that it’s really a static utility method, but the caller doesn’t need to care about that or where it’s defined.

The other tool that C# provides us is called operator overloading. With it we’re able to add on the <, <=, >, >=, ==, and != operators to the Player class so we can make the comparisons even more natural. Here’s how that’d look:

class Player
{
	public string Name { get; set; }
	public int Score { get; set; }
 
	public static bool operator >(Player a, Player b)
	{
		return a.Score > b.Score;
	}
 
	public static bool operator >=(Player a, Player b)
	{
		return a.Score >= b.Score;
	}
 
	public static bool operator <(Player a, Player b)
	{
		return a.Score < b.Score;
	}
 
	public static bool operator <=(Player a, Player b)
	{
		return a.Score < b.Score;
	}
 
	public static bool operator ==(Player a, Player b)
	{
		return a.Score == b.Score;
	}
 
	public static bool operator !=(Player a, Player b)
	{
		return a.Score != b.Score;
	}
}

The comparison code now looks just like it did in the start!

if (mario > luigi)
{
	Debug.Log("Mario wins");
}
else if (mario < luigi)
{
	Debug.Log("Luigi wins");
}
else
{
	Debug.Log("Tie");
}

Unfortunately, we’re back to not having any safety with null. We can’t even add an extension method to fix the problem this time, so we’re forced to put the work on the operator overloads themselves. At least those operator overloads can call the extension method for IComparable<T> to clean up the code a bit. Here’s how it’d look if they did:

class Player : IComparable, IComparable<Player>
{
	public string Name { get; set; }
	public int Score { get; set; }
 
	public int CompareTo(object other)
	{
		return Score - ((Player)other).Score;
	}
 
	public int CompareTo(Player player)
	{
		return Score - player.Score;
	}
 
	public static bool operator >(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) > 0;
	}
 
	public static bool operator >=(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) >= 0;
	}
 
	public static bool operator <(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) < 0;
	}
 
	public static bool operator <=(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) <= 0;
	}
 
	public static bool operator ==(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) == 0;
	}
 
	public static bool operator !=(Player a, Player b)
	{
		return a.NullSafeCompareTo(b) != 0;
	}
}

So even with the operator overloading approach, the NullSafeCompareTo extension methods really come in handy. Regardless of how you choose to compare, add some safety against null with these extension methods!

How do you handle null objects in cases like these? Got a utility method to share and compare? Add your thoughts in the comments!