LINQ Select Distinct avec des types anonymes

150

J'ai donc une collection d'objets. Le type exact n'est pas important. À partir de là, je veux extraire toutes les paires uniques d'une paire de propriétés particulières, ainsi:

myObjectCollection.Select(item=>new
                                {
                                     Alpha = item.propOne,
                                     Bravo = item.propTwo
                                }
                 ).Distinct();

Donc ma question est: Will Distinct dans ce cas utilisera l'objet par défaut equals (ce qui me sera inutile, puisque chaque objet est nouveau) ou peut-on lui dire de faire un égal différent (dans ce cas, des valeurs égales d'Alpha et Bravo => instances égales)? Y a-t-il un moyen d'atteindre ce résultat, si cela ne le fait pas?

GWLlosa
la source
Est-ce LINQ-to-Objects ou LINQ-to-SQL? Si juste des objets, vous n'avez probablement pas de chance. Cependant, si L2S, cela peut fonctionner, car le DISTINCT serait passé à l'instruction SQL.
James Curran

Réponses:

188

Lisez ici l'excellent article de K. Scott Allen:

Et l'égalité pour tous ... les types anonymes

La réponse courte (et je cite):

Il s'avère que le compilateur C # remplace Equals et GetHashCode pour les types anonymes. L'implémentation des deux méthodes substituées utilise toutes les propriétés publiques du type pour calculer le code de hachage d'un objet et tester l'égalité. Si deux objets du même type anonyme ont tous les mêmes valeurs pour leurs propriétés - les objets sont égaux.

Il est donc totalement sûr d'utiliser la méthode Distinct () sur une requête qui renvoie des types anonymes.

Matt Hamilton
la source
2
Cela n'est vrai, je pense, que si les propriétés elles-mêmes sont des types de valeur ou implémentent l'égalité des valeurs - voir ma réponse.
tvanfosson le
Oui, car il utilise GetHashCode sur chaque propriété, il ne fonctionnerait que si chaque propriété avait sa propre implémentation unique. Je pense que la plupart des cas d'utilisation n'impliqueraient que des types simples en tant que propriétés, donc c'est généralement sûr.
Matt Hamilton
4
Cela finit par signifier que l'égalité de deux des types anonymes dépend de l'égalité des membres, ce qui est bien pour moi, puisque les membres sont définis quelque part où je peux arriver et remplacer l'égalité si je le dois. Je ne voulais tout simplement pas avoir à créer une classe pour cela juste pour remplacer les égaux.
GWLlosa
3
Cela pourrait valoir la peine de demander à MS d'introduire la syntaxe "clé" dans C # que VB a (où vous pouvez spécifier certaines propriétés d'un type anonyme comme étant la "clé primaire" - voir le billet de blog auquel j'ai lié).
Matt Hamilton
1
Article très intéressant. Merci!
Alexander Prokofyev
14
public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();

Désolé pour le formatage gâché plus tôt


la source
Ces extensions ne peuvent pas gérer le type objectet object. Si les deux objectsont, stringil renvoie toujours les lignes en double. Essayez le FirstNametype objectet attribuez-y le même string.
CallMeLaNN
C'est une excellente réponse pour les objets typés mais pas nécessaire pour les types anonymes.
crokusek
5

Intéressant que cela fonctionne en C # mais pas en VB

Renvoie les 26 lettres:

var MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ";
MyBet.ToCharArray()
.Select(x => new {lower = x.ToString().ToLower(), upper = x.ToString().ToUpper()})
.Distinct()
.Dump();

Renvoie 52 ...

Dim MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
MyBet.ToCharArray() _
.Select(Function(x) New With {.lower = x.ToString.ToLower(), .upper = x.ToString.ToUpper()}) _
.Distinct() _
.Dump()
GeorgeBarker
la source
11
Si vous ajoutez le Keymot - clé au type anonyme, .Distinct()cela fonctionnera comme prévu (par exemple New With { Key .lower = x.ToString.ToLower(), Key .upper = x.ToString.ToUpper()}).
Cᴏʀʏ
3
Cory a raison. La traduction correcte du code C # new {A = b}est New {Key .A = b}. Les propriétés non clés des classes VB anonymes sont modifiables, c'est pourquoi elles sont comparées par référence. En C #, toutes les propriétés des classes anonymes sont immuables.
Heinzi
4

J'ai fait un petit test et j'ai trouvé que si les propriétés sont des types de valeur, cela semble fonctionner correctement. S'il ne s'agit pas de types valeur, le type doit fournir ses propres implémentations Equals et GetHashCode pour que cela fonctionne. Les cordes, je pense, fonctionneraient.

Tvanfosson
la source
2

Vous pouvez créer votre propre méthode d'extension distincte qui prend une expression lambda. Voici un exemple

Créer une classe qui dérive de l'interface IEqualityComparer

public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

Ensuite, créez votre méthode d'extension distincte

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

et vous pouvez utiliser cette méthode pour trouver des éléments distincts

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();
Buildstarted
la source
Ces extensions ne peuvent pas gérer le type objectet object. Si les deux objectsont, stringil renvoie toujours les lignes en double. Essayez le FirstNametype objectet attribuez-y le même string.
CallMeLaNN
0

Si Alphaet les Bravodeux héritent d'une classe commune, vous pourrez dicter le contrôle d'égalité dans la classe parente en implémentant IEquatable<T>.

Par exemple:

public class CommonClass : IEquatable<CommonClass>
{
    // needed for Distinct()
    public override int GetHashCode() 
    {
        return base.GetHashCode();
    }

    public bool Equals(CommonClass other)
    {
        if (other == null) return false;
        return [equality test];
    }
}
ern
la source
donc si vous utilisez comme propriétés de vos classes de types anonymes qui implémentent IEquatable <T>, Equals est appelé à la place du comportement par défaut (vérification de toutes les propriétés publiques via la réflexion?)
D_Guidi
0

Hé là, j'ai le même problème et j'ai trouvé une solution. Vous devez implémenter l'interface IEquatable ou simplement remplacer les méthodes (Equals & GetHashCode). Mais ce n'est pas l'astuce, l'astuce venant de la méthode GetHashCode. Vous ne devez pas renvoyer le code de hachage de l'objet de votre classe mais vous devez renvoyer le hachage de la propriété que vous souhaitez comparer comme ça.

public override bool Equals(object obj)
    {
        Person p = obj as Person;
        if ( obj == null )
            return false;
        if ( object.ReferenceEquals( p , this ) )
            return true;
        if ( p.Age == this.Age && p.Name == this.Name && p.IsEgyptian == this.IsEgyptian )
            return true;
        return false;
        //return base.Equals( obj );
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

Comme vous le voyez, j'ai obtenu une classe appelée personne a 3 propriétés (nom, âge, IsEgyptian "Parce que je suis") Dans le GetHashCode, j'ai renvoyé le hachage de la propriété Name et non l'objet Person.

Essayez-le et cela fonctionnera ISA. Merci, Modather Sadik

Modather Sadik
la source
1
GetHashCode doit utiliser tous les mêmes champs et propriétés que ceux utilisés dans la comparaison pour l'égalité, pas seulement l'un d'entre eux. iepublic override int GetHashCode() { return this.Name.GetHashCode() ^ this.Age.GetHashCode() ^ this.IsEgyptian.GetHashCode(); }
JG dans SD
Pour plus d'informations sur la génération d'un bon algorithme de hachage: stackoverflow.com/questions/263400/...
JG in SD
0

Pour que cela fonctionne dans VB.NET, vous devez spécifier le Keymot - clé avant chaque propriété du type anonyme, comme ceci:

myObjectCollection.Select(Function(item) New With
{
    Key .Alpha = item.propOne,
    Key .Bravo = item.propTwo
}).Distinct()

J'avais du mal avec cela, je pensais que VB.NET ne supportait pas ce type de fonctionnalité, mais en fait, c'est le cas.

Alisson
la source