69

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.

Coderer
  • 25,844
  • 28
  • 99
  • 154
  • 5
    For anyone just looking for the answer to the question of whether the language supports this, the answer is no. It requires some clever custom classes. – lehiester May 07 '18 at 23:26
  • Oddly they added `Comparer.Create` to create a Comparer, but no `EqualityComparer.Create()` which is what we need here. Also watch out for some of the remarks here that discuss whether to implement the interface or derive a new class from EqualityComparer : https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.equalitycomparer-1?view=netframework-4.7.2 – Simon_Weaver Feb 12 '19 at 04:53

9 Answers9

77

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
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 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 Oct 09 '08 at 17:10
  • I'll do both - but it'll probably be a fair chunk of code due to convenience methods etc. – Jon Skeet Oct 09 '08 at 18:27
  • 11
    Now the only question is, why isn't this sort of thing built into the language? – Coderer Nov 05 '08 at 15:17
  • 1
    @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 Nov 05 '08 at 15:40
  • @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 Apr 13 '09 at 07:07
  • @sambo99: I'm really not sure what you mean, I'm afraid. – Jon Skeet Apr 13 '09 at 11:16
  • 4
    @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 Apr 13 '09 at 13:31
  • 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 Apr 13 '09 at 16:50
  • Still needed today right? I don't understand why the framework added `Comparer.Create(...)` but no corresponding `EqualityComparer.Create(...)` (which is basically what your code is). – Simon_Weaver Feb 12 '19 at 04:16
  • This may need modernizing based on this advice `We recommend that you derive from the EqualityComparer class instead of implementing the IEqualityComparer interface, because the EqualityComparer class tests for equality using the IEquatable.Equals method instead of the Object.Equals method. This is consistent with the Contains, IndexOf, LastIndexOf, and Remove methods of the Dictionary class and other generic collections.`, but that should only take another 10 minutes :-) – Simon_Weaver Feb 12 '19 at 04:18
  • @Simon_Weaver: It's performing the actual comparisons with `EqualityComparer.Default` anyway. Can you point out a concrete example where this code would fail, but it would succeed if it derived from `EqualityComparer`? – Jon Skeet Feb 12 '19 at 06:55
  • @JonSkeet maybe I misunderstood the advise from the MSDN page and it’s not relevant here. Sorry :) this was very helpful answer btw especially the overloading of the class names with different generic parameters. – Simon_Weaver Feb 12 '19 at 07:09
23

here is a simple helper class that should do what you want

public class EqualityComparer<T> : IEqualityComparer<T>
{
    public EqualityComparer(Func<T, T, bool> cmp)
    {
        this.cmp = cmp;
    }
    public bool Equals(T x, T y)
    {
        return cmp(x, y);
    }

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

    public Func<T, T, bool> cmp { get; set; }
}

you can use it like this:

processed.Union(suburbs, new EqualityComparer<Suburb>((s1, s2)
    => s1.SuburbId == s2.SuburbId));
ahsteele
  • 26,243
  • 28
  • 134
  • 248
mike
  • 391
  • 3
  • 5
  • 13
    This doesn't work because Union and Distinct first check the hash code which can be different regardless of what the delegate says. Changing GetHashCode to always return 0 fixes the problem. – makhdumi Oct 10 '13 at 21:34
  • 4
    @makhdumi It may look like that is a "fix", but it will also destroy performance of these methods by turning them into O(n^2) operations. Proper implementation of `GetHashCode` is important. There is no way to do a proper implementation with the `cmp` function this answer uses. This answer and the above comment are both dangerously flawed. @jonskeets answer is much better. – MgSam Jun 16 '18 at 16:59
  • 1
    @MgSam You're right. I can't remember why I went this route but as you said, hashing is critical for checking sets. People, please don't do what I said. – makhdumi Jun 18 '18 at 05:30
  • Overriding GetHashCode is ok if you are only going to be working with small sets however if you are working with large sets then you do need a proper implementation of GetHashCode. – apc Apr 29 '20 at 10:17
10

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

Community
  • 1
  • 1
Sam Saffron
  • 128,308
  • 78
  • 326
  • 506
8

Why not something like:

    public class Comparer<T> : IEqualityComparer<T>
    {
        private readonly Func<T, T, bool> _equalityComparer;

        public Comparer(Func<T, T, bool> equalityComparer)
        {
            _equalityComparer = equalityComparer;
        }

        public bool Equals(T first, T second)
        {
            return _equalityComparer(first, second);
        }

        public int GetHashCode(T value)
        {
            return value.GetHashCode();
        }
    }

and then you could do for instance something like (e.g. in the case of Intersect in IEnumerable<T>):

list.Intersect(otherList, new Comparer<T>( (x, y) => x.Property == y.Property));

The Comparer class can be put in a utilities project and used wherever is needed.

I only now see the Sam Saffron's answer (which is very similar to this one).

Tamas Ionut
  • 4,240
  • 5
  • 36
  • 59
  • I think it's worth nothing that framework-included comparers can be used in conjunction with this approach (such as those provided by StringComparer: https://msdn.microsoft.com/en-us/library/system.stringcomparer(v=vs.110).aspx). – ryanwebjackson Apr 08 '18 at 21:31
7

So I know this is a workaround to your question, but when I find that I've run into the situation you have here (Combining a list and filtering duplicates), and Distinct needs an IEquityComparer that I don't have, I usually go with a Concat -> Group -> Select.

Original

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

New

var f1 = ...,
    f2 = ...;
var distinctF = f1
    .Concat(f2)                       // Combine the lists
    .GroupBy(x => x.key)              // Group them up by our equity comparison key
    .Select(x => x.FirstOrDefault()); // Just grab one of them.

Note that in the GroupBy() you have the opportunity to add logic to create hybrid keys like:

.GroupBy(f => new Uri(f.Url).PathAndQuery)  

As well as in the Select() if you want to want to specify which list the resulting item comes from you can say:

.Select(x => x.FirstOrDefault(y => f1.Contains(y))

Hope that helps!

WhiteleyJ
  • 1,393
  • 1
  • 22
  • 29
6

This project does something similar: AnonymousComparer - lambda compare selector for Linq, it has Extensions for LINQ Standard Query Operators as well.

Jeremy Thomas
  • 189
  • 2
  • 4
  • This really deserves more upvotes. One of the most handy libraries I've come accros while using Lambda / Linq – Nathan Feb 27 '15 at 07:38
1

For small sets, you can do:

f3 = f1.Where(x1 => f2.All(x2 => x2.key != x1.key));

For large sets, you will want something more efficient in the search like:

var tmp = new HashSet<string>(f2.Select(f => f.key));
f3 = f1.Where(f => tmp.Add(f.key));

But, here, the Type of key must implement IEqualityComparer (above I assumed it was a string). So, this doesn't really answer your question about using a lambda in this situation but it does use less code then some of the answers that do.

You might rely on the optimizer and shorten the second solution to:

f3 = f1.Where(x1 => (new HashSet<string>(f2.Select(x2 => x2.key))).Add(x1.key));

but, I haven't run tests to know if it runs at the same speed. And that one liner might be too clever to maintain.

mheyman
  • 4,211
  • 37
  • 34
1

Building on other answers the creation of a generic comparer was the one I liked most. But I got a problem with Linq Enumerable.Union (msdn .Net reference) which was that its using the GetHashCode directly without taking into account the Equals override.

That took me to implement the Comparer as:

public class Comparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, int> _hashFunction;

    public Comparer(Func<T, int> hashFunction)
    {
        _hashFunction = hashFunction;
    }

    public bool Equals(T first, T second)
    {
        return _hashFunction(first) == _hashFunction(second);
    }

    public int GetHashCode(T value)
    {
        return _hashFunction(value);
    }
}

Using it like this:

list.Union(otherList, new Comparer<T>( x => x.StringValue.GetHashCode()));

Note that comparison might give some false positive since information being compared is mapped to an int value.

OriolBG
  • 2,031
  • 2
  • 18
  • 21
-1

Like the other answers but more concise c# 7:

public class LambdaComparer<T> : IEqualityComparer<T> {
  private readonly Func<T, T, bool> lambdaComparer;
  private readonly Func<T, int> lambdaHash;
  public LambdaComparer(Func<T, T, bool> lambdaComparer) : this(lambdaComparer, o => o.GetHashCode()) {}
  public LambdaComparer(Func<T, T, bool> lambdaComparer, Func<T, int> lambdaHash) { this.lambdaComparer = lambdaComparer; this.lambdaHash = lambdaHash; }
  public bool Equals(T x, T y) => lambdaComparer is null ? false : lambdaComparer(x, y);
  public int GetHashCode(T obj) => lambdaHash is null ? 0 : lambdaHash(obj);
}

then:

var a=List<string> { "a", "b" };
var b=List<string> { "a", "*" };
return a.SequenceEquals(b, new LambdaComparer<string>((s1, s2) => s1 is null ? s2 is null : s1 == s2 || s2 == "*");  
kofifus
  • 17,260
  • 17
  • 99
  • 173