2010-02-08 6 views
6

У меня есть оператор LINQ, который извлекает верхние N идентификаторов записи из коллекции, а затем еще один запрос, который извлекает все записи, которые имеют эти идентификаторы. Он чувствует себя очень неуклюжим и неэффективным, и мне было интересно, если там может быть более лаконичным, LINQy способ получить те же результатыИспользование LINQ для получения результатов из другой коллекции LINQ

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

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

FYI - будет несколько записей с одинаковым идентификатором, поэтому существует Distinct() и почему я не могу использовать простой Take() в первую очередь.

Спасибо!

ответ

4

Как насчет чего-то подобного?

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

Это замечательно. Работает. LINQy. Вероятно, не будет быстрее, чем ваш оригинал, может быть медленнее в зависимости от того, что происходит с GroupBy. – David

+0

Он всегда будет немного медленнее оригинала, потому что он должен сделать полный проход в первый раз; оригинал может остановиться, когда он попадает * n * элементов. Вероятно, это не серьезная проблема, если список небольшой. – Aaronaught

+0

@Aaronaught: Но оригинал должен выполнить полный проход во втором запросе, * и * выполнять поиск «Содержит» на каждом шаге. Это потенциально может быть настоящим убийцей производительности. Конечно, единственный способ узнать наверняка - это сравнить с реальными данными. – LukeH

0

Да, к сожалению, LINQ не поддерживает, позволяя пользователю выбрать член, чтобы получать различные записи. Поэтому я рекомендую создать свой собственный метод расширения для нее:

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

А затем создать общий компаратор, который вы заметите довольно общий характер.

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 
    } 

Теперь только приковать ваше заявление LINQ:. Var записи = cache.Select (гес => rec.Id) .distinct() Возьмем (п);

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

НТН

+0

Я не думаю, что это даст вам те же результаты. Мне кажется, что это даст вам только n результатов - первый элемент с каждым отдельным идентификатором, а не все элементы, соответствующие первым n идентификаторам (т. Е. Возможно больше, чем n). – David

1

То же самое, что вы сделали, но в одной строке и с Join() вместо Содержит():

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

Единственный способ, которым я могу думать, делать это в SQL будет с подзапросом, поэтому, вероятно, будут также два запроса LINQ ...
Это «чувствует» неэффективность ... не так ли? Возможно, вы беспокоитесь о чем-то, о чем не стоит беспокоиться. Вы можете, вероятно, сделать это в одну строку, выполнив объединение, но вопрос о том, является ли это более ясным/лучшим/эффективным, - это другой вопрос.

Edit: Метод расширения ответ на Aaronaught можно работать так:

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

Однако GroupBy/SelectMany выглядит опрятная. Я бы пошел с этим.

+0

. Ваш метод расширения будет более эффективным, если вы используйте 'HashSet ', а не 'List ' для коллекции ключей. Поиск 'Contains' должен быть близок к O (1) для' HashSet ', по сравнению с O (n) для' List '. – LukeH

+0

Я не тестировал эффективность, но поиск «Содержит» во втором выражении кажется, что это может быть узким местом. Это то, что торчало для меня. В основном, хотя я только знал, что было бы лучше сделать то же самое, и было любопытно, что люди должны будут сказать. Я понятия не имел, что получаю так много хороших идей! :-) – Josh

+0

Полностью, спасибо. Сначала я предпочитаю простой, а затем оптимизирую позже, но для этого типа вещей (только для полностью установленных операций) следует, вероятно, использовать Set types from get go. Thanks – David

0

Существует нет встроенного способа «Linqy» (вы могли бы группу, но это было бы очень неэффективно), но это не значит, что вы не можете сделать свой собственный путь:

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

Тогда вы можете вызвать его с этим:

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

Если у вас есть составные ключи или что-нибудь подобное, что вы могли бы легко распространить метод выше, чтобы принять IEqualityComparer<TKey> в качестве аргумента.

Также обратите внимание, что это зависит от того, какие элементы находятся в отсортированном порядке по ключу.Если это не так, вы можете либо изменить алгоритм выше использовать HashSet<TKey> вместо прямого счета и последний пункт сравнения, или вызвать его вместо этого:

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

Edit - Я хотел бы также укажите, что в SQL я либо использовал бы запрос ROW_NUMBER, либо рекурсивный CTE, в зависимости от требования к производительности - четкое + объединение - это , а не - самый эффективный метод. Если ваш кеш находится в отсортированном порядке (или если вы можете изменить его в отсортированном порядке), то указанный выше метод будет самым дешевым с точки зрения как памяти, так и времени выполнения.

+0

Я думаю, что это близко, но это не даст первые (до) n элементов с первым найденным ключом ? Я чувствую, что это близко, просто нужно изменить его, чтобы сохранить список ключей, как они встречаются, и добавлять только новые ключи в этот список, пока в списке не появится n ключей. Продолжайте просматривать весь список, уступая элементам, которые соответствуют ключам (или это новый ключ до n, как уже упоминалось). Постскриптум Я думаю, что ваш способ сделать это хорошо иначе :) – David

+0

@David - не совсем уверен, что вы имеете в виду - если нет ошибки, это расширение должно возвращать все элементы в источнике с помощью первых N отдельных ключей (если они отсортированы порядок, иначе это операция O (N), и вам нужен хэш-набор, и в этом случае, возможно, я просто перейду с ответом 'GroupBy' /' SelectMany'). Я думаю, это то, чего хотел OP ... я неправильно прочитал вопрос? – Aaronaught

+0

Да. Я не думаю, что это то, что они хотели. Ваш код будет работать только в предварительно отсортированном списке, он получит весь первый идентификатор, пропустит первый элемент со вторым идентификатором, а затем вернет только max n элементов, а не все элементы с первыми n идентификаторами. если я не ошибаюсь в ОП. – David