Can I specify my explicit type comparator inline?
Asked Answered
D

9

71

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.

Demonetize answered 9/10, 2008 at 16:41 Comment(2)
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.Cantwell
Oddly they added Comparer<T>.Create to create a Comparer<T>, but no EqualityComparer<T>.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 : learn.microsoft.com/en-us/dotnet/api/…Zebra
L
78

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));
Land answered 9/10, 2008 at 16:43 Comment(12)
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?Demonetize
I'll do both - but it'll probably be a fair chunk of code due to convenience methods etc.Land
Now the only question is, why isn't this sort of thing built into the language?Demonetize
@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.Land
@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)Nonrecognition
@sambo99: I'm really not sure what you mean, I'm afraid.Land
@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...Nonrecognition
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.Land
Still needed today right? I don't understand why the framework added Comparer<T>.Create(...) but no corresponding EqualityComparer<T>.Create(...) (which is basically what your code is).Zebra
This may need modernizing based on this advice We recommend that you derive from the EqualityComparer<T> class instead of implementing the IEqualityComparer<T> interface, because the EqualityComparer<T> class tests for equality using the IEquatable<T>.Equals method instead of the Object.Equals method. This is consistent with the Contains, IndexOf, LastIndexOf, and Remove methods of the Dictionary<TKey,TValue> class and other generic collections., but that should only take another 10 minutes :-)Zebra
@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<T>?Land
@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.Zebra
S
24

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));
Sawhorse answered 23/5, 2012 at 12:46 Comment(4)
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.Goliath
@Goliath 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.Whity
@Whity 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.Goliath
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.Ignatius
N
11

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

Nonrecognition answered 13/4, 2009 at 7:6 Comment(1)
I wish we can do this without the extension methodsOmentum
O
9

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).

Onwards answered 1/4, 2015 at 14:24 Comment(1)
I think it's worth nothing that framework-included comparers can be used in conjunction with this approach (such as those provided by StringComparer: msdn.microsoft.com/en-us/library/…).Commensurate
A
7

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

Adlay answered 23/1, 2014 at 22:18 Comment(1)
This really deserves more upvotes. One of the most handy libraries I've come accros while using Lambda / LinqLavone
C
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!

Carcinogen answered 18/2, 2020 at 14:53 Comment(0)
A
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.

Ammeter answered 23/10, 2015 at 13:25 Comment(0)
B
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.

Buttaro answered 12/6, 2019 at 11:35 Comment(0)
A
-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 == "*");  
Agonist answered 26/7, 2018 at 23:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.