Click here to Skip to main content
15,881,561 members
Articles / Programming Languages / Visual Basic

Your full fledged type comparer

Rate me:
Please Sign up or sign in to vote.
4.40/5 (5 votes)
18 Mar 2012CPOL6 min read 13.7K   11   3
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:

VB
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:

VB.NET
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):

Non generic version (most complicated, because not strongly typed and thus needs conversion):

VB.NET
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):

VB.NET
#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:

VB.NET
#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.

VB.NET
#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:

VB.NET
' 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):

VB
#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:

VB
' 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:

VB
''' <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:

VB.NET
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:

VB.NET
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:

VB.NET
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.

VB.NET
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

VB
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.

VB
''' <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

VB
''' <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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
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.

Comments and Discussions

 
QuestionCustomized Comparers of 'unkown' object. Pin
Jens Madsen, Højby15-Jun-14 1:48
Jens Madsen, Højby15-Jun-14 1:48 
I quite like your approach - 5!
I've been playing a Little with this kind of comparisons, so I've got a couple of
helper methods that may be usefull to some of you ouit there. Wink | ;)


VB
Public Function IsComparable(m As MemberInfo) As Boolean
      If TypeOf m Is FieldInfo Then
          Return GetType(IComparable).IsAssignableFrom(CType(m, FieldInfo).FieldType)
      ElseIf TypeOf m Is MethodInfo Then
          Return GetType(IComparable).IsAssignableFrom(CType(m, MethodInfo).ReturnType)
      ElseIf TypeOf m Is PropertyInfo Then
          Dim p = CType(m, PropertyInfo)
          Return p.CanRead AndAlso GetType(IComparable).IsAssignableFrom(p.PropertyType)
      Else
          Return False
      End If
  End Function

  Public Function GetComparables(Of T)(flags As BindingFlags, members As MemberTypes) As IEnumerable(Of MemberInfo)
      Return GetType(T).FindMembers(members, flags, Function(m, c) (IsComparable(m)), Nothing)
  End Function

GeneralVery useful Pin
Duncan Edwards Jones23-Jan-14 22:18
professionalDuncan Edwards Jones23-Jan-14 22:18 
AnswerRe: Very useful Pin
Torsten Tiedt24-Jan-14 10:02
Torsten Tiedt24-Jan-14 10:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.