понедельник, 24 сентября 2012 г.

Lambda IEqualityComparer


You’ve probably used an IEqualityComparer before.  If you have, you probably know that using one is a pain because you need to implement the interface in your own class and override your Equals and GetHashCode methods.  In this post, we’ll focus on the most common scenario for checking equality: primary key comparisons.  For example, the CustomerID property of a Customer class.  This article offers a simple solution inspired by a StackOverflow post.  We’ll use lambda expressions instead of subclassing IEqualityComparer for each class, thus eliminating lots of code and making everything far more readable.
A simple example should clarify the situation.  Here’s the traditional way you’d use an IEqualityComparer to compare two Customer objects.
First extend from IEqualityComparer:
public class CustomerComparer : IEqualityComparer
{
    public bool Equals(Customer x, Customer y)
    {
        return x.CustomerID == y.CustomerID;
    }
 
    public int GetHashCode(Customer obj)
    {
        return obj.CustomerID.GetHashCode();
    }
}
Then you would use this new CustomerComparer class like so:
// First parameter == CustomerID
var x = new Customer(1);
var y = new Customer(2);
 
var list = new List()
{
    new Customer(1),
    new Customer(2)
};
 
list.Contains(new Customer(1), new CustomerComparer());
As you can see, quite a bit of code just to find an object in a collection. (In this example, if we had not used our CustomerComparer, we would not have been able to find this new instance of the same customer in our list.)  Most of the time, the comparisons we want to make are pretty simple and could be expressed using a single line of code. Allow me to introduce my class (strongly inspired from this StackOverflow post) before showing a code example. This class allows us to pass a simple lambda expression to construct an IEqualityComparer on the fly:
public class KeyEqualityComparer : IEqualityComparer
{
    private readonly Funcbool
> comparer;
    private readonly Funcobject
> keyExtractor;
 
    // Allows us to simply specify the key to compare with: y => y.CustomerID
    public KeyEqualityComparer(Funcobject
> keyExtractor) : this(keyExtractor, null) { }
    // Allows us to tell if two objects are equal: (x, y) => y.CustomerID == x.CustomerID
    public KeyEqualityComparer(Funcbool
> comparer) : this(null, comparer) { }
 
    public KeyEqualityComparer(Funcobject
> keyExtractor, Funcbool> comparer)
    {
        this.keyExtractor = keyExtractor;
        this.comparer = comparer;
    }
 
    public bool Equals(T x, T y)
    {
        if (comparer != null)
            return comparer(x, y);
        else
        {
            var valX = keyExtractor(x);
            if (valX is IEnumerable<object>) // The special case where we pass a list of keys
                return ((IEnumerable<object>)valX).SequenceEqual((IEnumerable<object>)keyExtractor(y));
 
            return valX.Equals(keyExtractor(y));
        }
    }
 
    public int GetHashCode(T obj)
    {
        if (keyExtractor == null)
            return obj.ToString().ToLower().GetHashCode();
        else
        {
            var val = keyExtractor(obj);
            if (val is IEnumerable<object>) // The special case where we pass a list of keys
                return (int)((IEnumerable<object>)val).Aggregate((x, y) => x.GetHashCode() ^ y.GetHashCode());
 
            return val.GetHashCode();
        }
    }
}
Here is how you would use this code:
var x = new Customer(1);
var y = new Customer(2);
 
var list = new List()
{
    new Customer(1),
    new Customer(2)
};
 
list.Contains(new Customer(1), z => z.CustomerID);
To be able to use this in Contains, we need to add a new extension method (which was not included in the original StackOverflow post):
public static bool Contains(this IEnumerable list, T item, Funcobject
> keyExtractor)
{
    return list.Contains(item, new KeyEqualityComparer(keyExtractor));
}
Once you’ve done this work for Contains, it is very easy to create extensions methods for Distinct, Except, Union, etc. Furthermore, the same principles also apply toIComparer.
For a complete source code with all the classes and extension methods, you can go here: http://gist.github.com/391397
You can modify this code by forking the gist; don’t be shy to add useful methods!

Комментариев нет: