Thursday, July 2, 2009

Sorting Arrays and Collections (Part 3)

Part 1 here
Part 2 here

In part 1 of this series we looked at using the IComparable interface to provide sorting by allowing objects to compare themselves to each other. In part 2 we looked at using the IComparer interface to provide custom sorting through the external comparison of objects that may or may not be inherently comparable. In this third and final part we look at using the Comparison(T) delegate to provide similar functionality to the IComparer interface but without the need to define a whole class. The Comparison delegate allows us to use a method declared anywhere to perform the comparisons. Most often that would be in the same code file as we’re performing a one-off sort.

The signature of the Comparison delegate should put you in mind of the methods we’ve been using for comparisons already. It takes two arguments of type T and returns an Int32 value that indicates their relative order. That’s exactly the same as the IComparer.Compare method. Not surprisingly, the implementation of the method referred to by this delegate should be exactly the same as the implementation of an equivalent IComparer.Compare method.

Going back to our example from part 2, where we were able to sort a list of Person objects by both LastName and FirstName, we can basically extract the implementations of the CompareByFirstName and CompareByLastName methods. Instead of declaring them in a separate class that implements the IComparer interface we can declare them right there in the same class that contains the sorting code:

C#

private int ComparePeopleByFirstName(Person x, Person y)
{
    // Compare by FirstName by default.
    int result = x.FirstName.CompareTo(y.FirstName);
 
    // If FirstNames are the same...
    if (result == 0)
    {
        // ...compare by LastName.
        result = x.LastName.CompareTo(y.LastName);
    }
 
    return result;
}
 
private int ComparePeopleByLastName(Person x, Person y)
{
    // Compare by LastName by default.
    int result = x.LastName.CompareTo(y.LastName);
 
    // If LastNames are the same...
    if (result == 0)
    {
        // ...compare by FirstName.
        result = x.FirstName.CompareTo(y.FirstName);
    }
 
    return result;
}

VB

Private Function ComparePeopleByFirstName(ByVal x As Person, _
                                          ByVal y As Person) As Integer
    'Compare by FirstName by default.
    Dim result As Integer = x.FirstName.CompareTo(y.FirstName)
 
    'If FirstNames are the same...
    If result = 0 Then
        '...compare by LastName.
        result = x.LastName.CompareTo(y.LastName)
    End If
 
    Return result
End Function
 
Private Function ComparePeopleByLastName(ByVal x As Person, _
                                         ByVal y As Person) As Integer
    'Compare by LastName by default.
    Dim result As Integer = x.LastName.CompareTo(y.LastName)
 
    'If LastNames are the same...
    If result = 0 Then
        '...compare by FirstName.
        result = x.FirstName.CompareTo(y.FirstName)
    End If
 
    Return result
End Function

We can then create an instance of the Comparison delegate that references one or other of those methods to sort by the appropriate properties:

C#

List<Person> people = new List<Person>();
 
people.Add(new Person("Mary", "Smith"));
people.Add(new Person("John", "Williams"));
people.Add(new Person("John", "Smith"));
people.Add(new Person("Andrew", "Baxter"));
 
Console.WriteLine("Before sorting:");
 
foreach (Person person in people)
{
    Console.WriteLine(string.Format("{0}, {1}",
                                    person.LastName,
                                    person.FirstName));
}
 
people.Sort(new Comparison<Person>(ComparePeopleByLastName));
 
Console.WriteLine("After sorting by LastName:");
 
foreach (Person person in people)
{
    Console.WriteLine(string.Format("{0}, {1}",
                                    person.LastName,
                                    person.FirstName));
}
 
people.Sort(new Comparison<Person>(ComparePeopleByFirstName));
 
Console.WriteLine("After sorting by FirstName:");
 
foreach (Person person in people)
{
    Console.WriteLine(string.Format("{0} {1}",
                                    person.FirstName,
                                    person.LastName));
}

VB

Dim people As New List(Of Person)
 
people.Add(New Person("Mary", "Smith"))
people.Add(New Person("John", "Williams"))
people.Add(New Person("John", "Smith"))
people.Add(New Person("Andrew", "Baxter"))
 
Console.WriteLine("Before sorting:")
 
For Each person As Person In people
    Console.WriteLine(String.Format("{0}, {1}", _
                                    person.LastName, _
                                    person.FirstName))
Next
 
people.Sort(New Comparison(Of Person)(AddressOf ComparePeopleByLastName))
 
Console.WriteLine("After sorting by LastName:")
 
For Each person As Person In people
    Console.WriteLine(String.Format("{0}, {1}", _
                                    person.LastName, _
                                    person.FirstName))
Next
 
people.Sort(New Comparison(Of Person)(AddressOf ComparePeopleByFirstName))
 
Console.WriteLine("After sorting by FirstName:")
 
For Each person As Person In people
    Console.WriteLine(String.Format("{0} {1}", _
                                    person.FirstName, _
                                    person.LastName))
Next

Removing the need to declare a whole new class makes the Comparison delegate more convenient than the IComparer interface in many cases, but it can be more convenient still if we employ anonymous methods or lambda expressions.

C# 2.0 introduced the notion of anonymous methods. Anonymous methods can be used to initialise a delegate just as a named method can. If the code to perform our comparisons is relatively simple then using an anonymous method to create our Comparison delegate is simpler than writing a separate named method. Going back to our example from part 2, where we sorted strings by length instead of alphabetically, we could rewrite that code as follows:

C#

List<string> strings = new List<string>();
 
strings.Add("The longest of all");
strings.Add("Short");
strings.Add("Even longer");
strings.Add("Longer");
 
Console.WriteLine("Before sorting:");
 
foreach (string str in strings)
{
    Console.WriteLine(str);
}
 
strings.Sort(delegate(string x,
                      string y)
{
    return x.Length.CompareTo(y.Length);
});
 
Console.WriteLine("After sorting:");
 
foreach (string str in strings)
{
    Console.WriteLine(str);
}

Our anonymous method has the appropriate signature to initialise a Comparison delegate, i.e. it has two parameters of type string and a return type of int, so the C# compiler accepts it and invokes the appropriate overload of the Sort method.

VB 8 has no equivalent to C# anonymous methods but both VB 9 and C# 3.0 introduced lambda expressions as a supporting feature for LINQ. A lambda expression can be used to initialise a delegate in essentially the same way as an anonymous method:

C#

var strings = new List<string> {"The longest of all",
                                "Short",
                                "Even longer",
                                "Longer"};
 
Console.WriteLine("Before sorting:");
 
foreach (var str in strings)
{
    Console.WriteLine(str);
}
 
strings.Sort((x, y) => x.Length.CompareTo(y.Length));
 
Console.WriteLine("After sorting:");
 
foreach (var str in strings)
{
    Console.WriteLine(str);
}

VB

Dim strings As New List(Of String)
 
strings.Add("The longest of all")
strings.Add("Short")
strings.Add("Even longer")
strings.Add("Longer")
 
Console.WriteLine("Before sorting:")
 
For Each s In strings
    Console.WriteLine(s)
Next
 
strings.Sort(Function(x, y) x.Length.CompareTo(y.Length))
 
Console.WriteLine("After sorting:")
 
For Each s In strings
    Console.WriteLine(s)
Next

In each case the compiler infers the types of the lambda parameters based on the generic type of the list being sorted.

Just note that anonymous methods and lambdas make your code simpler if they themselves are simple but your code can quickly become difficult to read if you try to perform complex comparisons using an inline method.

So, let’s sum up the different ways we’ve discussed for sorting arrays and collections in the three parts of this series. First, we looked at performing automatic sorts on lists of objects that could compare themselves, thanks to their implementing the IComparable interface. Next, we looked at using the IComparer interface to create a class that could perform complex comparisons on objects that might not be inherently comparable. That class could also be reused in multiple code files or even multiple projects. Finally, we looked at using the generic Comparison delegate to invoke standard methods, anonymous methods and lambda expressions for simple custom sorts.

Happy sorting!

No comments: