Использование LINQ для получения результатов из другой коллекции LINQ

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

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

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

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

спасибо!

5 ответов


Как насчет чего-то вроде этого?

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

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

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

Да, к сожалению, 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 = кэш.Выберите (rec => rec.Id).Distinct().Возьмите(n);

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

hth


единственный способ, которым я могу думать об этом в 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 выглядит самым аккуратным. Я бы пошел с этим.


нет встроенного способа "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, в зависимости от требования к производительности - отличное+соединение не самый эффективный метод. Если ваш кэш находится в отсортированном порядке (или если вы можете изменить его, чтобы быть в отсортированном порядке), то метод выше будет намного дешевле в плане памяти и времени выполнения.