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

Буферизация запроса LINQ

ОКОНЧАТЕЛЬНОЕ ИЗОБРАЖЕНИЕ:

Я выбрал Тимоти, но если вы хотите, чтобы cuter-реализация, использующая оператор С# yield, проверяет ответ Eamon: qaru.site/info/309876/...


По умолчанию LINQ запросы лениво передаются.

ToArray/ToList дают полную буферизацию, но сначала они нетерпеливы, а во-вторых, для завершения бесконечной последовательности может потребоваться довольно много времени.

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

Вот пример использования:

static IEnumerable<int> Numbers
{
    get
    {
        int i = -1;

        while (true)
        {
            Console.WriteLine("Generating {0}.", i + 1);
            yield return ++i;
        }
    }
}

static void Main(string[] args)
{
    IEnumerable<int> evenNumbers = Numbers.Where(i => i % 2 == 0);

    foreach (int n in evenNumbers)
    {
        Console.WriteLine("Reading {0}.", n);
        if (n == 10) break;
    }

    Console.WriteLine("==========");

    foreach (int n in evenNumbers)
    {
        Console.WriteLine("Reading {0}.", n);
        if (n == 10) break;
    }
}

Вот результат:

Generating 0.
Reading 0.
Generating 1.
Generating 2.
Reading 2.
Generating 3.
Generating 4.
Reading 4.
Generating 5.
Generating 6.
Reading 6.
Generating 7.
Generating 8.
Reading 8.
Generating 9.
Generating 10.
Reading 10.
==========
Generating 0.
Reading 0.
Generating 1.
Generating 2.
Reading 2.
Generating 3.
Generating 4.
Reading 4.
Generating 5.
Generating 6.
Reading 6.
Generating 7.
Generating 8.
Reading 8.
Generating 9.
Generating 10.
Reading 10.

Код генерации запускается 22 раза.

Я бы хотел, чтобы он запускался 11 раз, при первом повторном перечислении.

Тогда вторая итерация выиграла бы от уже сгенерированных значений.

Это будет что-то вроде:

IEnumerable<int> evenNumbers = Numbers.Where(i => i % 2 == 0).Buffer();

Для тех, кто знаком с Rx, это похоже на a ReplaySubject.

4b9b3361

Ответ 1

IEnumerable<T>.Buffer() метод расширения

public static EnumerableExtensions
{
    public static BufferEnumerable<T> Buffer(this IEnumerable<T> source)
    {
        return new BufferEnumerable<T>(source);
    }
}

public class BufferEnumerable<T> : IEnumerable<T>, IDisposable
{
    IEnumerator<T> source;
    List<T> buffer;
    public BufferEnumerable(IEnumerable<T> source)
    {
        this.source = source.GetEnumerator();
        this.buffer = new List<T>();
    }
    public IEnumerator<T> GetEnumerator()
    {
        return new BufferEnumerator<T>(source, buffer);
    }
    public void Dispose()
    {
        source.Dispose()
    }
}

public class BufferEnumerator<T> : IEnumerator<T>
{
    IEnumerator<T> source;
    List<T> buffer;
    int i = -1;
    public BufferEnumerator(IEnumerator<T> source, List<T> buffer)
    {
        this.source = source;
        this.buffer = buffer;
    }
    public T Current
    {
        get { return buffer[i]; }
    }
    public bool MoveNext()
    {
        i++;
        if (i < buffer.Count)
            return true;
        if (!source.MoveNext())
            return false;
        buffer.Add(source.Current);
        return true;
    }
    public void Reset()
    {
        i = -1;
    }
    public void Dispose()
    {
    }
}

Использование

using (var evenNumbers = Numbers.Where(i => i % 2 == 0).Buffer())
{
    ...
}

Комментарии

Ключевым моментом здесь является то, что IEnumerable<T> source, указанный как вход для метода Buffer, имеет только GetEnumerator, вызываемый один раз, независимо от того, сколько раз результат Buffer перечислит. Все счетчики для результата Buffer используют один и тот же исходный счетчик и внутренний список.

Ответ 2

Вы можете использовать тип Microsoft.FSharp.Collections.LazyList<> из блока питания F # (да, с С# без F #, установленного - без проблем!) для этого. Это в пакете Nuget FSPowerPack.Core.Community.

В частности, вы хотите вызвать LazyListModule.ofSeq(...), который возвращает LazyList<T>, который реализует IEnumerable<T> и является ленивым и кэшированным.

В вашем случае использование - это всего лишь вопрос...

var evenNumbers = LazyListModule.ofSeq(Numbers.Where(i => i % 2 == 0));
var cachedEvenNumbers = LazyListModule.ofSeq(evenNumbers);

Хотя я лично предпочитаю var во всех таких случаях, обратите внимание, что это означает, что тип времени компиляции будет более конкретным, чем просто IEnumerable<> - не то, что это, вероятно, когда-либо будет недостатком. Еще одно преимущество типов без интерфейса F # заключается в том, что они предоставляют некоторые эффективные операции, которые вы не можете сделать эффективно с помощью простых IEnumerables, таких как LazyListModule.skip.

Я не уверен, является ли LazyList потокобезопасным, но я подозреваю, что это так.


Еще одна альтернатива, указанная в комментариях ниже (если у вас установлена ​​F #), есть SeqModule.Cache (namespace Microsoft.FSharp.Collections, она будет в сборке GACed FSharp.Core.dll), которая имеет такое же эффективное поведение. Как и другие перечисления .NET, Seq.cache не имеет оператора хвоста (или пропуска), который вы можете эффективно цепочки.

Thread-safe: в отличие от других решений по этому вопросу Seq.cache является потокобезопасным в том смысле, что вы можете иметь несколько счетчиков, работающих параллельно (каждый перечислитель небезопасный поток).

Производительность. Я сделал быстрый тест, а перечисление LazyList имеет как минимум в 4 раза больше накладных расходов, чем вариант SeqModule.Cache, который имеет как минимум в три раза больше накладных расходов, чем пользовательские ответы на реализацию, Таким образом, в то время как варианты F # работают, они не такие быстрые. Обратите внимание, что в 3-12 раз медленнее все еще не очень медленно по сравнению с перечислимым, который делает (скажем) ввод-вывод или любые нетривиальные вычисления, поэтому это, вероятно, не будет иметь большого значения в большинстве случаев, но хорошо держать ум.

TL; DR Если вам нужен эффективный, потокобезопасный кешированный перечислимый, просто используйте SeqModule.Cache.

Ответ 3

Основываясь на Eamon ответить выше, здесь еще одно функциональное решение (без новых типов), которое работает также с одновременной оценкой. Это демонстрирует, что общая проблема (итерация с общим состоянием) лежит в основе этой проблемы.

Сначала мы определяем очень общий вспомогательный метод, который позволяет нам моделировать недостающую особенность анонимных итераторов в С#:

public static IEnumerable<T> Generate<T>(Func<Func<Tuple<T>>> generator)
{
    var tryGetNext = generator();
    while (true)
    {
        var result = tryGetNext();
        if (null == result)
        {
            yield break;
        }
        yield return result.Item1;
    }
}

Генерация - это как агрегатор с состоянием. Он принимает функцию, возвращающую начальное состояние, и функцию генератора, которая была бы анонимной с yield return в ней, если бы она была разрешена в С#. Состояние, возвращаемое initialize, предназначено для перечисления, тогда как более глобальное состояние (разделяемое между всеми перечислениями) может поддерживаться вызывающим абонентом для генерации, например. в переменных замыкания, как мы покажем ниже.

Теперь мы можем использовать это для проблемы с буферизацией Enumerable:

public static IEnumerable<T> Cached<T>(IEnumerable<T> enumerable)
{
    var cache = new List<T>();
    var enumerator = enumerable.GetEnumerator();

    return Generate<T>(() =>
    {
        int pos = -1;
        return () => {
            pos += 1;
            if (pos < cache.Count())
            {
                return new Tuple<T>(cache[pos]);
            }
            if (enumerator.MoveNext())
            {
                cache.Add(enumerator.Current);
                return new Tuple<T>(enumerator.Current);
            }
            return null;
        };
    });
}

Ответ 4

Надеюсь, этот ответ сочетает в себе краткость и ясность sinelaw answer и поддержку множественных перечислений Тимоти отвечает:

public static IEnumerable<T> Cached<T>(this IEnumerable<T> enumerable) {
    return CachedImpl(enumerable.GetEnumerator(), new List<T>());
}

static IEnumerable<T> CachedImpl<T>(IEnumerator<T> source, List<T> buffer) {
    int pos=0;
    while(true) {
        if(pos == buffer.Count) 
            if (source.MoveNext()) 
                buffer.Add(source.Current); 
            else 
                yield break;
        yield return buffer[pos++];
    }
}

Ключевыми идеями являются использование синтаксиса yield return для создания короткой перечислимой реализации, но вам все еще нужна машина состояния, чтобы решить, можете ли вы получить следующий элемент из буфера или вам нужно проверить базовую переписчик.

Ограничения:. Это не делает попытку быть потокобезопасной, а также не использует базовый перечислитель (что, в общем, довольно сложно сделать, поскольку лежащий в основе неэквивалентный перечислитель должен оставаться нераскрытым так долго так как все кэшированные enumerabl все еще могут быть использованы).

Ответ 5

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

Однако вы можете реализовать его самостоятельно. Легкий способ был бы на сайте вызова, так как

var evenNumbers = Numbers.Where(i => i % 2 == 0).
var startOfList = evenNumbers.Take(10).ToList();

// use startOfList instead of evenNumbers in the loop.

В более общем и точном виде вы можете сделать это в генераторе: создайте List<int> cache, и каждый раз, когда вы создаете новый номер, добавьте его в cache перед тем, как вы yield return его. Затем, когда вы снова зацикливаетесь, сначала обслуживайте все кэшированные числа. Например.

List<int> cachedEvenNumbers = new List<int>();
IEnumerable<int> EvenNumbers
{
  get
  {
    int i = -1;

    foreach(int cached in cachedEvenNumbers)
    {
      i = cached;
      yield return cached;
    }

    // Note: this while loop now starts from the last cached value
    while (true) 
    {
        Console.WriteLine("Generating {0}.", i + 1);
        yield return ++i;
    }
  }
}

Я думаю, если вы подумаете об этом достаточно долго, вы можете придумать общую реализацию метода расширения IEnumerable<T>.Buffered() - опять же, требование состоит в том, чтобы перечисление не менялось между вызовами, и вопрос в том, стоит ли это Это.

Ответ 6

Здесь неполная, но компактная "функциональная" реализация (не определены новые типы).

Ошибка заключается в том, что она не допускает одновременного перечисления.


Исходное описание: Первой функцией должна была быть анонимная лямбда внутри второй, но С# не разрешает yield в анонимных лямбдах:

// put these in some extensions class

private static IEnumerable<T> EnumerateAndCache<T>(IEnumerator<T> enumerator, List<T> cache)
{
    while (enumerator.MoveNext())
    {
        var current = enumerator.Current;
        cache.Add(current);
        yield return current;
    }
}
public static IEnumerable<T> ToCachedEnumerable<T>(this IEnumerable<T> enumerable)
{
    var enumerator = enumerable.GetEnumerator();
    var cache = new List<T>();
    return cache.Concat(EnumerateAndCache(enumerator, cache));
}

Использование:

var enumerable = Numbers.ToCachedEnumerable();

Ответ 7

Полный кредит Eamon Nerbonne и sinelaw за их ответы, всего лишь пару настроек! Во-первых, чтобы освободить счетчик, когда он будет завершен. Во-вторых, чтобы защитить базовый счетчик с помощью блокировки, поэтому перечислимое можно безопасно использовать для нескольких потоков.

// This is just the same as @sinelaw Generator but I didn't like the name
public static IEnumerable<T> AnonymousIterator<T>(Func<Func<Tuple<T>>> generator)
{
    var tryGetNext = generator();
    while (true)
    {
        var result = tryGetNext();
        if (null == result)
        {
            yield break;
        }
        yield return result.Item1;
    }
}

// Cached/Buffered/Replay behaviour
public static IEnumerable<T> Buffer<T>(this IEnumerable<T> self)
{
    // Rows are stored here when they've been fetched once
    var cache = new List<T>();

    // This counter is thread-safe in that it is incremented after the item has been added to the list,
    // hence it will never give a false positive. It may give a false negative, but that falls through
    // to the code which takes the lock so it ok.
    var count = 0;

    // The enumerator is retained until it completes, then it is discarded.
    var enumerator = self.GetEnumerator();

    // This lock protects the enumerator only. The enumerable could be used on multiple threads
    // and the enumerator would then be shared among them, but enumerators are inherently not
    // thread-safe so a) we must protect that with a lock and b) we don't need to try and be
    // thread-safe in our own enumerator
    var lockObject = new object();

    return AnonymousIterator<T>(() =>
    {
        int pos = -1;
        return () =>
        {
            pos += 1;
            if (pos < count)
            {
                return new Tuple<T>(cache[pos]);
            }
            // Only take the lock when we need to
            lock (lockObject)
            {
                // The counter could have been updated between the check above and this one,
                // so now we have the lock we must check again
                if (pos < count)
                {
                    return new Tuple<T>(cache[pos]);
                }

                // Enumerator is set to null when it has completed
                if (enumerator != null)
                {
                    if (enumerator.MoveNext())
                    {
                        cache.Add(enumerator.Current);
                        count += 1;
                        return new Tuple<T>(enumerator.Current);
                    }
                    else
                    {
                        enumerator = null;
                    }
                }
            }
        }
        return null;
    };
});

}