¿Distinto () con lambda?

746

Bien, entonces tengo un enumerable y deseo obtener valores distintos de él.

Usando System.Linq, por supuesto, hay un método de extensión llamado Distinct. En el caso simple, se puede usar sin parámetros, como:

var distinctValues = myStringList.Distinct();

Bien y bien, pero si tengo un número de objetos para los que necesito especificar igualdad, la única sobrecarga disponible es:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

El argumento del comparador de igualdad debe ser una instancia de IEqualityComparer<T>. Puedo hacer esto, por supuesto, pero es algo detallado y, bueno, deslucido.

Lo que hubiera esperado es una sobrecarga que tomaría una lambda, digamos una Func <T, T, bool>:

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

¿Alguien sabe si existe alguna extensión de este tipo, o alguna solución alternativa equivalente? ¿O me estoy perdiendo algo?

Alternativamente, ¿hay alguna forma de especificar un IEqualityComparer en línea (me avergüenza)?

Actualizar

Encontré una respuesta de Anders Hejlsberg a una publicación en un foro de MSDN sobre este tema. Él dice:

El problema con el que se encontrará es que cuando dos objetos se comparan iguales, deben tener el mismo valor de retorno GetHashCode (o la tabla hash utilizada internamente por Distinct no funcionará correctamente). Utilizamos IEqualityComparer porque empaqueta implementaciones compatibles de Equals y GetHashCode en una sola interfaz.

Supongo que tiene sentido..

Tor Haugen
fuente
2
consulte stackoverflow.com/questions/1183403/… para obtener una solución usando GroupBy
17
¡Gracias por la actualización de Anders Hejlsberg!
Tor Haugen
No, no tiene sentido: ¿cómo dos objetos que contienen valores idénticos pueden devolver dos códigos hash diferentes?
GY
Podría ayudar - solución para .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), y explicar por qué GetHashCode () es importante para que funcione correctamente.
marbel82
Duplicado relacionado / posible de: LINQ's Distinct () en una propiedad particular
Marc.2377

Respuestas:

1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());
Carlo Bos
fuente
12
¡Excelente! Esto también es realmente fácil de encapsular en un método de extensión, como DistinctBy(o incluso Distinct, ya que la firma será única).
Tomas Aschan
1
¡No me funciona! <El método 'Primero' solo puede usarse como una operación de consulta final. Considere usar el método 'FirstOrDefault' en esta instancia en su lugar.> Incluso intenté 'FirstOrDefault' no funcionó.
JatSing
63
@TorHaugen: Solo tenga en cuenta que hay un costo involucrado en la creación de todos esos grupos. Esto no puede transmitir la entrada y terminará almacenando en búfer todos los datos antes de devolver cualquier cosa. Eso puede no ser relevante para su situación, por supuesto, pero prefiero la elegancia de DistinctBy :)
Jon Skeet
2
@ JonSkeet: Esto es lo suficientemente bueno para los codificadores de VB.NET que no desean importar bibliotecas adicionales para una sola función. Sin ASync CTP, VB.NET no admite la yielddeclaración, por lo que técnicamente no es posible la transmisión. Gracias por tu respuesta sin embargo. Lo usaré cuando codifique en C #. ;-)
Alex Essilfie
2
@BenGripka: Eso no es lo mismo. Solo le proporciona los ID de cliente. Quiero a todo el cliente :)
ryanman
496

Me parece que quieres DistinctByde MoreLINQ . Entonces puedes escribir:

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

Aquí hay una versión reducida de DistinctBy(sin verificación de nulidad y sin opción para especificar su propio comparador de claves):

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;
        }
    }
}
Jon Skeet
fuente
14
Sabía que Jon Skeet publicaría la mejor respuesta simplemente leyendo el título de la publicación. Si tiene algo que ver con LINQ, Skeet es tu hombre. Lea 'C # In Depth' para obtener el conocimiento linq de Dios.
nocarrier
2
¡¡¡gran respuesta!!! también, para todos los VB_Complainers sobre la yieldlib extra +, foreach puede reescribirse comoreturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov el
55
@ sudhAnsu63 esta es una limitación de LinqToSql (y otros proveedores de linq). La intención de LinqToX es traducir su expresión lambda de C # al contexto nativo de X. Es decir, LinqToSql convierte su C # en SQL y ejecuta ese comando de forma nativa siempre que sea posible. Esto significa que cualquier método que resida en C # no se puede "pasar" a través de linqProvider si no hay forma de expresarlo en SQL (o cualquier proveedor de linq que esté utilizando). Veo esto en los métodos de extensión para convertir objetos de datos para ver modelos. Puede solucionar este problema "materializando" la consulta, llamando a ToList () antes de DistinctBy ().
Michael Blackburn
1
Y cada vez que vuelvo a esta pregunta, me pregunto por qué no adoptan al menos parte de MoreLinq en el BCL.
Shimmy Weitzhandler
2
@Shimmy: Ciertamente agradecería eso ... No estoy seguro de cuál es la factibilidad. Puedo levantarlo en la Fundación .NET aunque ...
Jon Skeet
39

Para terminar las cosas . Creo que la mayoría de las personas que vinieron aquí como yo quieren la solución más simple posible sin usar bibliotecas y con el mejor rendimiento posible .

(El grupo aceptado por método para mí, creo que es una exageración en términos de rendimiento).

Aquí hay un método de extensión simple que usa la interfaz IEqualityComparer que funciona también para valores nulos.

Uso:

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

Código de método de extensión

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();
    }
}
Anestis Kivranoglou
fuente
19

No, no existe tal sobrecarga del método de extensión para esto. He encontrado esto frustrante en el pasado y, como tal, generalmente escribo una clase auxiliar para tratar este problema. El objetivo es convertir un Func<T,T,bool>a IEqualityComparer<T,T>.

Ejemplo

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

Esto le permite escribir lo siguiente

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));
JaredPar
fuente
8
Sin embargo, eso tiene una desagradable implementación de código hash. Es más fácil crear IEqualityComparer<T>desde una proyección: stackoverflow.com/questions/188120/…
Jon Skeet
77
(Solo para explicar mi comentario sobre el código hash: es muy fácil con este código terminar con Equals (x, y) == true, pero GetHashCode (x)! = GetHashCode (y). Eso básicamente rompe algo como una tabla hash .)
Jon Skeet
Estoy de acuerdo con la objeción del código hash. Aún así, +1 para el patrón.
Tor Haugen
@ Jon, sí, estoy de acuerdo en que la implementación original de GetHashcode es menos que óptima (estaba siendo perezosa). Lo cambié para usar esencialmente ahora EqualityComparer <T> .Default.GetHashcode (), que es un poco más estándar. A decir verdad, lo único garantizado para que funcione la implementación de GetHashcode en este escenario es simplemente devolver un valor constante. Mata la búsqueda de tabla hash, pero se garantiza que es funcionalmente correcto.
JaredPar
1
@JaredPar: Exactamente. El código hash debe ser coherente con la función de igualdad que está utilizando, que presumiblemente no es la predeterminada, de lo contrario no se molestaría :) Por eso prefiero usar una proyección: puede obtener tanto la igualdad como un hash sensible codificar de esa manera. También hace que el código de llamada tenga menos duplicación. Es cierto que solo funciona en los casos en que desea la misma proyección dos veces, pero ese es cada caso que he visto en la práctica :)
Jon Skeet el
18

Solución abreviada

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());
Arasu RRK
fuente
1
¿Podría agregar alguna explicación de por qué esto se mejora?
Keith Pinson
En realidad, esto funcionó muy bien para mí cuando Konrad no lo hizo.
neoscribe
13

Esto hará lo que quieras pero no sé sobre rendimiento:

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

Al menos no es detallado.

Gordon Freeman
fuente
12

Aquí hay un método de extensión simple que hace lo que necesito ...

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

Es una pena que no hayan preparado un método distinto como este en el marco, pero bueno, ho

David Kirkland
fuente
esta es la mejor solución sin tener que agregar esa biblioteca morelinq.
toddmo
Sin embargo, tuve que cambiar x.Keya x.First()y cambiar el valor de retorno aIEnumerable<T>
toddmo
@toddmo Gracias por los comentarios :-) Sí, suena lógico ... Actualizaré la respuesta después de investigar más.
David Kirkland
1
nunca es demasiado tarde para agradecer la solución, simple y limpia
Ali
4

Algo que he usado y que me funcionó bien.

/// <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);
    }
}
Kleinux
fuente
@Mukus No estoy seguro de por qué preguntas por el nombre de la clase aquí. Necesitaba ponerle un nombre a la clase para poder implementar IEqualityComparer, así que solo le puse el prefijo My.
Kleinux
4

Todas las soluciones que he visto aquí se basan en seleccionar un campo ya comparable. Sin embargo, si uno necesita comparar de una manera diferente, esta solución aquí parece funcionar en general, para algo como:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()
Dmitry Ledentsov
fuente
¿Qué es LambdaComparer, de dónde importa eso?
Patrick Graham el
@PatrickGraham vinculado en la respuesta: brendan.enrick.com/post/…
Dmitry Ledentsov
3

Toma otra forma:

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

La secuencia devuelve elementos distintos y los compara por propiedad '_myCaustomerProperty'

Beto
fuente
1
Vine aquí para decir esto. ESTA debería ser la respuesta aceptada
Aún así
55
No, esta no debería ser la respuesta aceptada, a menos que todo lo que desee sean valores distintos de la propiedad personalizada. La pregunta general de OP era cómo devolver objetos distintos basados ​​en una propiedad específica del objeto.
Tomo
2

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

Muestra de uso :

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

Fuente: https://stackoverflow.com/a/5969691/206730
Uso de IEqualityComparer para Union
¿Puedo especificar mi comparador de tipos explícito en línea?

Kiquenet
fuente
2

Puede usar 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;
    }
Валентин Миронов
fuente
1

Una forma complicada de hacer esto es usar la Aggregate()extensión, usando un diccionario como acumulador con los valores de las propiedades clave como claves:

var customers = new List<Customer>();

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

Y una solución de estilo GroupBy está utilizando ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());
Arturo Menchaca
fuente
Bien, pero ¿por qué no simplemente crear un Dictionary<int, Customer>en su lugar?
ruffin
0

Supongo que tiene un IEnumerable, y en su delegado de ejemplo, ¿le gustaría que c1 y c2 se refieran a dos elementos en esta lista?

Creo que podría lograr esto con una autounión var distinctResults = de c1 en myList únete c2 en myList en

MattH
fuente
0

Si Distinct()no produce resultados únicos, prueba este:

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);
Andy Singh
fuente
0

El paquete Microsoft System.Interactive tiene una versión de Distinct que toma un selector de clave lambda. Esto es efectivamente lo mismo que la solución de Jon Skeet, pero puede ser útil que la gente sepa y revise el resto de la biblioteca.

Niall Connaughton
fuente
0

Así es como puedes hacerlo:

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

Este método le permite usarlo especificando un parámetro como .MyDistinct(d => d.Name), pero también le permite especificar una condición de tener como segundo parámetro de esta manera:

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

NB Esto también le permitiría especificar otras funciones como, por ejemplo .LastOrDefault(...), también.


Si desea exponer solo la condición, puede hacerlo aún más simple al implementarlo como:

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

En este caso, la consulta se vería así:

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

NB Aquí, la expresión es más simple, pero la nota se .MyDistinct2usa .FirstOrDefault(...)implícitamente.


Nota: Los ejemplos anteriores están utilizando la siguiente clase de demostración

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"}
};
Mate
fuente
0

IEnumerable extensión 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);
    }
}

Uso:

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()); 
Shantanu
fuente