Your full fledged type comparer






4.40/5 (5 votes)
Never implementing sorting again
Introduction
Sorting is a basic need in many applications. In easy scenarios all you have to do is to call the sort-method and your are done. But sorting becomes more complex when you want to sort your own objects, and it becomes even more complex if you don't want to implement your custom sorting logic for each business object again and again.
This article takes you on journey where you first meet the most basic sort implementations and continue the way to more advanced sorting until you end up having a reusable, sophisticated generic comparer class.
The zoo: it's all about animal (types)
Imagine you're a developer in the zoo and the custom type you're working on is an animal class:
Public Class Animal
#Region "Constructors"
Public Sub New(ByVal name As String)
_Name = name
End Sub
Public Sub New(ByVal name As String, ByVal weigth As Integer)
Me.New(name)
_Weight = weigth
End Sub
Public Sub New(ByVal name As String, ByVal weigth As Integer, ByVal size As Double)
Me.New(name, weigth)
_Size = size
End Sub
#End Region
#Region "Properties"
Public Property Name As String
Public Property Size As Double
Public Property Weight As Integer
Public Property Aggression As Integer
Public Property Employee As Employee
#End Region
End Class
There can by only one (criteria to sort by): IComparable
The director comes to you and and says: “Please give me a list of all the animals we have sorted by their name”. No problem you think, I will put all the animals into a generic list and call the list’s sort method like this:
Dim animals As New List(Of Animal)
Dim tiger As New Animal("Tiger", 250, 150)
Dim monkey As New Animal("Monkey", 50, 80)
Dim elephant As New Animal("Elephant", 1500, 250)
animals.Add(elephant)
animals.Add(tiger)
animals.Add(monkey)
animals.Sort() ''Error!!!
As you might already know or guess, the sort method throws a runtime error, because the compiler does not know how to compare your custom animal type to do the sorting.
Solution: we can tell the compiler to sort the animals by name by implementing the
IComparable
interface. By doing so, the animal type is now comparable and ready for sorting. Here is the the code you need to insert to create a sortable animal type by name :
Non generic version (most complicated, because not strongly typed and thus needs conversion):
Public Function CompareTo(ByVal obj As Object) As Integer Implements System.IComparable.CompareTo
Dim otherAnimal As Animal = DirectCast(obj, Animal)
If Me.Name > otherAnimal.Name Then Return 1
If Me.Name < otherAnimal.Name Then Return -1
Return 0
End Function
Generic version (no type conversion needed):
#Region "IComparable: Compare animals by name"
Public Function CompareTo(ByVal other As Animal) As Integer _
Implements System.IComparable(Of Animal).CompareTo
If Me.Name > other.Name Then Return 1
If Me.Name < other.Name Then Return -1
Return 0
' This is a shortcut for the three lines of code above
'Return String.Compare(Me.Name, other.Name)
End Function
#End Region
Generic version in it’s shortest implementation:
#Region "IComparable: Compare animals by name"
Public Function CompareTo(ByVal other As Animal) As Integer _
Implements System.IComparable(Of Animal).CompareTo
Return String.Compare(Me.Name, other.Name)
End Function
#End Region
There is more to sort by: IComparer
Problem solved, you think and relax, when the director suddenly appears in you back and asks for a second list. This time sorted by weight.
“Damn!”, you think. “No problem!”, you say and start speculating how to solve this after the director is gone. For sure, you could change the logic
of the CompareTo
-function, but you would need to do this over and over again and recompile every time the director changes his mind how
the list should be sorted. Luckily, you see that the sort-method has an overload to take a generic IComparer
object as parameter. You continue
the path and design a new class implementing the generic IComparer
:
Note that this class is nested inside the animal class which is no technical need but makes sense, because no other class will ever use it.
#Region "IComparer: Compare animals by other criteria than standard by name"
Public Class AnimalWeigthComparer
Implements IComparer(Of Animal)
Public Function Compare(ByVal x As Animal, ByVal y As Animal) As _
Integer Implements System.Collections.Generic.IComparer(Of Animal).Compare
If x.Weight > y.Weight Then Return 1
If x.Weight < y.Weight Then Return -1
Return 0
End Function
End Class
#End Region
After implementing the generic IComparer
you finally try to sort the animals by weight:
' First lines as in the code sample above...
' Create an instance of the IComparable class first
Dim animalWeightComparer As New Animal.AnimalWeigthComparer
' Use this class to sort by weight
animals.Sort(animalWeightComparer)
“Hey, it works!”, you scream and happily deliver the director the list of animals sorted by weight.
All (properties) for one: the generic AnimalComparer
As you might guess, your temporary luck is suddenly interrupted when the director wants another list. This time sorted by size. “Sure!”, you say and really mean
this time, because you know you could easily implement a second class called AnimalSizeComparer
implementing the generic IComparer
-interface.
But doing so requires to write a new comparer class and recompile the application every time the director enters the room with a new sorting wish,
and – what the reader does not know – the animal class has many, many properties the director could use as sorting criteria. “There must be a better
way to achieve some kind of dynamic sorting by defining the property I need.”, you think and start experimenting by expanding the IComparable
class you
already implemented until you got this (watch code comments for further explanation):
#Region "IComparer generic: Compare animals by the property you define"
Public Class AnimalGenericComparer
Implements IComparer(Of Animal)
' PropertyInfo is an object from the sytem.reflection-namespace.
' Import this namespace into your project.
Private _ComparionProperty As PropertyInfo
' To ensure that the class get's it's sort property
' the parameter less constructor is disabled
Private Sub New()
End Sub
' Assign the property you want to use for sorting
Public Sub New(ByVal comparisonProperty As System.Reflection.PropertyInfo)
_ComparionProperty = comparisonProperty
End Sub
' This method is called by the sort method under the cover
Public Function Compare(ByVal x As Animal, ByVal y As Animal) As _
Integer Implements System.Collections.Generic.IComparer(Of Animal).Compare
' Here we get the animals property values we want to compare
' GetValue is a reflection method allowing us to get the value
' of the property for the animal x and y.
Dim xVal As Object = _ComparionProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparionProperty.GetValue(y, Nothing)
' All basic value-types like integer, boolean, date, etc.
' are comparable by default because the already implement
' the IComparable-interface. We try to use this interface
' for comparison if it is implemented.
If TypeOf xVal Is IComparable Then
Return DirectCast(xVal, IComparable).CompareTo(yVal)
Else
' If the property is not comparable, eg it is an object
' you defined, we do a comparison by it's string
' representation.
Return String.Compare(xVal.ToString, yVal.ToString)
End If
End Function
End Class
#End Region
You know try your new generic animal comparer class:
' First lines as in the code sample above...
Dim animalGenericComparer As New Animal.AnimalGenericComparer(GetType(Animal).GetProperty("Size"))
animals.Sort(animalGenericComparer)
It works and happy you are! This time you deliver not only the list the director wanted but one list for each of the missing properties an animal has (you should have seen the director’s face!).
There is more than just animals: the basic generic comparer
But you notice that the director is not the director for nothing as he wants a list for another application of yours which administers employees.
“No problem.”, you say and think, knowing that you can just code an employeeGenericComparer
-class the same way you did for the animal class.
But suddenly you halt and reflect. “What, if the director now likes sorted lists so much that he wants sorted lists for all kind of objects I have defined in my apps?”.
Creating a new comparer class for each object would be a pain. “There must be a better way!”, you think and start experimenting until you got a class allowing you
to sort any object you want by any property you define:
''' <summary>
''' Allows to sort each class by the property you define.
''' </summary>
''' <typeparam name="T">The type of the object you want to sort.</typeparam>
''' <summary>
''' Allows to sort each class by the property you define.
''' </summary>
''' <typeparam name="T">The type of the object you want to sort.</typeparam>
Public Class GenericComparer(Of T)
Implements IComparer(Of T)
' PropertyInfo is an object from the sytem.reflection-namespace.
' Import this namespace into your project.
Private _ComparisonProperty As PropertyInfo
' To ensure that the class get's it's sort property
' the parameter less constructor is disabled
Private Sub New()
End Sub
''' <summary>
''' Creates new GenericComparer
''' </summary>
''' PropertyInfo of property to sort.
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
End Sub
''' <summary>
''' Allows to sort objects of same type by the defined property
''' </summary>
''' First object to compare.
''' Second object to compare.
''' <returns>Integer</returns>
''' <remarks>This method is called by the sort method under the cover.</remarks>
Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
Implements System.Collections.Generic.IComparer(Of T).Compare
' Get comparison property values to compare
Dim xVal As Object = _ComparisonProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparisonProperty.GetValue(y, Nothing)
If TypeOf xVal Is IComparable Then
' Compare based on non generic IComparable interface
Return DirectCast(xVal, IComparable).CompareTo(yVal)
Else
' Compare based on string representation
Return String.Compare(xVal.ToString, yVal.ToString)
End If
End Function
End Class
Now you test your generic comparer with the test proven animal class:
Dim animalProperty As PropertyInfo = GetType(Animal).GetProperty("Size")
Dim myGenericComparer As New GenericComparer(Of Animal)(animalProperty)
animals.Sort(myGenericComparer)
And once again: it works! For the animals, but also for the employees? Let’s try. First the employee’s class:
Public Class Employee
Public Sub New(ByVal workID As Integer, ByVal name As String, ByVal workExperience As Integer)
_WorkID = workID
_Name = name
_WorkExperience = workExperience
End Sub
Public Property WorkID As Integer
Public Property Name As String
Public Property WorkExperience As Integer
Public Overrides Function ToString() As String
Return "WorkID" & Me.WorkID.ToString & ";Name:" & Me.Name & ";Workexperience:" & Me.WorkExperience
End Function
End Class
And now we create a small collection of these and sort them by their WorkID:
Dim firstEmployee As New Employee(1, "Hans", 14)
Dim secondEmployee As New Employee(2, "Dieter", 3)
Dim thirdEmployee As New Employee(2, "Frank", 4)
Dim employees As New List(Of Employee)
With employees
.Add(firstEmployee)
.Add(secondEmployee)
.Add(thirdEmployee)
End With
' Sort employees by their name
Dim propertyInfo As PropertyInfo = GetType(Employee).GetProperty("Name")
Dim genericComparer As New GenericComparer(Of Employee)(propertyInfo)
employees.Sort(genericComparer)
' By the way: the implemented generic IComparable let's the default sort-method
' also sort by Name
employees.Sort()
That works! You give the sorted list of employees to the director, want to leave his office when you hear in your back: “But I also need a list of the animals sorted by their employee’s names!” Ok, what can you do but try to create the wanted list. First you add the responsible employees to the animals and then you sort.
Dim firstEmployee As New Employee(1, "Hans", 14)
Dim secondEmployee As New Employee(2, "Dieter", 3)
Dim thirdEmployee As New Employee(2, "Frank", 4)
Dim employees As New List(Of Employee)
With employees
.Add(firstEmployee)
.Add(secondEmployee)
.Add(thirdEmployee)
End With
Dim animals As New List(Of Animal)
Dim tiger As New Animal("Tiger", 250, 150)
tiger.Employee = firstEmployee
Dim monkey As New Animal("Monkey", 50, 80)
monkey.Employee = secondEmployee
Dim elephant As New Animal("Elephant", 1500, 250)
elephant.Employee = thirdEmployee
animals.Add(elephant)
animals.Add(tiger)
animals.Add(monkey)
Dim animalProperty As PropertyInfo = GetType(Animal).GetProperty("Employee")
Dim myGenericComparer As New GenericComparer(Of Animal)(animalProperty)
animals.Sort(myGenericComparer)
Shocking result! The employees are not sorted by their names but by their string-comparison, because our generic comparer did not find
the IComparable
-interface in the employee class and thus compared by using the
ToString
-method’s output (see screenshot below).
But you don’t panic, because you already know the IComparable
-interface, and because you’re so advanced and prefer
strong typing, you implement
the generic version of it in the employee class like this
Public Class Employee
' Employee now implements the generic version of IComparable
Implements IComparable(Of Employee)
Public Sub New(ByVal workID As Integer, ByVal name As String, ByVal workExperience As Integer)
_WorkID = workID
_Name = name
_WorkExperience = workExperience
End Sub
Public Property WorkID As Integer
Public Property Name As String
Public Property WorkExperience As Integer
Public Overrides Function ToString() As String
Return "WorkID" & Me.WorkID.ToString & _
";Name:" & Me.Name & _
";Workexperience:" & Me.WorkExperience
End Function
' This should ensure that collections of employees are sorted by their name by default
Public Function CompareTo(ByVal other As Employee) As Integer _
Implements System.IComparable(Of Employee).CompareTo
Return String.Compare(Me.Name, other.Name)
End Function
End Class
Some like it generic: the advanced generic type comparer
You try again and just to find out that nothing improved. The employee list is sorted like before and your generic
IComparable
-interface ignored,
and the same second you notice what to do and extend the GenericComparer
class to additionally look for a generic
IComparer
-interface of the property (represented
by the employee type in our case). Pay attention to the new _HasGenericIComparable
-variable, how it’s value is assigned in the constructor and how it is used later when doing the comparison.
''' <summary>
''' Allows to sort each class by the property you define.
''' </summary>
''' <typeparam name="T">The type of the object you want to sort.</typeparam>
''' <remarks>Now also uses generic IComparable implementation.</remarks>
Public Class GenericComparerExtended(Of T)
Implements IComparer(Of T)
Private _HasGenericIComparable As Boolean = False
' PropertyInfo is an object from the sytem.reflection-namespace.
' Import this namespace into your project.
Private _ComparisonProperty As PropertyInfo
' To ensure that the class get's it's sort property
' the parameter less constructor is disabled
Private Sub New()
End Sub
''' <summary>
''' Creates new GenericComparer
''' </summary>
''' PropertyInfo of property to sort.
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
' Check if property implements generic version of IComparable
If Not comparisonProperty.DeclaringType.GetInterface("IComparable`1") Is Nothing Then
_HasGenericIComparable = True
End If
End Sub
''' <summary>
''' Compares two objects of same type by evaluating the defined comparison property.
''' </summary>
''' First object to compare.
''' Second object to compare.
''' <returns>Integer</returns>
Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
Implements System.Collections.Generic.IComparer(Of T).Compare
' Get comparison property values to compare
Dim xVal As Object = _ComparisonProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparisonProperty.GetValue(y, Nothing)
If TypeOf xVal Is IComparable Then
' Compare based on non generic IComparable interface
' Handles all value types
Return DirectCast(xVal, IComparable).CompareTo(yVal)
ElseIf _HasGenericIComparable Then
' Compare based on generic IComparable interface
' Handles reference type implementing generic IComparable only
Return CInt(xVal.GetType.GetMethod("CompareTo").Invoke(xVal, {yVal}))
Else
' Compare based on string representation
Return String.Compare(xVal.ToString, yVal.ToString)
End If
End Function
End Class
You try and the final result is just what you wanted: sorted animals by the name of their responsible employee:
Can can be proud to yourself now, because this generic comparer is already advanced, but are you sure that the director can’t find another list to sort
which still causes problems? What’s about null-values? This could cause a problem, because mostly you will sort value-types like integers which may be nothing
if they make use of the generic nullable type. Assume the weight of the animals would be of nullable (of integer) – because the employees take the animals
weight only after the are full-grown – and you wanted to sort the list with a child animal whose weight is nothing, you would get an
InvalidOperationException
when calling because null-values (=nothing) can’t be compared. To handle this, we replace a null-value with a the minimal value of the type during the
sort operation. If no minimal value for the type exist, we assign it the (somewhat arbitrary) string value “” and compare the values based on their string-comparison.
There are nullables out there: the full fledged generic type comparer
''' <summary>
''' Allows to sort each class by the property you define.
''' </summary>
''' <typeparam name="T">The type of the object you want to sort.</typeparam>
''' <remarks>Now also uses generic IComparable implementation
''' and sorts types having null-values.</remarks>
Public Class GenericComparerFinal(Of T)
Implements IComparer(Of T)
Private _HasGenericIComparable As Boolean = False
Private _DefaultValue As Object = "<null>"
' PropertyInfo is an object from the sytem.reflection-namespace.
' Import this namespace into your project.
Private _ComparisonProperty As PropertyInfo
' To ensure that the class get's it's sort property the parameter less constructor is disabled
Private Sub New()
End Sub
''' <summary>
''' Creates new GenericComparer
''' </summary>
''' <param name="comparisonProperty" />PropertyInfo of property to sort.
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
' Check if property implements generic version of IComparable
If Not comparisonProperty.DeclaringType.GetInterface("IComparable`1") Is Nothing Then
_HasGenericIComparable = True
End If
' If property to sort is nullable the null value must be replaced with a default value
' which is determined depending on the datatype inside the nullable datatype.
If comparisonProperty.Equals(GetType(String)) Then
_DefaultValue = String.Empty
ElseIf comparisonProperty.PropertyType.IsGenericType AndAlso _
comparisonProperty.PropertyType.GetGenericTypeDefinition.Equals(GetType(Nullable(Of ))) Then
' Try to get the minimal value (supported for numeric datatypes and datetime)
Dim fld As FieldInfo = _
Nullable.GetUnderlyingType(comparisonProperty.PropertyType).GetField("MinValue")
If Not fld Is Nothing Then
_DefaultValue = fld.GetValue(Nothing)
End If
End If
End Sub
''' <summary>
''' Compares two objects of same type by evaluating the defined comparison property.
''' </summary>
''' <param name="x" />First object to compare.
''' <param name="y" />Second object to compare.
''' <returns>Integer</returns>
Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
Implements System.Collections.Generic.IComparer(Of T).Compare
Dim doStringComparison As Boolean = False
' Get comparison property values to compare
Dim xVal As Object = Nothing
Dim yVal As Object = Nothing
' Get values of properties to compare
' Replace null-value with default value
If _ComparisonProperty.GetValue(x, Nothing) Is Nothing Then
xVal = _DefaultValue
Else
xVal = _ComparisonProperty.GetValue(x, Nothing)
End If
If _ComparisonProperty.GetValue(y, Nothing) Is Nothing Then
yVal = _DefaultValue
Else
yVal = _ComparisonProperty.GetValue(y, Nothing)
End If
' Only instances of same type can be compared.
' If no type specific default value was set we would try to compare
' different types resulting in an error.
If _DefaultValue.ToString = "<null>" AndAlso Not _
Type.Equals(xVal.GetType, yVal.GetType) Then
doStringComparison = True
End If
If TypeOf xVal Is IComparable AndAlso doStringComparison = False Then
' Compare based on non generic IComparable interface
' Handles all value types
Return DirectCast(xVal, IComparable).CompareTo(yVal)
ElseIf _HasGenericIComparable AndAlso doStringComparison = False Then
' Compare based on generic IComparable interface
' Handles reference type implementing generic IComparable only
Return CInt(xVal.GetType.GetMethod("CompareTo").Invoke(xVal, {yVal}))
Else
' Compare based on string representation
Return String.Compare(xVal.ToString, yVal.ToString)
End If
End Function
End Class
The (happy) end
Congratulations. Now you have a full-fledged type comparer!