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

Почему обработка ошибок для IEnumerator.Current отличается от IEnumerator <T>.Current?

Я бы подумал, что выполнение следующего кода для пустой коллекции, реализующей IEnumerable<T>, вызовет исключение:

var enumerator = collection.GetEnumerator();
enumerator.MoveNext();
var type = enumerator.Current.GetType(); // Surely should throw?

Поскольку коллекция пуста, доступ к IEnumerator.Current недопустим, и я ожидал бы исключения. Однако для List<T> исключение не исключено.

Это разрешено документацией для IEnumerator<T>.Current, которая гласит, что Current есть undefined при любом из следующих условий:

  • Перечислитель помещается перед первым элементом в коллекции сразу после создания счетчика. MoveNext должен быть вызван для продвижения счетчика к первому элементу коллекции перед чтением значения Current.
  • Последний вызов MoveNext возвращает false, что указывает на конец коллекции.
  • Перечислитель недействителен из-за изменений, внесенных в коллекцию, таких как добавление, изменение или удаление элементов.

(Я предполагаю, что "не удается исключить исключение" можно классифицировать как "undefined поведение"...)

Однако, если вы сделаете то же самое, но вместо этого используйте IEnumerable, вы получите исключение. Это поведение указано документацией для IEnumerator.Current, которая гласит:

  • Current должен вызывать InvalidOperationException, если последний вызов MoveNext возвратил false, что указывает на конец коллекции.

Мой вопрос: зачем эта разница? Есть ли хорошая техническая причина, о которой я не знаю?

Это означает, что идентично выглядящий код может вести себя по-разному в зависимости от того, использует ли он IEnumerable<T> или IEnumerable, как демонстрирует следующая программа (обратите внимание, как код внутри showElementType1() и showElementType1() идентичен):

using System;
using System.Collections;
using System.Collections.Generic;

namespace ConsoleApplication2
{
    class Program
    {
        public static void Main()
        {
            var list = new List<int>();

            showElementType1(list); // Does not throw an exception.
            showElementType2(list); // Throws an exception.
        }

        private static void showElementType1(IEnumerable<int> collection)
        {
            var enumerator = collection.GetEnumerator();
            enumerator.MoveNext();
            var type = enumerator.Current.GetType(); // No exception thrown here.
            Console.WriteLine(type);
        }

        private static void showElementType2(IEnumerable collection)
        {
            var enumerator = collection.GetEnumerator();
            enumerator.MoveNext();
            var type = enumerator.Current.GetType(); // InvalidOperationException thrown here.
            Console.WriteLine(type);
        }
    }
}
4b9b3361

Ответ 1

Проблема с IEnumerable<T> заключается в том, что Current имеет тип T. Вместо того, чтобы бросать исключение, default(T) возвращается (он установлен из MoveNextRare).

При использовании IEnumerable у вас нет этого типа, и вы не можете вернуть значение по умолчанию.

Фактическая проблема заключается в том, что вы не проверяете возвращаемое значение MoveNext. Если он возвращает false, вы не должны называть Current. Исключение - все в порядке. Я считаю, что удобнее было вернуть default(T) в случай IEnumerable<T>.

Обработка исключений приводит к накладным расходам, а возврат default(T) не так много. Возможно, они просто подумали, что нет ничего полезного для возврата из свойства Current в случае IEnumerable (они не знают тип). Эта проблема "решена" в IEnumerable<T> при использовании default(T).

В соответствии с этим отчет об ошибке (спасибо Jesse для комментариев)

По соображениям производительности текущее свойство сгенерированных Enumerators поддерживается чрезвычайно просто - оно просто возвращает значение сгенерированного "текущего" поля поддержки.

Это может указывать на накладные расходы на обработку исключений. Или необходимый дополнительный шаг для проверки значения Current.

Они эффективно размахивают ответственность до foreach, так как это главный пользователь перечислителя:

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

Ответ 2

Чтобы лучше соответствовать тому, как люди стремятся реализовать его на практике. Как и изменение формулировки "Current также выбрасывает исключение..." в предыдущих версиях документации на "Current should throw..." в текущей версии.

В зависимости от того, как работает реализация, бросать исключение может быть довольно немного работы, и все же из-за того, что Current используется в сочетании с MoveNext(), это исключительное состояние вряд ли когда-либо появится. Это тем более, когда мы считаем, что подавляющее большинство применений генерируется компилятором и на самом деле не имеет возможности для ошибки, в которой Current вызывается до MoveNext() или после того, как он возвратил false, чтобы когда-либо произойти, При нормальном использовании мы можем ожидать, что случай никогда не придет.

Итак, если вы пишете реализацию IEnumerable или IEnumerable<T>, где усложнение ошибки является сложным, вы, возможно, решите не делать этого. И если вы сделаете это решение, это, вероятно, не вызовет у вас никаких проблем. Да, вы нарушили правила, но это, вероятно, не имело значения.

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

Но все, что было сказано, поскольку IEnumerable.Current по-прежнему документируется как "должен бросать InvalidOperationException для обратной совместимости, и поскольку это будет соответствовать" undefined "поведению IEnumerable<T>.Current, возможно, лучший способ идеально выполнить документированное поведение интерфейса, чтобы IEnumerable<T>.Current выбрал InvalidOperationException в таких случаях и имел IEnumerable.Current только вызов.

В некотором роде это противоположно тому, что IEnumerable<T> также наследуется от IDisposable. Сгенерированное компилятором использование IEnumerable проверяет, реализует ли реализация также IDisposable и вызывает Dispose(), если это так, но, помимо незначительных издержек на производительность этого теста, это означает, что и исполнители, и обработчики вручную забыть об этом и не выполнять или называть Dispose(), когда они должны. Заставляя все реализации иметь хотя бы пустую Dispose() жизнь проще для людей, обращаясь к тому, чтобы иметь Current поведение undefined, когда оно недействительно.

Если бы не было проблем с обратной совместимостью, мы бы, вероятно, имели бы Current, задокументированные как undefined в таких случаях для обоих интерфейсов, и оба интерфейса, наследующие от IDisposable. Вероятно, у нас также не было бы Reset(), что было бы неприятностью.