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()
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):
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
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"
ublic 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:
Dim animalWeightComparer As New Animal.AnimalWeigthComparer
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)
Private _ComparionProperty As PropertyInfo
Private Sub New()
End Sub
Public Sub New(ByVal comparisonProperty As System.Reflection.PropertyInfo)
_ComparionProperty = comparisonProperty
End Sub
Public Function Compare(ByVal x As Animal, ByVal y As Animal) As _
Integer Implements System.Collections.Generic.IComparer(Of Animal).Compare
Dim xVal As Object = _ComparionProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparionProperty.GetValue(y, Nothing)
If TypeOf xVal Is IComparable Then
Return DirectCast(xVal, IComparable).CompareTo(yVal)
Else
Return String.Compare(xVal.ToString, yVal.ToString)
End If
End Function
End Class
#End Region
You know try your new generic animal comparer class:
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:
Public Class GenericComparer(Of T)
Implements IComparer(Of T)
Private _ComparisonProperty As PropertyInfo
Private Sub New()
End Sub
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
End Sub
Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
Implements System.Collections.Generic.IComparer(Of T).Compare
Dim xVal As Object = _ComparisonProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparisonProperty.GetValue(y, Nothing)
If TypeOf xVal Is IComparable Then
Return DirectCast(xVal, IComparable).CompareTo(yVal)
Else
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
Dim propertyInfo As PropertyInfo = GetType(Employee).GetProperty("Name")
Dim genericComparer As New GenericComparer(Of Employee)(propertyInfo)
employees.Sort(genericComparer)
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
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
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.
Public Class GenericComparerExtended(Of T)
Implements IComparer(Of T)
Private _HasGenericIComparable As Boolean = False
Private _ComparisonProperty As PropertyInfo
Private Sub New()
End Sub
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
If Not comparisonProperty.DeclaringType.GetInterface("IComparable`1") Is Nothing Then
_HasGenericIComparable = True
End If
End Sub
Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
Implements System.Collections.Generic.IComparer(Of T).Compare
Dim xVal As Object = _ComparisonProperty.GetValue(x, Nothing)
Dim yVal As Object = _ComparisonProperty.GetValue(y, Nothing)
If TypeOf xVal Is IComparable Then
Return DirectCast(xVal, IComparable).CompareTo(yVal)
ElseIf _HasGenericIComparable Then
Return CInt(xVal.GetType.GetMethod("CompareTo").Invoke(xVal, {yVal}))
Else
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
Public Class GenericComparerFinal(Of T)
Implements IComparer(Of T)
Private _HasGenericIComparable As Boolean = False
Private _DefaultValue As Object = "<null>"
Private _ComparisonProperty As PropertyInfo
Private Sub New()
End Sub
Public Sub New(ByVal comparisonProperty As PropertyInfo)
_ComparisonProperty = comparisonProperty
If Not comparisonProperty.DeclaringType.GetInterface("IComparable`1") Is Nothing Then
_HasGenericIComparable = True
End If
If comparisonProperty.Equals(GetType(String)) Then
_DefaultValue = String.Empty
ElseIf comparisonProperty.PropertyType.IsGenericType AndAlso _
comparisonProperty.PropertyType.GetGenericTypeDefinition.Equals(GetType(Nullable(Of ))) Then
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
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
Dim xVal As Object = Nothing
Dim yVal As Object = Nothing
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
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
Return DirectCast(xVal, IComparable).CompareTo(yVal)
ElseIf _HasGenericIComparable AndAlso doStringComparison = False Then
Return CInt(xVal.GetType.GetMethod("CompareTo").Invoke(xVal, {yVal}))
Else
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!
Currently I’m working as a database and application developer for a big auditing company in Frankfurt, Germany. At desk my daily doing is based on SQL-Server and .Net-programming, and I admit that in my off-time things can stay the same. But before worrying about my social life let me tell you that I love doing sports with my friends, to travel with my wife, to read ficitional literature and that I desperately try to learn russian as third foreign language.