Почему методы расширения Linq не помещаются на IEnumerator, а не на IEnumerable?

Существует множество алгоритмов Linq, которые нужно выполнить только один проход через вход, например. Выбрать.

Однако все методы расширения Linq расположены на IEnumerable, а не на IEnumerator

    var e = new[] { 1, 2, 3, 4, 5 }.GetEnumerator(); 
    e.Select(x => x * x); // Doesn't work 

Это означает, что вы не можете использовать Linq в любой ситуации, когда вы читаете из "уже открытого" потока.

Этот сценарий очень много работает для проекта, над которым я сейчас работаю - я хочу вернуть IEnumerator, метод IDispose которого закроет поток, и на нем будет работать весь нижеперечисленный код Linq.

Короче говоря, у меня есть "уже открытый" поток результатов, который я могу преобразовать в подходящий одноразовый IEnumerator, но, к сожалению, для всего нижестоящего кода требуется IEnumerable, а не IEnumerator, хотя он будет делать только один ", пройти".

то есть. Я хочу "реализовать" этот тип возврата в различных источниках (файлы CSV, IDataReaders и т.д.):

class TabularStream 
{ 
    Column[] Columns; 
    IEnumerator<object[]> RowStream; 
}

Чтобы получить "Столбцы", я должен был открыть файл CSV, инициировал SQL-запрос или что-то еще. Затем я могу вернуть "IEnumerator", метод Dispose которого закрывает ресурс, но для всех операций Linq требуется IEnumerable.

Лучшим обходным решением, которое я знаю, является реализация IEnumerable, метод GetEnumerator() возвращает одномерный IEnumerator и выдает ошибку, если что-то пытается дважды выполнить вызов GetEnumerator().

Все это звучит нормально или есть намного лучший способ реализовать "TabularStream" таким образом, который легко использовать из Linq?

Ответ 1

Использование IEnumerator<T> напрямую редко является хорошей идеей, на мой взгляд.

Во-первых, он кодирует тот факт, что он разрушительный, тогда как запросы LINQ обычно запускаются несколько раз. Они должны быть свободными от побочных эффектов, в то время как действие итерации по IEnumerator<T> естественно является побочным эффектом.

Это также делает практически невозможным выполнение некоторых из оптимизаций в LINQ для объектов, например использование свойства Count, если вы действительно запрашиваете ICollection<T> для его подсчета.

Что касается вашего обходного пути: да, OneShotEnumerable будет разумным подходом.

Ответ 2

Пока я вообще согласен с ответом Jon Skeet, я также сталкивался с очень немногими случаями, когда работа с IEnumerator действительно казалась более подходящей, чем упаковка их в один раз только- IEnumerable.

Я начну с иллюстрации одного такого случая и описав свое собственное решение проблемы.

Пример примера: прямые, неперематываемые курсоры баз данных только вперед,

ESRI API для доступа к геоданным (ArcObjects) содержит курсоры баз данных, которые не могут быть reset. Они по сути являются эквивалентом API IEnumerator. Но нет эквивалента IEnumerable. Поэтому, если вы хотите обернуть этот API в "путь .NET", у вас есть три варианта (которые я изучил в следующем порядке):

  • Оберните курсор как IEnumerator (так как это действительно так) и работайте напрямую с этим (что громоздко).

  • Оберните курсор или обертку IEnumerator из (1) как только один раз IEnumerable (чтобы сделать его совместимым с LINQ и, как правило, более удобным для работы). Ошибка здесь в том, что она не является IEnumerable, потому что ее нельзя перечислить более одного раза, и это может быть упущено пользователями или сторонниками вашего кода.

  • Не помещайте курсор в качестве IEnumerable, а тот, который может использоваться для извлечения курсора (например, критерии запроса и ссылка на запрашиваемый объект базы данных). Таким образом, несколько итераций возможно просто повторно выполнить весь запрос. Это то, что я в конечном итоге решил тогда.

Этот последний вариант - это прагматическое решение, которое я обычно рекомендую для подобных случаев (если применимо). Если вы ищете другие решения, читайте дальше.


Повторно реализовать операторы запросов LINQ для интерфейса IEnumerator<T>?

Технически возможно реализовать некоторые или все операторы запроса LINQ для интерфейса IEnumerator<T>. Один из подходов - написать кучу методов расширения, таких как:

public static IEnumerator<T> Where(this IEnumerator<T> xs, Func<T, bool> predicate)
{
    while (xs.MoveNext())
    {
        T x = xs.Current;
        if (predicate(x)) yield return x;
    }
    yield break;
}

Рассмотрим несколько ключевых вопросов:

  • Операторы никогда не должны возвращать IEnumerable<T>, потому что это может означать, что вы можете вырваться из своего собственного "LINQ to IEnumerator" мира и выйти в обычный LINQ. Там вы столкнулись с проблемой не повторяемости, уже описанной выше.

  • Вы не можете обрабатывать результаты какого-либо запроса с помощью цикла foreach & hellip; если каждый из объектов IEnumerator<T>, возвращаемых вашими операторами запроса, реализует метод GetEnumerator, который возвращает this. Поставка этого дополнительного метода означала бы, что вы не можете использовать yield return/break, но должны писать классы IEnumerator<T> вручную.

    Это просто странно и возможно злоупотребление конструкцией IEnumerator<T> или foreach.

  • Если возврат IEnumerable<T> запрещен и возврат IEnumerator<T> является громоздким (потому что foreach не работает), почему бы не вернуть простые массивы? Потому что тогда запросы больше не могут лениться.


IQueryable + IEnumerator= IQueryator

Как насчет задержки выполнения запроса, пока он не будет полностью скомпонован? В мире IEnumerable это то, что делает IQueryable; поэтому мы теоретически могли бы построить эквивалент IEnumerator, который я назову IQueryator.

  • IQueryator может проверять логические ошибки, например делать что-либо с последовательностью после того, как она полностью поглощена предыдущей операцией, например Count. То есть всепоглощающие операторы типа Count всегда должны были быть последними в конкатенации оператора запроса.

  • IQueryator может возвращать массив (например, предложенный выше) или какую-либо другую коллекцию только для чтения, но не отдельными операторами; только при выполнении запроса.

Реализация IQueryator займет довольно много времени... вопрос в том, действительно ли это стоило бы усилий?