2010-02-08 7 views
6

Ho un'istruzione LINQ che estrae gli ID record N più importanti da una raccolta e quindi un'altra query che recupera tutti i record che contengono tali ID. Ci si sente molto goffo e inefficiente e mi chiedevo se ci potrebbe essere un, LINQy modo più succinto per ottenere gli stessi risultatiUtilizzo di LINQ per ottenere i risultati da un'altra raccolta LINQ

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

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

CRONACA - non ci sarà più record con lo stesso ID, che è il motivo per cui v'è la Distinto() e perché non posso usare un semplice Take() in primo luogo.

Grazie!

risposta

4

Che ne dici di qualcosa di simile?

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

Questo è fantastico. Lavori. LINQy. Probabilmente non sarà più veloce dell'originale, potrebbe essere più lento a seconda di cosa succede con GroupBy. – David

+0

Sarà sempre un po 'più lento dell'originale perché deve eseguire un passaggio completo la prima volta; l'originale può fermarsi quando colpisce * n * elementi. Probabilmente non è un grosso problema se la lista è piccola. – Aaronaught

+0

@Aaronaught: ma l'originale deve eseguire un passaggio completo nella seconda query, * e * eseguire una ricerca 'Contains' ad ogni passaggio. Quello potrebbe potenzialmente essere un vero killer delle prestazioni. Ovviamente, l'unico modo per sapere con certezza è di fare un benchmark con i dati del mondo reale. – LukeH

0

Sì, purtroppo LINQ non supporta nativamente il fatto di consentire all'utente di scegliere un membro per ottenere record distinti. Quindi ti consiglio di creare il tuo metodo di estensione per questo:

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

E quindi creare un comparatore generico, che noterai è abbastanza generico.

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 
    } 

Ora basta catena fino vostra dichiarazione LINQ:. record var = cache.Select (rec => rec.Id) .Distinct() Prendere (n);

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

hth

+0

Non penso che questo vi darà gli stessi risultati. Questo mi sembra che ti darebbe solo n risultati - il primo elemento con ogni ID distinto, piuttosto che tutti gli elementi corrispondenti ai primi n ID (cioè forse più di n) – David

1

La stessa cosa che hai fatto, ma in una linea e con Join() al posto di Contiene():

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

L'unico modo che posso pensare di fare questo in SQL sarebbe con una sottoquery, quindi probabilmente ci saranno anche due query LINQ ...
"Sembra" inefficiente ... vero? Forse ti stai preoccupando di qualcosa di cui non vale la pena preoccuparsi. È possibile farlo in una riga facendo un join, ma se ciò è più chiaro/migliore/più efficiente è una domanda diversa.

Edit: Il metodo di estensione risposta da Aaronaught può essere fatto funzionare in questo modo:

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

Tuttavia, il GroupBy/SelectMany sembra il più grazioso. Vorrei andare con quello.

+0

Il tuo metodo di estensione sarà più efficiente se utilizzare 'HashSet ' piuttosto che 'Lista ' per la raccolta chiavi. Le ricerche 'Contains' dovrebbero essere vicine a O (1) per' HashSet ', rispetto a O (n) per' Elenco '. – LukeH

+0

Non ho testato l'efficienza ma la ricerca "Contiene" nella seconda istruzione sembra essere un collo di bottiglia. Questo è quello che stava per me. Per lo più sapevo solo che c'erano modi migliori per fare la stessa cosa ed ero curioso di sapere cosa avrebbero dovuto dire le persone. Non avevo idea che avrei avuto tante buone idee! :-) – Josh

+0

Totalmente, grazie. Tendo ad andare prima con semplici, poi ottimizzo più tardi, ma per questo tipo di cose (solo operazioni totalmente impostate) si dovrebbe probabilmente usare Set types dal get go. Grazie – David

0

Non c'è modo "Linqy" built-in (si potrebbe gruppo, ma sarebbe piuttosto inefficiente), ma questo non significa che non si può fare il proprio modo:

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

Poi è possibile richiamare con questo:

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

Se si dispone di chiavi composte o qualcosa di simile si potrebbe facilmente estendere il metodo di cui sopra per prendere un IEqualityComparer<TKey> come argomento.

Si noti inoltre che ciò dipende dal fatto che gli elementi si trovano nell'ordine ordinato per chiave.Se non lo sono, si potrebbe o cambia l'algoritmo di cui sopra per usare un HashSet<TKey> invece di un confronto conteggio e ultimo elemento dritto, o invocare con questo, invece:

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

Edit - Mi piacerebbe anche Fai notare che in SQL avrei usato una query ROW_NUMBER o una CTE ricorsiva, a seconda dei requisiti di prestazione: un + join è diverso da il metodo più efficiente. Se la cache è in ordine ordinato (o se è possibile modificarla in ordine ordinato), il metodo sopra riportato sarà di gran lunga il più economico in termini di tempo di memoria e di esecuzione.

+0

Penso che questo sia vicino, ma non fornirà i primi (fino a) n elementi con la prima chiave trovata ? Ritengo che questo sia vicino, ma è necessario cambiarlo per mantenere un elenco dei tasti così come vengono rilevati e aggiungere solo nuove chiavi a quell'elenco finché non ci sono n chiavi nell'elenco. Continua a scorrere l'intera lista producendo elementi che corrispondono alle chiavi (o che sono una nuova chiave fino a n, come detto). Post scriptum Penso che il tuo modo di farlo sia buono altrimenti :) – David

+0

@David - Non sei sicuro di cosa intendi - a meno che non ci sia un bug, questa estensione dovrebbe restituire tutti gli elementi nell'origine con le prime N chiavi distinte (purché siano ordinate ordine, altrimenti è un'operazione O (N) e hai bisogno di un hash set, nel qual caso forse andrei solo con la risposta 'GroupBy' /' SelectMany'). Penso che sia quello che voleva l'OP ... ho letto male la domanda? – Aaronaught

+0

Sì. Non penso che sia quello che volevano. Il tuo codice funzionerà solo su un elenco pre-ordinato, otterrà tutto il primo id, salterà il primo elemento con il secondo id e quindi restituirà solo max n elementi invece di tutti gli elementi con i primi n ID. a meno che non stia misundertanding OP. – David

Problemi correlati