web-dev-qa-db-fra.com

Puis-je spécifier mon comparateur de type explicite en ligne?

Ainsi .NET 3.0/3.5 nous offre de nombreuses nouvelles façons d'interroger, trier et manipuler des données, grâce à toutes les fonctions intéressantes fournies avec LINQ. Parfois, j'ai besoin de comparer des types définis par l'utilisateur qui n'ont pas d'opérateur de comparaison intégré. Dans de nombreux cas, la comparaison est vraiment simple - quelque chose comme foo1.key? = Foo2.key. Plutôt que de créer un nouveau IEqualityComparer pour le type, puis-je simplement spécifier la comparaison en ligne à l'aide de fonctions délégués/lambda anonymes? Quelque chose comme:

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

Je suis presque sûr que ce qui précède ne fonctionne pas vraiment. Je ne veux pas avoir à faire quelque chose d'aussi "lourd" que toute une classe juste pour dire au programme comment comparer les pommes aux pommes.

55
Coderer

Ma bibliothèque MiscUtil contient un ProjectionComparer pour construire un IComparer <T> à partir d'un délégué de projection. Ce serait le travail de 10 minutes pour faire un ProjectionEqualityComparer de faire la même chose.

EDIT: Voici le code pour 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));
    }
}

Et voici un exemple d'utilisation:

var f3 = f1.Except(f2, ProjectionEqualityComparer<Foo>.Create(a => a.key));
63
Jon Skeet

voici une classe d'aide simple qui devrait faire ce que vous voulez

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; }
}

vous pouvez l'utiliser comme ceci:

processed.Union(suburbs, new EqualityComparer<Suburb>((s1, s2)
    => s1.SuburbId == s2.SuburbId));
19
mike

Je trouve que fournir des aides supplémentaires sur IEnumerable est une façon plus propre de le faire.

Voir: cette question

Vous pourriez donc avoir:

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

Si vous définissez correctement les méthodes d'extension

8
Sam Saffron

Ce projet fait quelque chose de similaire: AnonymousComparer - sélecteur de comparaison lambda pour Linq , il a également des extensions pour les opérateurs de requête standard LINQ.

6
Jeremy Thomas

Pourquoi pas quelque chose comme:

    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();
        }
    }

et ensuite vous pourriez faire par exemple quelque chose comme (par exemple dans le cas de Intersect dans IEnumerable<T>):

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

La classe Comparer peut être placée dans un projet d'utilitaires et utilisée partout où cela est nécessaire.

Je ne vois que maintenant la réponse de Sam Saffron (qui est très similaire à celle-ci).

5
Tamas Ionut

S'appuyant sur d'autres réponses, la création d'un comparateur générique était celle que j'aimais le plus. Mais j'ai un problème avec Linq Enumerable.Union ( référence msdn .Net ), c'est-à-dire qu'il utilise directement le GetHashCode sans tenir compte du remplacement Equals.

Cela m'a amené à implémenter le Comparer en tant que:

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);
    }
}

L'utiliser comme ceci:

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

Notez que la comparaison peut donner un faux positif car les informations comparées sont mappées à une valeur int.

1
OriolBG

Pour les petits ensembles, vous pouvez faire:

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

Pour les grands ensembles, vous voudrez quelque chose de plus efficace dans la recherche comme:

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

Mais, ici, le Type de clé doit implémenter IEqualityComparer (ci-dessus, j'ai supposé que c'était un string). Donc, cela ne répond pas vraiment à votre question sur l'utilisation d'un lambda dans cette situation, mais il utilise moins de code que certaines des réponses qui le font.

Vous pouvez compter sur l'optimiseur et raccourcir la deuxième solution pour:

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

mais, je n'ai pas exécuté de tests pour savoir s'il fonctionne à la même vitesse. Et cette doublure pourrait être trop intelligente pour être entretenue.

1
mheyman