Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Common .NET Comparer

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
18 Feb 2014CPOL7 min read 24K   183   17   6
Create an instant comparer for LINQ or other types that may require custom comparers.

Introduction

The .NET Framework provides a variety of comparison interfaces that you can implement within your own classes and that other core BCL components, such as LINQ, utilize.  These interfaces, such as IComparer, IEqualityComparer, IComparable, and IEquatable, allow you to write custom logic to provide sorting, comparing, equality, and more.  This article will not be a discussion on those interfaces and how to use them effectively or how they work, but instead, a way an ad-hoc comparer can be created when one is needed.   

Oftentimes, when developing for the .NET Framework, you will come across situations where you need a comparer (especially for types that do not implement the methods Equals and GetHashCode or that are implemented in a way unusable by your code), but then you must write a new class each time you need one.  The .NET Framework does not allow instantiation of interfaces, so any interface must be implemented within a class.  Sometimes you really should write a stand-alone comparer because the comparison logic is too complex to be represented cleanly or simply with a lambda expression and, as such, does require its own class to be effective or "clean," or the comparer itself provides such common functionality as it needs to be included within a custom BCL or Framework for maximum value and reuse potential.   

However, at other times, all you need is something simple and easy for a quick sort in a LINQ OrderBy method, or the Sort method of a List<T> instance, or a comparer for keys to store in a Dictionary.  Or you're using LINQ to join two dissimilar sequences together and the Join syntax needs an IEqualityComparer so that you can effectively "join" those two sequences into a meaningful output sequence.  You don't need a fancy class for that.  All you really need is a simple way to generate an appropriate comparer when needed.   

In comes the CommonComparer class to the rescue. 

Examining a Comparer Type 

Although this article is not a how-to usage manual on each of the comparer interface types, it will be helpful to briefly examine one of them, the IComparer interface, and see how we might utilize it effectively.  

C#
namespace System.Collections.Generic
{
  public interface IComparer<in T>
  {
    int Compare(T x, T y);
  }
}  

This interface has only one method.  This method takes two type T instances and returns an integer.  The general rule-of-thumb for the return value is that if X is greater than, less than, or equal to Y, you return either a 1, 0, or -1 respectively.  The key here about this method is that it can also be represented as a a standard generic delegate called Func.  A Func delegate, as opposed to an Action delegate, returns a value.  The IComparer<T>.Compare method signature has the same signature that a Func<T, T, int> delegate type would have.  We will leverage this to build our CommonComparer class.  

The CommonComparer class 

The CommonComparer class is a static class that contains a nested class and static methods to provide the desired functionality.  

 
C#
public static class CommonComparer
{
    private class InternalComparer<T> : IComparer<T>, IEqualityComparer<T>, IComparable<T>, IEquatable<T>
    {
        private readonly Func<T, T, int> CompareMethod;
    }
}
 

Although the .NET Framework does not support class-based multiple inheritance, it does support multiple inheritance through interfaces.  We will utilize this capability here by creating a single class that implements all of the interface types we need.  In this case, we will implement the interfaces IComparer<T>, IEqualityComparer<T>, IComparable<T>, and the IEquitable<T>.   

For brevity this article will only demonstrate and provide examples of the IComparer<T> interface.  If you would like the full source code, please download the sample attached to this article. 

Inside our class, we will define several delegates that we will call when the appropriate comparer method is called.  So, in the above example, we have defined a CompareMethod field that is of type Func<T, T, int> which matches the signature of the method inside the IComparer<T>.Compare method. 

Next, we need to implement the IComparer<T>.Compare method so that the delegate can be utilized. 

C#
 public static class CommonComparer
        {
            private class InternalComparer<T> : IComparer<T>, IEqualityComparer<T>, IComparable<T>, IEquatable<T>
            {
 
                private readonly Func<T, T, int> CompareMethod;
 
                #region IComparer implementation
 
                public InternalComparer(Func<T, T, int> compareMethod)
                {
                    CompareMethod = compareMethod;
                }
 
                int IComparer<T>.Compare(T x, T y)
                {
                    return CompareMethod(x, y);
                }
 
                #endregion
 
            }
        }
 

Here, we implement the interface method, Compare, explicitly. This allows us to implement methods that other interfaces may have with the same signature without causing confusion for the compiler, and us.

Note how we also created a constructor. The constructor takes the Func<T, T, int> delegate as its only parameter. This allows us to create an instance of the InternalComparer<T> class by supplying the method that the IComparer<T>.Compare method will call.  

The implementation of the Compare method, merely executes and returns our caller-supplied delegate function.  The delegate allows us to supply a method from outside this class to be called when comparing of objects needs to occur. 

We still have one more method to create.  That's the method used to create a new Comparer when we need one. 

C#
 public static class CommonComparer
        {
            private class InternalComparer<T> : IComparer<T>, IEqualityComparer<T>, IComparable<T>, IEquatable<T>
            {
 
                private readonly Func<T, T, int> CompareMethod;
 
                #region IComparer implementation
 
                public InternalComparer(Func<T, T, int> compareMethod)
                {
                    CompareMethod = compareMethod;
                }
 
                int IComparer<T>.Compare(T x, T y)
                {
                    return CompareMethod(x, y);
                }
 
                #endregion
            }
 
            /// <summary>
            /// Gets a standard <see cref="IComparer{T}"/> instance using the specified compare method.
            /// </summary>
            /// <typeparam name="T">The type to compare.</typeparam>
            /// <param name="compareMethod">The method to use in the comparison.</param>
            /// <returns>A new <see cref="IComparer{T}"/> instance.</returns>
            public static IComparer<T> GetComparer<T>(Func<T, T, int> compareMethod)
            {
                return new InternalComparer<T>(compareMethod);
            }
        }
 

In this case we've created a static method, GetComparer, within our static CommonComparer class, that instantiates a new InternalComparer<T> instance but returns it to the caller as an IComparer<T> instance. This obfuscates the private class, since anyone calling doesn't need to know the implementation class, only that the object returned is an IComparer<T> this works since the InternalComparer<T> is an IComparer<T>. 

 

Using the code   

Using the comparer is, again, simple.  First, in this contrived example, lets say that we have a set of strings, contained in a List<string> type that we need to sort.  The List<T> class provides a handy Sort method.  This method has various overloads, one of them being a method that takes an IComparer<T> instance, or more specifically, an IComparer<string> instance for a given List<string> instance.

For this first example, we want to do an ordinal case-insensitive sort on our List<string> type.  To do this, we will use the Sort method overload that allows us to pass an IComparer<string> instance.  We will get an instance of this using our static GetComparer method that we create earlier.   

C#
List<string> characterNames = new List<string>
    {
        "Bette",
        "Elmer",
        "Fred",
        "Barney",
        "Wilma",
        "Billy",
        "Daisy"
    };
characterNames.Sort(CommonComparer.GetComparer<string>((v1, v2) => string.Compare(v1, v2, StringComparison.OrdinalIgnoreCase)));
 
foreach (var name in characterNames) {
    Console.WriteLine(name);
}  

In this example, we can depend on the string.Compare method to get us through.  This method works exactly as we expect ours to work and, prevents us from having to perform the complex string compares ourselves, while still allowing us to spin one up at a moments notice. 

This will sort the names in descending order.  When run, you should have output similar to the following:

Barney
Bette
Billy
Daisy
Elmer
Fred
Wilma  

Now, what if we wanted to sort that same list in ascending order?  It's actually quite simple to do with what we have already done with one minor tweak:  

C#
 List<string> characterNames = new List<string>
    {
        "Bette",
        "Elmer",
        "Fred",
        "Barney",
        "Wilma",
        "Billy",
        "Daisy"
    };
characterNames.Sort(CommonComparer.GetComparer<string>((v1, v2) => -string.Compare(v1, v2, StringComparison.OrdinalIgnoreCase)));
 
foreach (var name in characterNames) {
    Console.WriteLine(name);
}

In this case, we merely negate the string.Compare method value using the negation operator. 

 Now, when you run the code, this is what you should get: 

Wilma
Fred
Elmer
Daisy
Billy
Bette
Barney  

 For one more example, lets examine one of the compare methods that is not shown in this article, that of the CommonComparer.GetEqualityComparer.  In this sample, we will be using an IEqualityComparer<T> type instance to help us with storing values in a HashSet<T>.  Most keyed type lists, such as Dictionary<K, V> and HashSet<T> allow us to pass, as part of a constructor, a comparer that will help determine if a "key" is contained in the list.  This allows us to customize how values get stored or keyed, based on our own determination of what constitutes equality, over any existing equality code that the owner of the type may have implemented. 

C#
 IEqualityComparer<Person> personComparer = 
    CommonComparer
        .GetEqualityComparer<Person>(
            (p1, p2) => p1.FirstName == p2.FirstName && p1.LastName == p2.LastName, 
            p => string.Concat(p.FirstName, p.LastName).ToLowerInvariant().GetHashCode()
        );
HashSet<Person> characters = new HashSet<Person>(personComparer);
 
characters.Add(new Person { FirstName = "Bette", LastName = "Johnson" });
characters.Add(new Person { FirstName = "Elmer", LastName = "Pickle" });
characters.Add(new Person { FirstName = "Fred", LastName = "Smith" });
characters.Add(new Person { FirstName = "Barney", LastName = "Rhinestone" });
characters.Add(new Person { FirstName = "Wilma", LastName = "Green" });
characters.Add(new Person { FirstName = "Billy", LastName = "Thompson" });
characters.Add(new Person { FirstName = "Daisy", LastName = "Anderson" });
 
//Add returns true if an element is added, false if it's already there.
bool addSamePersonAgain = characters.Add(new Person { FirstName = "Elmer", LastName = "Pickle" });
 
Console.WriteLine("Was Elmer Pickle added again?:  {0}\r\n", addSamePersonAgain);
 
foreach (var character in characters) {
    Console.WriteLine(character);
}

 When run, you should see the following output:

C#
Was Elmer Pickle added again?:  False
 
Johnson, Bette
Pickle, Elmer
Smith, Fred
Rhinestone, Barney
Green, Wilma
Thompson, Billy
Anderson, Daisy    

Notice that Elmer Pickle was not added again.  Even though I created a brand-new instance of the Person class, according to my IEqualityComparer<Person> implementation, Elmer Pickle was already in my list.  If we did not have the IEqualityComparer<Person> instance, Elmer Fudd would have been added twice.  This is due to the fallback position of the .NET Framework for equality for types:  if the type does not override the methods Equals and GetHashCode, reference equality is assumed.  Since new types are always a new reference, we can add as many "Elmer Pickle" persons to the list if we don't have a way of determining sameness or equality.  

Conclusion  

 I hope you found this article interesting and enlightening and I welcome your feedback.  As an exercise,  try filling in the missing InternalComparer<T> methods without looking at the example.  There should be enough code contained in this article to follow the pattern.

I didn't include error handling in this article.  Error handling is important because the class, although private does implement more than one interface, and although not likely to occur, someone could do something like this: 

C#
IComparer<string> comparer = CommonComparer.GetComparer<string>((s1, s2) => s1.CompareTo(s2));
IEqualityComparer<string> otherComparer = comparer as IEqualityComparer<string>;
int result = otherComparer.GetHashCode("Test"); 

However, you can let someone know that the usage is wrong by simply providing feedback through an exception if similar code is executed. 

Points of Interest 

The following are MSDN articles on the comparer interfaces used in this article:



Also, you may want to check out the following MSDN articles:



History  

  • Version 1.0  
  • Version 1.1 - Fixed missing example code.


 

License

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


Written By
United States United States
I am a software engineer, developer, programmer with 20+ years of experience working on various types of systems and design and have built a lot of software from the ground up and maintained a lot of software developed by others.

I truly enjoy working in this field and I'm glad I started this career more than 20 years ago.

I've learned, over the years, that the biggest and toughest bugs to find usually have the simplest solution once found.

Comments and Discussions

 
QuestionMay sound completly idotic Pin
mph628-Jan-15 5:12
mph628-Jan-15 5:12 
GeneralMy vote of 5 Pin
Duncan Edwards Jones11-Jul-14 1:20
professionalDuncan Edwards Jones11-Jul-14 1:20 
QuestionChanging the sort order Pin
George Swan16-Feb-14 20:51
mveGeorge Swan16-Feb-14 20:51 
GeneralArticle Description Pin
Giacomo Pozzoni16-Feb-14 0:37
Giacomo Pozzoni16-Feb-14 0:37 
GeneralRe: Article Description Pin
M. Eugene Andrews16-Feb-14 1:13
M. Eugene Andrews16-Feb-14 1:13 
Questionsample attached to this article? Pin
Member 966172915-Feb-14 15:54
Member 966172915-Feb-14 15:54 

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.