Подтвердить что ты не робот

LINQ с запросом "Память"

Имеет ли LINQ способ "запоминать" свои предыдущие результаты запроса при запросе?

Рассмотрим следующий случай:

public class Foo {
    public int Id { get; set; }
    public ICollection<Bar> Bars { get; set; }
}

public class Bar {
    public int Id { get; set; }
}

Теперь, если два или более Foo имеют один и тот же набор Bar (независимо от порядка), они считаются похожими Foo.

Пример:

foo1.Bars = new List<Bar>() { bar1, bar2 };
foo2.Bars = new List<Bar>() { bar2, bar1 };
foo3.Bars = new List<Bar>() { bar3, bar1, bar2 };

В приведенном выше случае foo1 похож на foo2, но оба foo1 и foo2 не похожи на foo3

Учитывая, что мы имеем результат query, состоящий из IEnumerable или IOrderedEnumerable of Foo. Из query мы должны найти первый N Foo, который не является похожим.

Для этой задачи требуется память коллекции bars, которая была выбрана ранее.

С частичным LINQ мы могли бы сделать это следующим образом:

private bool areBarsSimilar(ICollection<Bar> bars1, ICollection<Bar> bars2) {
    return bars1.Count == bars2.Count && //have the same amount of bars
        !bars1.Select(x => x.Id)
        .Except(bars2.Select(y => y.Id))
        .Any(); //and when excepted does not return any element mean similar bar
}

public void somewhereWithQueryResult(){
    .
    .
    List<Foo> topNFoos = new List<Foo>(); //this serves as a memory for the previous query
    int N = 50; //can be any number
    foreach (var q in query) { //query is IOrderedEnumerable or IEnumerable
        if (topNFoos.Count == 0 || !topNFoos.Any(foo => areBarsSimilar(foo.Bars, q.Bars)))
            topNFoos.Add(q);
        if (topNFoos.Count >= N) //We have had enough Foo
            break;
    }
}

topNFoos List будет использоваться как память предыдущего запроса, и мы можем пропустить Foo q в цикле foreach, который уже имеет идентичный bars с Any Foo в topNFoos.

Мой вопрос: есть ли способ сделать это в LINQ (полностью LINQ)?

var topNFoos = from q in query
               //put something
               select q;

Если требуемая "память" относится к определенному элементу запроса q или переменной вне запроса, мы могли бы использовать переменную let для ее кеширования:

int index = 0;
var topNFoos = from q in query
               let qc = index++ + q.Id //depends on q or variable outside like index, then it is OK
               select q;

Но если это должно произойти из предыдущего запроса самого запроса, все начинает становиться более неприятным.

Есть ли способ сделать это?


Edit:

(Я в настоящее время создаю тестовый пример (github link) для ответов. Еще разобраться, как я могу проверить все ответы честно)

(Большинство ответов ниже направлены на решение моего конкретного вопроса и сами по себе хорошие (ответы Роба, Спайдера и Дэвида Б., которые используют IEqualityComparer, особенно удивительны). Тем не менее, если есть кто-нибудь, кто может дать ответ на мой более общий вопрос: "LINQ имеет способ" запомнить "свои предыдущие результаты запроса при запросе", я также был бы рад)

(Помимо существенной разницы в производительности для конкретного случая, представленного выше при использовании полного/частичного LINQ, один ответ, направленный на то, чтобы ответить на мой общий вопрос о памяти LINQ, - это Иван Стоев, другой с хорошей комбинацией - Роб. сделайте себя яснее, я ищу общее и эффективное решение, если оно есть, используя LINQ)

4b9b3361

Ответ 1

Итак, это... возможно. Но это далеко не показатель производительности.

var res = query.Select(q => new {
    original = q, 
    matches = query.Where(innerQ => areBarsSimilar(q.Bars, innerQ.Bars))
}).Select(g => new { original = g, joinKey = string.Join(",", g.matches.Select(m => m.Id)) })
.GroupBy (g => g.joinKey)
.Select(g => g.First().original.original)
.Take(N);

Это предполагает, что Id уникальны для каждого Foo (вы также можете использовать их GetHashCode(), я полагаю).

Гораздо лучшее решение - либо сохранить то, что вы сделали, либо реализовать пользовательский сопоставитель, как показано ниже:


Примечание. Как указано в комментариях @spender, ниже Equals и GetHashCode не будут работать для коллекций с дубликатами. Обратитесь к их ответу за лучшую реализацию - однако код использования останется тем же самым
class MyComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo left, Foo right)
    {
        return left.Bars.Count() == right.Bars.Count() && //have the same amount of bars
            left.Bars.Select(x => x.Id)
            .Except(right.Bars.Select(y => y.Id))
            .ToList().Count == 0; //and when excepted returns 0, mean similar bar
    }

    public int GetHashCode(Foo foo)
    {
        unchecked {
            int hc = 0;
            if (foo.Bars != null)
                foreach (var p in foo.Bars)
                hc ^= p.GetHashCode();
            return hc;
        }
    }
}

И тогда ваш запрос будет просто:

var res = query
    .GroupBy (q => q, new MyComparer())
    .Select(g => g.First())
    .Take(N);

Ответ 2

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

Сначала рассмотрим запись IEqualityComparer<Foo>, которая использует коллекцию Bars для измерения равенства. Здесь я предполагаю, что списки могут содержать повторяющиеся записи, поэтому имеют довольно строгое определение сходства:

public class FooSimilarityComparer:IEqualityComparer<Foo>
{
    public bool Equals(Foo a, Foo b)
    {
        //called infrequently
        return a.Bars.OrderBy(bar => bar.Id).SequenceEqual(b.Bars.OrderBy(bar => bar.Id));
    }
    public int GetHashCode(Foo foo)
    {
        //called frequently
        unchecked
        {
            return foo.Bars.Sum(b => b.GetHashCode());
        }
    }
}

Вы можете эффективно получить верхние N не похожие элементы, используя HashSet с IEqualityComparer выше:

IEnumerable<Foo> someFoos; //= some list of Foo
var hs = new HashSet<Foo>(new FooSimilarityComparer());
foreach(var f in someFoos)
{
    hs.Add(f); //hashsets don't add duplicates, as measured by the FooSimilarityComparer
    if(hs.Count >= 50)
    {
        break;
    }
}

Подход @Rob s выше аналогичен и показывает, как вы можете использовать компаратор непосредственно в LINQ, но обратите внимание на комментарии, которые я сделал для его ответа.

Ответ 3

IEnumerable<Foo> dissimilarFoos =
  from foo in query
  let key = string.Join('|',
    from bar in foo.Bars
    order by bar.Id
    select bar.Id.ToString())
  group foo by key into g
  select g.First();

IEnumerable<Foo> firstDissimilarFoos =
  dissimilarFoos.Take(50);

Иногда вам может не нравиться поведение groupby в вышеуказанных запросах. Во время перечисления запроса groupby будет перечислять весь источник. Если вам требуется только частичное перечисление, вы должны переключиться на Distinct и Comparer:

class FooComparer : IEqualityComparer<Foo>
{
  private string keyGen(Foo foo)
  {
    return string.Join('|',
      from bar in foo.Bars
      order by bar.Id
      select bar.Id.ToString());
  }
  public bool Equals(Foo left, Foo right)
  {
    if (left == null || right == null) return false;
    return keyGen(left) == keyGen(right);
  }
  public bool GetHashCode(Foo foo)
  {
    return keyGen(foo).GetHashCode();
  }
}

тогда напишите:

IEnumerable<Foo> dissimilarFoos = query.Distinct(new FooComparer());
IEnumerable<Foo> firstDissimilarFoos = dissimilarFoos.Take(50);

Ответ 4

Идея. Возможно, вы сможете что-то взломать, разработав собственный свободно управляемый интерфейс мутаторов над кешем, который вы бы захватили в предложениях "let x =..." в строках

from q in query
let qc = ... // your cache mechanism here
select ...

но я подозреваю, что вам нужно быть осторожным, чтобы ограничить обновление кеша этими "let...", поскольку я сомневаюсь, что реализация стандартных операторов Linq и методов расширений будет счастлива, если вы разрешите такие побочные эффекты происходят в их спине через предикаты, применяемые в предложениях "where" или "присоединяться", "группировать по" и т.д..

'НТН,

Ответ 5

Я полагаю, что "full LINQ" означает стандартные методы LINQ/ Enumerable.

Я не думаю, что это можно сделать с помощью синтаксиса запроса LINQ. Из стандартных методов единственное, поддерживающее изменчивое состояние обработки, - Enumerable.Aggregate, но оно дает вам не что иное, как аромат LINQ над простой foreach:

var result = query.Aggregate(new List<Foo>(), (list, next) =>
{
    if (list.Count < 50 && !list.Any(item => areBarsSimilar(item.Bars, next.Bars)))
        list.Add(next);
    return list;
});

Так как нам нравится использовать вспомогательные методы (например, areBarsSimilar), самое лучшее, что мы можем сделать, это сделать его, по крайней мере, более LINQ-ish, определив и используя собственный метод расширения

var result = query.Aggregate(new List<Foo>(), (list, next) => list.Count < 50 && 
    !list.Any(item => areBarsSimilar(item.Bars, next.Bars)) ? list.Concat(next) : list);

где пользовательский метод

public static class Utils
{
    public static List<T> Concat<T>(this List<T> list, T item) { list.Add(item); return list; }
}

Но обратите внимание, что по сравнению с vanilla foreach, Aggregate имеет дополнительный недостаток в том, что он не может выйти раньше, поэтому будет потреблять всю входную последовательность (которая помимо производительности также означает, что она не работает с бесконечными последовательностями).

Заключение:. Хотя это должно ответить на ваш первоначальный вопрос, т.е. технически можно делать то, о чем вы просите, LINQ (например, стандартный SQL) не подходит для такого типа обработки.