2009-04-04 8 views
27

IMPORTANT: CECI EST PAS UN LINQ-TO-SQL QUESTION. C'est LINQ aux objets.Pouvez-vous créer un simple 'EqualityComparer <T>' en utilisant une expression lambda

courte question:

est-il un moyen simple LINQ à des objets pour obtenir une liste distincte des objets à partir d'une liste basée sur une propriété clé sur les objets.

longue question:

Je suis en train de faire une opération Distinct() sur une liste des objets qui ont une clé comme une de leurs propriétés.

class GalleryImage { 
    public int Key { get;set; } 
    public string Caption { get;set; } 
    public string Filename { get; set; } 
    public string[] Tags {g et; set; } 
} 

J'ai une liste des objets qui contiennent GalleryGalleryImage[]. En raison de la façon dont fonctionne le service web, j'ai des doublons de l'objet GalleryImage. Je pensais qu'il serait simple d'utiliser Distinct() pour obtenir une liste distincte.

Ceci est la requête LINQ Je veux utiliser:

var allImages = Galleries.SelectMany(x => x.Images); 
var distinctImages = allImages.Distinct<GalleryImage>(new 
        EqualityComparer<GalleryImage>((a, b) => a.id == b.id)); 

Le problème est que EqualityComparer est une classe abstraite.

Je ne veux:

  • mettre en œuvre IEquatable sur GalleryImage car il est généré
  • doivent écrire une catégorie distincte pour mettre en œuvre IEqualityComparer comme shown here

Y at-il une mise en œuvre concrète de EqualityComparer quelque part que je manque?

J'aurais pensé qu'il y aurait un moyen facile d'obtenir des objets «distincts» d'un ensemble basé sur une clé.

Répondre

33

(Il y a deux solutions ici - voir la fin de la seconde):

Ma bibliothèque MiscUtil a une ProjectionEqualityComparer classe (et deux classes de support pour faire usage de l'inférence de type).

Voici un exemple d'utilisation:

EqualityComparer<GalleryImage> comparer = 
    ProjectionEqualityComparer<GalleryImage>.Create(x => x.id); 

Voici le code (commentaires supprimés)

// Helper class for construction 
public static class ProjectionEqualityComparer 
{ 
    public static ProjectionEqualityComparer<TSource, TKey> 
     Create<TSource, TKey>(Func<TSource, TKey> projection) 
    { 
     return new ProjectionEqualityComparer<TSource, TKey>(projection); 
    } 

    public static ProjectionEqualityComparer<TSource, TKey> 
     Create<TSource, TKey> (TSource ignored, 
           Func<TSource, TKey> projection) 
    { 
     return new ProjectionEqualityComparer<TSource, TKey>(projection); 
    } 
} 

public static class ProjectionEqualityComparer<TSource> 
{ 
    public static ProjectionEqualityComparer<TSource, TKey> 
     Create<TKey>(Func<TSource, TKey> projection) 
    { 
     return new ProjectionEqualityComparer<TSource, TKey>(projection); 
    } 
} 

public class ProjectionEqualityComparer<TSource, TKey> 
    : IEqualityComparer<TSource> 
{ 
    readonly Func<TSource, TKey> projection; 
    readonly IEqualityComparer<TKey> comparer; 

    public ProjectionEqualityComparer(Func<TSource, TKey> projection) 
     : this(projection, null) 
    { 
    } 

    public ProjectionEqualityComparer(
     Func<TSource, TKey> projection, 
     IEqualityComparer<TKey> comparer) 
    { 
     projection.ThrowIfNull("projection"); 
     this.comparer = comparer ?? EqualityComparer<TKey>.Default; 
     this.projection = projection; 
    } 

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

    public int GetHashCode(TSource obj) 
    { 
     if (obj == null) 
     { 
      throw new ArgumentNullException("obj"); 
     } 
     return comparer.GetHashCode(projection(obj)); 
    } 
} 

solution Deuxième

Pour ce faire, juste pour Distinct, vous pouvez utiliser l'extension DistinctBy dans MoreLINQ:

public static IEnumerable<TSource> DistinctBy<TSource, TKey> 
     (this IEnumerable<TSource> source, 
     Func<TSource, TKey> keySelector) 
    { 
     return source.DistinctBy(keySelector, null); 
    } 

    public static IEnumerable<TSource> DistinctBy<TSource, TKey> 
     (this IEnumerable<TSource> source, 
     Func<TSource, TKey> keySelector, 
     IEqualityComparer<TKey> comparer) 
    { 
     source.ThrowIfNull("source"); 
     keySelector.ThrowIfNull("keySelector"); 
     return DistinctByImpl(source, keySelector, comparer); 
    } 

    private static IEnumerable<TSource> DistinctByImpl<TSource, TKey> 
     (IEnumerable<TSource> source, 
     Func<TSource, TKey> keySelector, 
     IEqualityComparer<TKey> comparer) 
    { 
     HashSet<TKey> knownKeys = new HashSet<TKey>(comparer); 
     foreach (TSource element in source) 
     { 
      if (knownKeys.Add(keySelector(element))) 
      { 
       yield return element; 
      } 
     } 
    } 

Dans les deux cas, ThrowIfNull ressemble à ceci:

public static void ThrowIfNull<T>(this T data, string name) where T : class 
{ 
    if (data == null) 
    { 
     throw new ArgumentNullException(name); 
    } 
} 
+0

Excellente info! Merci! –

+0

Quel est le but de la seconde méthode statique Create avec le paramètre 'TSource ignored'? ProjectionEqualityComparer public static Créer (TSource ignoré, Func projection ) {return new ProjectionEqualityComparer (projection); } – flesh

+1

@flesh: Cela permet à l'inférence de type de se déclencher lorsque vous ne pouvez pas spécifier explicitement le type - par ex. pour les types anonymes. –

2

Vous pouvez regrouper par la valeur de clé, puis sélectionner l'élément supérieur de chaque groupe. Cela marcherait-il pour toi?

+0

oui je suis juste en train de regarder ça en réalité - via le ToLookup(). peut-être inefficace et lent mais correct pour cette tâche. poster ma déclaration ci-dessus/ci-dessous –

3

C'est le meilleur que je peux trouver pour le problème en main. Encore curieux de savoir s'il existe une bonne façon de créer un EqualityComparer à la volée.

Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First()); 

Créer table de consultation et prendre « top » de chacun

Note: ceci est le même que celui @charlie suggéré, mais en utilisant iLookup - qui je pense est ce qu'un groupe doit être de toute façon.

+0

Je suis d'accord qu'il semble que le cadre manque quelque chose. Je ne sais pas si c'est IEqualityComparer si ... il faut vraiment les deux méthodes. Il semble qu'il devrait y avoir un moyen plus simple d'utiliser Distinct: un override qui prend un prédicat. –

+0

Pas un prédicat. Je veux dire un remplacement de Distinct qui prendrait T et vous laisser choisir l'objet que vous voulez utiliser pour le caractère distinctif. –

+0

@charlie - à droite, c'est ce que je pensais réellement que j'allais obtenir avec l'existant Distinct (..). Je ne l'avais jamais utilisé dans ce contexte avant, et bien sûr il s'est avéré ne pas être ce que je m'attendais –

2

Qu'en est-il un jeter IEqualityComparer générique?

public class ThrowAwayEqualityComparer<T> : IEqualityComparer<T> 
{ 
    Func<T, T, bool> comparer; 

    public ThrowAwayEqualityComparer<T>(Func<T, T, bool> comparer) 
    { 
    this.comparer = comparer; 
    } 

    public bool Equals(T a, T b) 
    { 
    return comparer(a, b); 
    } 

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

Vous pouvez maintenant utiliser Distinct.

var distinctImages = allImages.Distinct(
    new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key)); 

Vous pourriez être en mesure de sortir avec le <GalleryImage>, mais je ne suis pas sûr que le compilateur peut déduire le type (ne pas y avoir accès dès maintenant.)

Et dans un méthode d'extension supplémentaire:

public static class IEnumerableExtensions 
{ 
    public static IEnumerable<TValue> Distinct<TValue>(this IEnumerable<TValue> @this, Func<TValue, TValue, bool> comparer) 
    { 
    return @this.Distinct(new ThrowAwayEqualityComparer<TValue>(comparer); 
    } 

    private class ThrowAwayEqualityComparer... 
} 
+0

Plutôt bien. Ensuite, vous pouvez également implémenter le remplacement de Distinct que je souhaitais. –

+0

Oui, vous pourriez facilement faire cela et obtenir ce que vous vouliez. – Samuel

+0

Mais n'installez-vous pas encore IEqualityComparer . On aurait dit que tu ne voulais pas faire ça. –

4

Miser sur la réponse de Charlie Flowers, vous pouvez créer votre propre méthode d'extension pour faire ce que vous voulez qui utilise en interne groupement:

public static IEnumerable<T> Distinct<T, U>(
     this IEnumerable<T> seq, Func<T, U> getKey) 
    { 
     return 
      from item in seq 
      group item by getKey(item) into gp 
      select gp.First(); 
    } 

Vous pouvez également créer une classe générique provenant de EqualityComparer, mais il semble que vous souhaitez éviter cela:

public class KeyEqualityComparer<T,U> : IEqualityComparer<T> 
    { 
     private Func<T,U> GetKey { get; set; } 

     public KeyEqualityComparer(Func<T,U> getKey) { 
      GetKey = getKey; 
     } 

     public bool Equals(T x, T y) 
     { 
      return GetKey(x).Equals(GetKey(y)); 
     } 

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

Voici un article intéressant qui va LINQ à cette fin ... http://www.singingeels.com/Articles/Extending_LINQ__Specifying_a_Property_in_the_Distinct_Function.aspx

la valeur par défaut Distinct compare les objets en fonction de leur hashcode - pour faire facilement vos objets travailler avec Distinct, vous pouvez remplacer la méthode GetHashCode .. mais vous avez dit que vous récupérez vos objets à partir d'un service Web, de sorte que vous ne pouvez pas être en mesure pour faire cela dans ce cas.

0

mettre en œuvre IEquatable sur GalleryImage car il est généré

Une autre approche serait de générer GalleryImage en tant que classe partielle, puis un autre fichier avec l'héritage et IEquatable, Égale, mise en œuvre getHash.

0

Cette idée est débattue here, et pendant que je suis en espérant que le.NET équipe de base adopte une méthode pour générer IEqualityComparer<T> s de lambda, je vous conseille de bien vouloir voter et commenter cette idée, et utiliser les éléments suivants:

Utilisation:

IEqualityComparer<Contact> comp1 = EqualityComparerImpl<Contact>.Create(c => c.Name); 
var comp2 = EqualityComparerImpl<Contact>.Create(c => c.Name, c => c.Age); 

class Contact { public Name { get; set; } public Age { get; set; } } 

code:

public class EqualityComparerImpl<T> : IEqualityComparer<T> 
{ 
    public static EqualityComparerImpl<T> Create(
    params Expression<Func<T, object>>[] properties) => 
    new EqualityComparerImpl<T>(properties); 

    PropertyInfo[] _properties; 
    EqualityComparerImpl(Expression<Func<T, object>>[] properties) 
    { 
    if (properties == null) 
     throw new ArgumentNullException(nameof(properties)); 

    if (properties.Length == 0) 
     throw new ArgumentOutOfRangeException(nameof(properties)); 

    var length = properties.Length; 
    var extractions = new PropertyInfo[length]; 
    for (int i = 0; i < length; i++) 
    { 
     var property = properties[i]; 
     extractions[i] = ExtractProperty(property); 
    } 
    _properties = extractions; 
    } 

    public bool Equals(T x, T y) 
    { 
    if (ReferenceEquals(x, y)) 
     //covers both are null 
     return true; 
    if (x == null || y == null) 
     return false; 
    var len = _properties.Length; 
    for (int i = 0; i < _properties.Length; i++) 
    { 
     var property = _properties[i]; 
     if (!Equals(property.GetValue(x), property.GetValue(y))) 
     return false; 
    } 
    return true; 
    } 

    public int GetHashCode(T obj) 
    { 
    if (obj == null) 
     return 0; 

    var hashes = _properties 
     .Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray(); 
    return Combine(hashes); 
    } 

    static int Combine(int[] hashes) 
    { 
    int result = 0; 
    foreach (var hash in hashes) 
    { 
     uint rol5 = ((uint)result << 5) | ((uint)result >> 27); 
     result = ((int)rol5 + result)^hash; 
    } 
    return result; 
    } 

    static PropertyInfo ExtractProperty(Expression<Func<T, object>> property) 
    { 
    if (property.NodeType != ExpressionType.Lambda) 
     throwEx(); 

    var body = property.Body; 
    if (body.NodeType == ExpressionType.Convert) 
     if (body is UnaryExpression unary) 
     body = unary.Operand; 
     else 
     throwEx(); 

    if (!(body is MemberExpression member)) 
     throwEx(); 

    if (!(member.Member is PropertyInfo pi)) 
     throwEx(); 

    return pi; 

    void throwEx() => 
     throw new NotSupportedException($"The expression '{property}' isn't supported."); 
    } 
} 
Questions connexes