2010-02-08 10 views
6

Tengo una declaración de LINQ que tira de los mejores ID de registros N de una colección y luego otra consulta que tira de todos los registros que tienen esas identificaciones. Se siente muy torpe e ineficiente, y me preguntaba si podría haber una forma más sucinta, LINQy para obtener los mismos resultadosmediante LINQ para obtener los resultados de otra colección de LINQ

var records = cache.Select(rec => rec.Id).Distinct().Take(n); 

var results = cache.Where(rec => records.Contains(rec.Id)); 

FYI - habrá múltiples registros con el mismo ID, por lo que existe la distinct() y por qué no puedo utilizar una simple toma() en el primer lugar.

Gracias!

Respuesta

4

¿Qué tal algo así?

var results = cache.GroupBy(rec => rec.Id, rec => rec) 
        .Take(n) 
        .SelectMany(rec => rec); 
+0

Esto es genial. Trabajos. LINQy. Probablemente no sea más rápido que el original, puede ser más lento según lo que ocurra con GroupBy. – David

+0

Siempre será un poco más lento que el original porque tiene que hacer un pase completo la primera vez; el original puede detenerse cuando golpea * n * elementos. Probablemente no sea un problema importante si la lista es pequeña. – Aaronaught

+0

@Aaronaught: Pero el original tiene que hacer un pase completo en la segunda consulta, * y * realizar una búsqueda 'Contiene' en cada paso. Eso podría ser un asesino de rendimiento real. Por supuesto, la única manera de saber con seguridad es comparar con datos del mundo real. – LukeH

0

Sí, Unfortuately LINQ no soporta de forma nativa dejar que el usuario elija un miembro para obtener registros distintos sucesivamente. Así que recomiendo la creación de su propio método de extensión para el mismo:

/// <summary> 
    /// Returns a list with the ability to specify key(s) to compare uniqueness on 
    /// </summary> 
    /// <typeparam name="T">Source type</typeparam> 
    /// <param name="source">Source</param> 
    /// <param name="keyPredicate">Predicate with key(s) to perform comparison on</param> 
    /// <returns></returns> 
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source, 
              Func<T, object> keyPredicate) 
    { 
     return source.Distinct(new GenericComparer<T>(keyPredicate)); 
    } 

y luego crear un comparador genérico, que se dará cuenta es bastante genérico.

public class GenericComparer<T> : IEqualityComparer<T> 
    { 
     private Func<T, object> _uniqueCheckerMethod; 

     public GenericComparer(Func<T, object> keyPredicate) 
     { 
      _uniqueCheckerMethod = keyPredicate; 
     } 

     #region IEqualityComparer<T> Members 

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

     int IEqualityComparer<T>.GetHashCode(T obj) 
     { 
      return _uniqueCheckerMethod(obj).GetHashCode(); 
     } 

     #endregion 
    } 

Ahora acaba de encadenar hasta su declaración de LINQ:. registros var = cache.Select (rec => rec.Id) .Distinct() Tome (n);

var results = cache.Distinct(rec => rec.Id).Take(n)); 

hth

+0

No creo que esto le dará los mismos resultados. Esto me parece que le daría n resultados solamente: el primer elemento con cada ID distinta, en lugar de todos los elementos que coinciden con los primeros n ID (es decir, posiblemente más que n) – David

1

El mismo que tú, pero en una línea y con Join() en lugar de Contiene():

var results = cache 
    .Select(rec => rec.Id) 
    .Distinct() 
    .Take(n) 
    .ToList() 
    .Join(cache, rec => rec, record => record.Id, (rec, record) => record); 
0

La única manera que se me ocurre hacer esto en SQL sería con una subconsulta, por lo que probablemente no van a ser dos consultas LINQ también ...
se "siente" ineficiente ... ¿verdad? Tal vez estés preocupado por algo que no vale la pena preocuparse. Puede probly lo hace en una sola línea al hacer una combinación, pero si eso es más claro/mejor/más eficiente es una cuestión diferente.

Editar: La respuesta método de extensión por Aaronaught puede ser hecho para trabajar de esta manera:

public static IEnumerable<T> TakeByDistinctKey<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keyFunc, int numKeys) { 
    if(keyFunc == null) { 
     throw new ArgumentNullException("keyFunc"); 
    } 

    List<TKey> keys = new List<TKey>(); 
    foreach(T item in source) { 
     TKey key = keyFunc(item); 
     if(keys.Contains(key)) { 
      // one if the first n keys, yield 
      yield return item; 
     } else if(keys.Count < numKeys) { 
      // new key, but still one of the first n seen, yield 
      keys.Add(key); 
      yield return item; 
     } 
     // have enough distinct keys, just keep going to return all of the items with those keys 
    } 
} 

Sin embargo, el GroupBy/SelectMany ve el más bonito. Yo iría con eso.

+0

Su método de extensión será más eficiente si use 'HashSet ' en lugar de 'List ' para la colección de claves. Las búsquedas 'Contiene' deben estar cerca de O (1) para' HashSet ', en comparación con O (n) para' List '. – LukeH

+0

No he probado la eficacia, pero la búsqueda "Contiene" en la segunda declaración parece que podría ser un cuello de botella. Eso es lo que estaba sobresaliendo para mí. En su mayoría, solo sabía que habría mejores formas de hacer lo mismo y tenía curiosidad por saber qué diría la gente. ¡No tenía idea de que obtendría tantas buenas ideas! :-) – Josh

+0

Totalmente, gracias. Tiendo a ir primero con simple luego optimizo más tarde, pero para este tipo de cosas (operaciones totalmente configuradas solamente) uno probablemente debería usar Set types desde el principio. Gracias – David

0

No hay manera "Linqy" incorporado (puede agrupar, pero sería muy ineficiente), pero eso no significa que no pueda hacer su propio camino:

public static IEnumerable<T> TakeDistinctByKey<T, TKey>(
    this IEnumerable<T> source, 
    Func<T, TKey> keyFunc, 
    int count) 
{ 
    if (keyFunc == null) 
     throw new ArgumentNullException("keyFunc"); 
    if (count <= 0) 
     yield break; 

    int currentCount = 0; 
    TKey lastKey = default(TKey); 
    bool isFirst = true; 
    foreach (T item in source) 
    { 
     yield return item; 
     TKey key = keyFunc(item); 
     if (!isFirst && (key != lastKey)) 
      currentCount++; 
     if (currentCount > count) 
      yield break; 
     isFirst = false; 
     lastKey = key; 
    } 
} 

Entonces se puede invocar con esto:

var items = cache.TakeDistinctByKey(rec => rec.Id, 20); 

Si tiene claves compuestas o algo así que fácilmente se podría extender el método anterior para tomar una IEqualityComparer<TKey> como argumento.

También tenga en cuenta que esto depende de que los elementos estén ordenados por clave.Si no es así, usted podría o bien cambia el algoritmo anterior para utilizar un HashSet<TKey> en lugar de un recuento y último elemento de comparación directa, o invoca con este lugar:

var items = cache.OrderBy(rec => rec.Id).TakeDistinctByKey(rec => rec.Id, 20); 

Editar - También me gusta señalar que en SQL utilizaría una consulta ROW_NUMBER o un CTE recursivo, según los requisitos de rendimiento; una combinación + distinta es no el método más eficiente. Si su caché está en orden ordenado (o si puede cambiarlo para que esté en orden), el método anterior será de lejos el más barato en términos de memoria y tiempo de ejecución.

+0

Creo que está cerca, pero ¿esto no dará los primeros (hasta) n elementos con la primera clave encontrada? ? Siento que esto está cerca, solo necesito cambiarlo para mantener una lista de las claves a medida que se encuentran, y solo agregar nuevas claves a esa lista hasta que haya n teclas en la lista. Continúe repasando toda la lista y obtenga elementos que coincidan con las claves (o que sean una nueva clave hasta n, como se mencionó). PD Creo que tu manera de hacerlo es buena de lo contrario :) – David

+0

@David - no estoy seguro de lo que quieres decir - a menos que haya un error, esta extensión debe devolver todos los elementos en la fuente con las primeras N teclas distintas (siempre y cuando estén ordenadas orden, de lo contrario se trata de una operación O (N) y necesita un conjunto de hash, en cuyo caso tal vez simplemente vaya con la respuesta 'GroupBy' /' SelectMany'). Creo que eso es lo que quería el OP ... ¿Leí mal la pregunta? – Aaronaught

+0

Sí. No creo que eso sea lo que ellos querían. Su código solo funcionará en una lista preordenada, obtendrá la primera identificación, omitirá el primer elemento con la segunda identificación y luego solo devolverá máximos elementos en lugar de todos los artículos con las primeras n identificaciones. A menos que esté cometiendo errores en el PO. – David

Cuestiones relacionadas