2009-02-06 21 views
46

Quiero obtener los valores distintos en una lista, pero no en la comparación de igualdad estándar.¿Por qué no hay un método Linq para devolver valores distintos por un predicado?

Lo que quiero hacer es algo como esto:

return myList.Distinct((x, y) => x.Url == y.Url); 

no puedo, no hay ningún método de extensión de LINQ que hará esto - sólo uno que toma un IEqualityComparer.

puedo hackear alrededor de ella con esto:

return myList.GroupBy(x => x.Url).Select(g => g.First()); 

pero que parece desordenado. Tampoco hace exactamente lo mismo: solo puedo usarlo aquí porque tengo una sola clave.

También podría añadir mi propia:

public static IEnumerable<T> Distinct<T>( 
    this IEnumerable<T> input, Func<T,T,bool> compare) 
{ 
    //write my own here 
} 

Pero eso parece más bien como escribir algo que debería estar allí en primer lugar.

¿Alguien sabe por qué este método no está allí?

¿Echo de menos algo?

Respuesta

50

Es molesto, sin duda. También es parte de mi proyecto "MoreLINQ" al que debo prestar atención en algún momento :) Hay muchas otras operaciones que tienen sentido cuando actúas en una proyección, pero devuelves el original: MaxBy y MinBy me vienen a la mente.

Como usted dice, es fácil de escribir - aunque me quedo con el nombre de "DistinctBy" para que coincida con OrdenarPor etc. Aquí está mi aplicación si está interesado:

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

    public static IEnumerable<TSource> DistinctBy<TSource, TKey> 
     (this IEnumerable<TSource> source, 
     Func<TSource, TKey> keySelector, 
     IEqualityComparer<TKey> comparer) 
    { 
     if (source == null) 
     { 
      throw new ArgumentNullException("source"); 
     } 
     if (keySelector == null) 
     { 
      throw new ArgumentNullException("keySelector"); 
     } 
     if (comparer == null) 
     { 
      throw new ArgumentNullException("comparer"); 
     } 
     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; 
      } 
     } 
    } 
+0

Gracias por la respuesta rápida. ¡Podría usar eso! ¿Alguna idea de por qué se saltaron todos estos ... por métodos (predicados)? – Keith

+0

No realmente, me temo. Escribiré sobre el proyecto MoreLinq cuando tenga un conjunto significativo de características ... básicamente será un proyecto de código abierto con extensiones para LINQ to Objects, y probablemente también Push LINQ. –

+7

Si tuviera que adivinar, supondría la paridad con las opciones IQueryable , y lo que es realista (sin enfermar) en TSQL. Así que DISTINCT (table.column) está bien, pero necesitaría una clave útil y un TSQL más complejo para DistinctBy ... –

31

Pero eso parece desordenado.

No es sucio, es correcto.

  • Si quieres Distinct programadores por nombre y hay cuatro David, ¿cuál quieres?
  • Si usted Group programadores por nombre y toma el First, entonces está claro lo que quiere hacer en el caso de cuatro David.

Solo puedo usarlo aquí porque tengo una sola tecla.

Usted puede hacer un múltiplo tecla "distinta" con el mismo patrón:

return myList 
    .GroupBy(x => new { x.Url, x.Age }) 
    .Select(g => g.First()); 
+0

No había pensado en usar tipos de anon como ese - es una buena idea (+1) – Keith

+0

Bueno, corto, simple, limpio: D +1 – Cerbrus

+0

Todos respondieron con sus propias implementaciones de DistinctBy(), pero este es el solo uno que aborda la pregunta real de POR QUÉ no hay DistinctBy() en LINQ. ¡Gracias! – josh2112

3

Jon, su solución es bastante bueno. Sin embargo, un pequeño cambio. No creo que necesitemos EqualityComparer.Default allí.Aquí está mi solución (por supuesto el punto de partida fue la solución de Jon Skeet)

public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector) 
    { 
     //TODO All arg checks 
     HashSet<TKey> keys = new HashSet<TKey>(); 
     foreach (T item in source) 
     { 
      TKey key = keySelector(item); 
      if (!keys.Contains(key)) 
      { 
       keys.Add(key); 
       yield return item; 
      } 
     } 
    } 
+2

No estoy seguro de por qué esto sería mejor que la solución de Jon. 'new HashSet ()' usará 'EqualityComparer .Default' de todos modos y al hacerlo a tu manera, perderás la posibilidad de anularlo (por ejemplo, si' TKey' es 'string' y quieres insensibilidad de mayúsculas y minúsculas). También Jon usa el método 'HashSet.Add', mientras usa' HashSet.Contains' y luego 'HashSet.Add' - dos operaciones. Es cierto que necesitaría un conjunto masivo para notar la diferencia, pero ¿por qué hacerlo más lento? – Keith

0

Usando @DavidB 's answer, he escrito un pequeño método de extensión DistinctBy, para permitir un predicado que se pasa:

/// <summary> 
/// Distinct method that accepts a perdicate 
/// </summary> 
/// <typeparam name="TSource">The type of the t source.</typeparam> 
/// <typeparam name="TKey">The type of the t key.</typeparam> 
/// <param name="source">The source.</param> 
/// <param name="predicate">The predicate.</param> 
/// <returns>IEnumerable&lt;TSource&gt;.</returns> 
/// <exception cref="System.ArgumentNullException">source</exception> 
public static IEnumerable<TSource> DistinctBy<TSource, TKey> 
    (this IEnumerable<TSource> source, 
    Func<TSource, TKey> predicate) 
{ 
    if (source == null) 
     throw new ArgumentNullException("source"); 

    return source 
     .GroupBy(predicate) 
     .Select(x => x.First()); 
} 

ahora puede pasar a un predicado a agrupar la lista por:

var distinct = myList.DistinctBy(x => x.Id); 

o grupo de múltiples propiedades:

var distinct = myList.DistinctBy(x => new { x.Id, x.Title }); 
Cuestiones relacionadas