BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

LINQ
.NET 3.5+

A Generic Equality Comparer for LINQ

LINQ operators generally use lambda expressions to control their processing and output. Some operators use IEqualityComparer<T> implementations to compare values. This article describes a generic comparer, driven by delegates, designed for use in queries.

IEqualityComparer<T>

In an earlier article I described how you can implement the IEqualityComparer<T> interface in order to create a class that compares values and determines if they are equal. Instances of such a class can be passed to standard .NET framework methods to allow complete control over the comparisons that they perform. For example, if you have a collection of Customer objects, you may wish to find all of the unique customers. Using Language Integrated Query's (LINQ) Distinct method without a comparer would generate a sequence of unique object references. With a custom comparer, you could specify that only the Id property defines uniqueness and eliminate all customers with duplicate IDs.

If you are using LINQ's standard query operators most of the actions that you execute will likely be defined using lambda expressions. The results of your query may often be returned as a sequence of anonymous type instances. It may be that the only named types that you use are for objects such as IEqualityComparer<T> instances that modify the internal behaviour of the LINQ extension methods. Whilst you are working with lambda expressions, you may decide that it would be better to use a generic comparer that can also be controlled with such delegates, rather than implementing multiple comparers, each with a slightly different effect. This article describes such a generic comparer, named LambdaComparer<T>.

Creating the LambdaEqualityComparer<T> Class

Let's begin by creating the class declaration for the comparer. In the downloadable code I've created this within a console application project. In a real-world scenario you might include the class in a dynamic linked library.

The declaration is shown below. The class is defined as public with a single generic type parameter. It implements IEqualityComparer<T> so that it can be used with LINQ operators and other classes within the .NET framework that accept such a type.

public class LambdaEqualityComparer<T> : IEqualityComparer<T>
{
}

Adding the Constructor

The comparer's functionality is set when it is instantiated by passing two lambda expressions to its constructor. The first accepts two objects of the type defined in the type parameter, "T". It defines how the two objects are compared and returns a Boolean value that is true if they match. The second delegate accepts one object and returns its hash code. Both lambda expressions are stored in private fields for later use. The code below should be added to the class to define the fields and the constructor. Note that the two Func delegates are validated to ensure neither is null.

NB: When you use the comparer you must ensure that the lambda expressions follow the rules defined for IEqualityComparer<T>. For example, the hash codes for two matching items must be the same and the order of comparison of two objects must not matter.

Func<T, T, bool> _equalsFunction;
Func<T, int> _hashCodeFunction;

public LambdaEqualityComparer(
    Func<T, T, bool> equalsFunction, Func<T, int> hashCodeFunction)
{
    if (equalsFunction == null) throw new ArgumentNullException();
    if (hashCodeFunction == null) throw new ArgumentNullException();

    _equalsFunction = equalsFunction;
    _hashCodeFunction = hashCodeFunction;
}

Implementing the Interface

We now need to implement the two methods described in the IEqualityComparer<T> interface. These call the appropriate stored Func delegate and return the results.

public bool Equals(T x, T y)
{
    return _equalsFunction(x, y);
}

public int GetHashCode(T obj)
{
    return _hashCodeFunction(obj);
}

Testing the LambdaComparer

In order to test the new comparer we need a class to compare. We'll use the same Customer class that was defined in the earlier article:

public class Customer
{
    Guid _id;
    string _forename;
    string _surname;

    public Guid Id
    {
        get { return _id; }
        set { _id = value; }
    }

    public string Forename
    {
        get { return _forename; }
        set { _forename = value; }
    }

    public string Surname
    {
        get { return _surname; }
        set { _surname = value; }
    }
}

If you have read the previous article you will have seen the test code, where three customers were created and compared, and where their hash codes were calculated to show that matching customers had the same hash code. We can recreate that code using the LambdaEqualityComparer<T> class, as shown below. Note the two arguments passed to the comparer's constructor, which define the actions of the Equals and GetHashCode methods.

Customer customer1 = new Customer();
customer1.Id = new Guid("12345678901234567890123456789012");
customer1.Forename = "Bob";

Customer customer2 = new Customer();
customer2.Id = new Guid("12345678901234567890123456789012");
customer2.Surname = "Smith";

Customer customer3 = new Customer();
customer3.Id = new Guid("11111111111111111111111111111111");
customer3.Forename = "Bob";

var comparer = new LambdaEqualityComparer<Customer>(
    (x, y) => x.Id == y.Id, x => x.Id.GetHashCode());

bool same1and2 = comparer.Equals(customer1, customer2);  // true
bool same1and3 = comparer.Equals(customer1, customer3);  // false

int hash1 = comparer.GetHashCode(customer1);             // -1876532676
int hash2 = comparer.GetHashCode(customer2);             // -1876532676
int hash3 = comparer.GetHashCode(customer3);             // 285212689

We can also try passing the comparer to a standard query operator. The code below creates a list containing the three customers. It then obtains a distinct sequence from the list. As the first and second customers have the same ID, customer2 is not present in the resultant sequence.

var customers = new List<Customer> { customer1, customer2, customer3 };
var uniqueCustomers = customers.Distinct(comparer);      // Returns customer1 and customer3
16 January 2012