web-dev-qa-db-fra.com

Distinct () avec lambda?

C'est vrai, j'ai un énumérable et je souhaite en tirer des valeurs distinctes.

En utilisant System.Linq, il existe bien sûr une méthode d'extension appelée Distinct. Dans le cas simple, il peut être utilisé sans paramètre, comme:

var distinctValues = myStringList.Distinct();

Bien, mais si j'ai un énumérable d'objets pour lesquels je dois spécifier une égalité, la seule surcharge disponible est la suivante:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

L'argument du comparateur d'égalité doit être une instance de IEqualityComparer<T>. Je peux le faire, bien sûr, mais c'est un peu prolixe et, bien, confus.

Ce à quoi je m'attendais, c’est une surcharge qui prendrait un lambda, disons un Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Quelqu'un sait si une telle extension existe, ou une solution de contournement équivalente? Ou est-ce que je manque quelque chose?

Sinon, existe-t-il un moyen de spécifier un IEqualityComparer inline (gêne-moi)?

Mettre à jour

J'ai trouvé une réponse d'Anders Hejlsberg à un post dans un forum MSDN sur ce sujet. Il dit:

Le problème que vous allez rencontrer est que lorsque deux objets comparent égales, elles doivent avoir la même valeur de retour GetHashCode (sinon la table de hachage .__ utilisée en interne par Distinct ne fonctionnera pas correctement) . Nous utilisons IEqualityComparer car il est compatible avec les packages implémentations d'Equals et de GetHashCode dans une interface unique.

Je suppose que cela a du sens ..

669
Tor Haugen
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());
938
Carlo Bos

Il me semble que vous voulez DistinctBy from MoreLINQ . Vous pouvez alors écrire:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Voici une version simplifiée de DistinctBy (pas de vérification de la nullité et aucune option permettant de spécifier votre propre comparateur de clé):

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
     (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> knownKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (knownKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}
450
Jon Skeet

Pour envelopper les choses. Je pense que la plupart des gens qui sont venus ici comme moi veulent la solution la plus simple possible sans utiliser de bibliothèques et avec la meilleure performance.

(Je pense que le groupe accepté selon la méthode est excessif en termes de performances.)

Voici une méthode d'extension simple utilisant l'interface IEqualityComparer qui fonctionne également pour les valeurs NULL.

_ {Usage:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Code de méthode d'extension

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}
25

Non, il n'y a pas de surcharge de méthode d'extension pour cela. J'ai trouvé cela frustrant par le passé et, en tant que tel, j'écris généralement un cours d'aide pour traiter ce problème. L'objectif est de convertir un Func<T,T,bool> en IEqualityComparer<T,T>

Exemple

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Cela vous permet d’écrire ce qui suit

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));
19
JaredPar

Solution abrégée 

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());
14
Arasu RRK

Cela fera ce que vous voulez mais je ne connais pas la performance:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

Au moins ce n'est pas verbeux.

12
Gordon Freeman

Voici une méthode d'extension simple qui fait ce dont j'ai besoin ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

C'est dommage qu'ils n'aient pas intégré une méthode distincte comme celle-ci dans le cadre, mais hé ho.

10
David Kirkland

Quelque chose que j'ai utilisé qui a bien fonctionné pour moi.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}
4
Kleinux

Toutes les solutions que j'ai vues ici reposent sur la sélection d'un domaine déjà comparable. Si on a besoin de comparer d'une manière différente, cependant, cette solution ici semble fonctionner généralement, pour quelque chose comme

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()
3
Dmitry Ledentsov

Prenons une autre voie: 

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

Les éléments distincts de retour de séquence les comparent par la propriété '_myCaustomerProperty'.

3
Bob

Vous pouvez utiliser InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

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

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

Exemple d'utilisation:

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Source: https://stackoverflow.com/a/5969691/206730
Utilisation d’IEqualityComparer pour Union
Puis-je spécifier mon comparateur de type explicite en ligne?

2
Kiquenet

Vous pouvez utiliser LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

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

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

        private readonly Func<T, T, bool> _equalsFunction;
    }

Une méthode délicate consiste à utiliser l'extension Aggregate(), en utilisant un dictionnaire comme accumulateur avec les propriétés key-property values:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

Et un GroupBy-style solution utilise ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());
1
Arturo Menchaca

Voici comment vous pouvez le faire:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Cette méthode vous permet de l'utiliser en spécifiant un paramètre tel que .MyDistinct(d => d.Name), mais vous permet également de spécifier une condition préalable en tant que second paramètre, comme suit:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

N.B. Ceci vous permettrait également de spécifier d'autres fonctions, comme par exemple .LastOrDefault(...).


Si vous souhaitez exposer uniquement la condition, vous pouvez l'avoir encore plus simple en l'implémentant comme suit:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

Dans ce cas, la requête ressemblerait à ceci:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

N.B. Ici, l'expression est plus simple, mais notez que .MyDistinct2 utilise .FirstOrDefault(...) implicitement.


Remarque: Les exemples ci-dessus utilisent la classe de démonstration suivante

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};
0
Matt

Le package Microsoft System.Interactive a une version de Distinct qui utilise un sélecteur de clé lambda. C'est effectivement la même chose que la solution de Jon Skeet, mais il peut être utile que les gens le sachent et consultent le reste de la bibliothèque. 

0
Niall Connaughton

IEnumerable extension lambda:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Usage:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
0
Shantanu

Si Distinct() ne produit pas de résultats uniques, essayez celui-ci:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);
0
Andy Singh

Je suppose que vous avez un IEnumerable, et dans votre exemple de délégué, vous voudriez que c1 et c2 fassent référence à deux éléments de cette liste?

Je pense que vous pouvez y parvenir avec une auto-jointure Var distinctResults = à partir de c1 dans maListe rejoindre c2 dans ma liste sur 

0
MattH