HomeC# Building C# Comparable Objects: IComparabl...
Building C# Comparable Objects: IComparable versus IComparer
Working with object-oriented languages (like C#) is all about manipulating objects. We create objects, change objects properties, save objects states, and sort objects. The part we are concentrating on in this article is sorting objects.
Contributed by Ayad Boudiab Rating: / 9 August 04, 2008
Objects that are part of the language (such as strings) can be sorted without any additional work on our end. But when it comes to classes that we create (Book, Employee, Car…), we need to give hints to the runtime on how objects of these classes can be sorted (what does it mean that book1 comes before book2? Are they sorted by title? By ISBN?...). The hints that we give to the runtime are the interfaces. We implement interfaces such as IComparable and IComparer so the runtime can tell how to compare and sort objects.
Interface definition: An interface is a named collection of abstract methods. Abstract means that the methods are not implemented. It is left up to the user of the interface to implement these methods the way he/she sees fit. When you create an interface you are creating a contract, which means that any code implementing the interface needs to implement all the methods in the interface (you simply cannot pick and choose which methods to implement).
To create an interface you use the keyword interface followed by the name of the interface. Interfaces do not specify a base class, and all interface members are implicitly public. Here is an interface declaration:
interface IDraw
{
void Draw();
}
By convention, interface names are prefixed by the letter ‘I’. Based on this declaration, any class that needs to implement the interface IDraw must provide implementation for the method Draw(). Let’s create a class named Circle that implements the IDraw Interface:
In our first attempt to sort objects, we create an Employee class with name and id. In the Main() method we create an array of five employees. The System.Array class provides a Sort() method that sorts an array of objects. When we try to run the following code we end up with a runtime exception: “At least one object must implement IComparable.” In other words, the error is asking us what we mean by sorting employees.
class Program
{
static void Main( string [] args)
{
Employee[] employees = new Employee[5];
employees[0] = new Employee( "Dan K." , 12456);
employees[1] = new Employee( "Jim L." , 99584);
employees[2] = new Employee( "Tony T." , 51472);
employees[3] = new Employee( "Jessica W." , 32788);
employees[4] = new Employee( "Leda B." , 44110);
//**Runtime exception: at least one object must
//**implement IComparable
Array .Sort(employees);
}
}
class Employee
{
private string name;
public string Name
{
get { return name; }
set { name = value ; }
}
private int id;
public int Id
{
get { return id; }
set { id = value ; }
}
public Employee( string a_name, int an_id)
{
name = a_name;
id = an_id;
}
}
It is clear then that we need to implement the IComparable interface to be able to sort the array of employees. The IComparable interface contains a single method called CompareTo:
public int CompareTo( object obj)
The method accepts an object as a parameter and returns an integer. Since object is the parent of all objects, passing an Employee or any other object will work. The return value could be positive, negative, or zero:
A return value of zero means that the two objects we are comparing are equal to each other.
A negative return value means that the first object comes before the second object.
A positive return value means that the first object comes after the second object.
The first thing we do in the CompareTo method is cast the object parameter into an Employee object. The we compare this object with the newly casted Employee object. In this case we decided to compare them by id. Here is how the code will look:
class Program
{
static void Main( string [] args)
{
Employee[] employees = new Employee[5];
employees[0] = new Employee( "Dan K." , 12456);
employees[1] = new Employee( "Jim L." , 99584);
employees[2] = new Employee( "Tony T." , 51472);
employees[3] = new Employee( "Lee W." , 32788);
employees[4] = new Employee( "Leda B." , 44110);
Array .Sort(employees);
foreach (Employee e in employees)
Console .WriteLine(e);
}
}
class Employee : IComparable
{
private string name;
public string Name
{
get { return name; }
set { name = value ; }
}
private int id;
public int Id
{
get { return id; }
set { id = value ; }
}
public Employee( string a_name, int an_id)
{
name = a_name;
id = an_id;
}
public override string ToString()
{
return string .Format( "Name: {0}ttId: {1}" ,
Name, Id);
}
#region IComparable Members
public int CompareTo( object obj)
{
Employee temp = (Employee)obj;
if ( this .Id > temp.Id)
return 1;
if ( this .Id < temp.Id)
return -1;
else
return 0;
}
#endregion
}
In the Main() method, we sort the employees array in place and we use the foreach loop to print the list of employees. Try to run the code and notice in the output how the objects are sorted by id.
If we need to sort objects for classes that we create, we need to implement the IComparable interface to tell the runtime how to sort the objects. In our employee example we sorted the list of employees by id. Can we sort the employees by name instead? Sure we can. Just change the CompareTo() method to return -1, 0, or 1 based on the name comparison, not the id.
Although this change works perfectly fine, it is short of being complete. When our sorting requirements change, the CompareTo() method changes. It will be ideal if we can keep the CompareTo() method as is, but implement other interfaces that satisfy different sorting conditions. This is where the IComparer interface comes to the rescue. The IComparer interface has a Compare() method that has the following signature:
public int Compare( object obj1, object obj2)
Notice that the method still returns an integer like the CompareTo() method in the IComparable interface. The difference, however, is in the parameters. This method takes two parameters instead of one: obj1 and obj2. These are the two objects that we are comparing. We first cast them to the appropriate type, then we compare them based on our needs.
Since the Compare() method takes two objects that we are comparing as parameters, we normally do not implement the IComparer interface in the same class where we implemented the IComparable interface. Instead, we create a separate class that implements the IComparer interface and pass a new instance of that class to the Array.Sort() method. This method is overloaded to accept an IComparer interface as a parameter:
public static void Sort(Array array, IComparer comparer)
Let’s first implement the method Compare()in a class called EmployeeComparer :
class EmployeeComparer : IComparer
{
public EmployeeComparer() { }
public int Compare( object obj1, object obj2)
{
Employee e1 = ( Employee )obj1;
Employee e2 = ( Employee )obj2;
return string .Compare(e1.Name, e2.Name);
}
}
Notice how we cast the objects to Employee , then we call the Compare() method of the string class to do the actual comparison by passing the employees’ names as parameters. The only thing we have left now is to pass an instance of the EmployeeComparer class to the Sort method:
Array .Sort(employees, new EmployeeComparer ());
By doing so, the employees can now be sorted by name instead of id. We can even go a step further by defining a static property in the Employee class that returns a new EmployeeComparer casted as an IComparer :
public static IComparer SortByEmployeeName
{
get { return ( IComparer ) new EmployeeComparer(); }
get { return ( IComparer ) new EmployeeComparer(); }
}
#region IComparable Members
public int CompareTo( object obj)
{
Employee temp = (Employee)obj;
if ( this .Id > temp.Id)
return 1;
if ( this .Id < temp.Id)
return -1;
else
return 0;
}
#endregion
}
class EmployeeComparer : IComparer
{
public EmployeeComparer() { }
#region IComparer Members
public int Compare( object obj1, object obj2)
{
Employee e1 = (Employee)obj1;
Employee e2 = (Employee)obj2;
return string .Compare(e1.Name, e2.Name);
}
#endregion
}
Conclusion
The System.Array.Sort() methods sorts objects of different types. While sorting predefined types (string…) is already implemented in the language, sorting objects for classes that we create requires implementing interfaces to tell the runtime how to sort these objects. We can implement the Comparable interface and override its CompareTo() method and/or the Comparer interface with its Compare() method. The Sort() method is overloaded to support both implementations.