tags:

views:

1231

answers:

2

So .NET 3.0/3.5 provides us with lots of new ways to query, sort, and manipulate data, thanks to all the neat functions supplied with LINQ. Sometimes, I need to compare user-defined types that don't have a built-in comparison operator. In many cases, the comparison is really simple -- something like foo1.key ?= foo2.key. Rather than creating a new IEqualityComparer for the type, can I simply specify the comparison inline using anonymous delegates/lambda functions? Something like:

var f1 = ...,
    f2 = ...;
var f3 = f1.Except(
           f2, new IEqualityComparer(
             (Foo a, Foo b) => a.key.CompareTo(b.key)
           ) );

I'm pretty sure the above doesn't actually work. I just don't want to have to make something as "heavy" as a whole class just to tell the program how to compare apples to apples.

+5  A: 

My MiscUtil library contains a ProjectionComparer to build an IComparer<T> from a projection delegate. It would be the work of 10 minutes to make a ProjectionEqualityComparer to do the same thing.

EDIT: Here's the code for ProjectionEqualityComparer:

using System;
using System.Collections.Generic;

/// <summary>
/// Non-generic class to produce instances of the generic class,
/// optionally using type inference.
/// </summary>
public static class ProjectionEqualityComparer
{
    /// <summary>
    /// Creates an instance of ProjectionEqualityComparer using the specified projection.
    /// </summary>
    /// <typeparam name="TSource">Type parameter for the elements to be compared</typeparam>
    /// <typeparam name="TKey">Type parameter for the keys to be compared,
    /// after being projected from the elements</typeparam>
    /// <param name="projection">Projection to use when determining the key of an element</param>
    /// <returns>A comparer which will compare elements by projecting 
    /// each element to its key, and comparing keys</returns>
    public static ProjectionEqualityComparer<TSource, TKey> Create<TSource, TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }

    /// <summary>
    /// Creates an instance of ProjectionEqualityComparer using the specified projection.
    /// The ignored parameter is solely present to aid type inference.
    /// </summary>
    /// <typeparam name="TSource">Type parameter for the elements to be compared</typeparam>
    /// <typeparam name="TKey">Type parameter for the keys to be compared,
    /// after being projected from the elements</typeparam>
    /// <param name="ignored">Value is ignored - type may be used by type inference</param>
    /// <param name="projection">Projection to use when determining the key of an element</param>
    /// <returns>A comparer which will compare elements by projecting
    /// each element to its key, and comparing keys</returns>
    public static ProjectionEqualityComparer<TSource, TKey> Create<TSource, TKey>
        (TSource ignored,
         Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }

}

/// <summary>
/// Class generic in the source only to produce instances of the 
/// doubly generic class, optionally using type inference.
/// </summary>
public static class ProjectionEqualityComparer<TSource>
{
    /// <summary>
    /// Creates an instance of ProjectionEqualityComparer using the specified projection.
    /// </summary>
    /// <typeparam name="TKey">Type parameter for the keys to be compared,
    /// after being projected from the elements</typeparam>
    /// <param name="projection">Projection to use when determining the key of an element</param>
    /// <returns>A comparer which will compare elements by projecting each element to its key,
    /// and comparing keys</returns>        
    public static ProjectionEqualityComparer<TSource, TKey> Create<TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

/// <summary>
/// Comparer which projects each element of the comparison to a key, and then compares
/// those keys using the specified (or default) comparer for the key type.
/// </summary>
/// <typeparam name="TSource">Type of elements which this comparer 
/// will be asked to compare</typeparam>
/// <typeparam name="TKey">Type of the key projected
/// from the element</typeparam>
public class ProjectionEqualityComparer<TSource, TKey> : IEqualityComparer<TSource>
{
    readonly Func<TSource, TKey> projection;
    readonly IEqualityComparer<TKey> comparer;

    /// <summary>
    /// Creates a new instance using the specified projection, which must not be null.
    /// The default comparer for the projected type is used.
    /// </summary>
    /// <param name="projection">Projection to use during comparisons</param>
    public ProjectionEqualityComparer(Func<TSource, TKey> projection)
        : this(projection, null)
    {
    }

    /// <summary>
    /// Creates a new instance using the specified projection, which must not be null.
    /// </summary>
    /// <param name="projection">Projection to use during comparisons</param>
    /// <param name="comparer">The comparer to use on the keys. May be null, in
    /// which case the default comparer will be used.</param>
    public ProjectionEqualityComparer(Func<TSource, TKey> projection, IEqualityComparer<TKey> comparer)
    {
        if (projection == null)
        {
            throw new ArgumentNullException("projection");
        }
        this.comparer = comparer ?? EqualityComparer<TKey>.Default;
        this.projection = projection;
    }

    /// <summary>
    /// Compares the two specified values for equality by applying the projection
    /// to each value and then using the equality comparer on the resulting keys. Null
    /// references are never passed to the projection.
    /// </summary>
    public bool Equals(TSource x, TSource y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        if (x == null || y == null)
        {
            return false;
        }
        return comparer.Equals(projection(x), projection(y));
    }

    /// <summary>
    /// Produces a hash code for the given value by projecting it and
    /// then asking the equality comparer to find the hash code of
    /// the resulting key.
    /// </summary>
    public int GetHashCode(TSource obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }
        return comparer.GetHashCode(projection(obj));
    }
}

And here's a sample use:

var f3 = f1.Except(f2, ProjectionEqualityComparer<Foo>.Create(a => a.key));
Jon Skeet
While I appreciate the link, I think it might be a bit easier for people reading the answer to find out how to do this if you could paste a snippet here on SO, rather than needing to go to your site, download and unzip the source, then page through it looking for a specific class. Please?
Coderer
I'll do both - but it'll probably be a fair chunk of code due to convenience methods etc.
Jon Skeet
Now the only question is, why isn't this sort of thing built into the language?
Coderer
@Coderer: I wouldn't really expect it to be built into the language. It's more of a framework thing. It would be nice to have "ExceptBy" etc as extra bit of LINQ to Objects though.
Jon Skeet
@Jon, what are your thoughts on overloading IEnumerable to handle this ... perhaps a candidate for MiscUtil (provide lambda based functions for all the places that IEnumerable expects IEqualityComparer)
Sam Saffron
@sambo99: I'm really not sure what you mean, I'm afraid.
Jon Skeet
@Jon Eg. f1.Except(f2, (a, b) => a.key.CompareTo(b.key)); No need for the projected equality comparer, same for l.Distinct(a=>a.key) Etc...
Sam Saffron
I've already got DistinctBy in MoreLinq and possibly ExceptBy (haven't checked yet). I think providing the projection itself is simpler than providing the *comparison*.
Jon Skeet
This was quite the time-saver, thanks Jon!
Kon
+3  A: 

I find providing extra helpers on IEnumerable is a cleaner way to do this.

See: this question

So you could have:

var f3 = f1.Except(
           f2, 
             (a, b) => a.key.CompareTo(b.key)
            );

If you define the extension methods properly

Sam Saffron