Когда я должен отдельно реализовать IEnumerator‹T›?

В фреймворковых классах коллекций я часто видел IEnumerator<T> отдельно реализованный как внутренний класс, и его экземпляр возвращается в методе GetEnumerator.

Теперь предположим, что я пишу свои собственные классы коллекций, которые будут иметь встроенную коллекцию, такую ​​​​как List<T> или T[], выступающую в качестве внутреннего держателя, скажем так:

public class SpecialCollection<T> : IEnumerable<T>
{
    List<T> list;

    public SpecialCollection<T>()
    {

    }

    public IEnumerator<T> GetEnumerator()
    {
        return list.GetEnumerator();

        //or
        return list.Where(x => some logic).GetEnumerator(); 

        //or directly rely on yield keyword
        yield return x; //etc
    }
}

Должен ли я писать свой собственный класс перечислителя или можно вернуть перечислитель класса List<T>? Есть ли какие-либо обстоятельства, при которых я должен писать свой собственный класс перечислителя?


У меня тоже есть связанный с этим вопрос. Если это не так важно или не имеет большого значения, то почему каждый класс коллекций в BCL пишет свой собственный IEnumerator?

Например, класс List<T> имеет что-то вроде

T[] items;
public IEnumerator<T> GetEnumerator()
{
    return new List<T>.Enumerator(items);
}

person nawfal    schedule 10.06.2014    source источник
comment
В отличие от чего? Многие из этих классов были написаны до появления итераторов.   -  person SLaks    schedule 10.06.2014
comment
Дженерики были введены одновременно с ключевым словом yield, если я не ошибаюсь.   -  person Dennis_E    schedule 10.06.2014
comment
Не используйте расточительное array.Select(x => x).GetEnumerator();, вы можете использовать ((IEnumerable<T>)array).GetEnumerator(). Одномерные массивы магическим образом реализуют универсальные интерфейсы, которые снаружи выглядят как явная реализация интерфейса.   -  person Jeppe Stig Nielsen    schedule 10.06.2014
comment
Многие из этих перечислителей являются структурами. Возможно, это как-то связано.   -  person Dennis_E    schedule 10.06.2014
comment
@JeppeStigNielsen Я не уверен, что понимаю вашу точку зрения. Это был какой-то пример.   -  person nawfal    schedule 10.06.2014
comment
@JeppeStigNielsen Я думаю, что // some logic означало, что в перечислитель можно добавить некоторую дополнительную логику.   -  person Maarten    schedule 10.06.2014
comment
Не является дубликатом, но может прояснить некоторые моменты: stackoverflow.com/questions/3737997/   -  person Yuval Itzchakov    schedule 10.06.2014
comment
Я могу понять, был ли этот q дубликатом или расплывчатым. Основано на мнении? Что случилось?   -  person nawfal    schedule 10.06.2014
comment
Посмотрите, есть ли ericlippert.com/2011/06/30/following-the-pattern поможет вам   -  person Jon Skeet    schedule 10.06.2014
comment
@Maarten Предположим, я пишу свой собственный тип class MyColl : IEnumerable<int>. Я мог бы хранить свои значения в массиве внутри, поэтому у меня есть поле private int[] array;. Теперь, когда мне нужно реализовать общий интерфейс, я не могу просто сказать return array.GetEnumerator(); (ошибка времени компиляции) из-за не совсем универсального характера массивов (здесь int[]). Я мог бы решить это на return array.Select(x => x).GetEnumerator();. Но это расточительно. Вместо этого используйте return ((IEnumerable<int>)array).GetEnumerator(); или эквивалентно return array.AsEnumerable().GetEnumerator();. Видишь, что я имею в виду?   -  person Jeppe Stig Nielsen    schedule 10.06.2014
comment
@JeppeStigNielsen хорошая мысль. Просто повторюсь, примеры не были тщательно обработаны. Я лишь хотел передать намерение.   -  person nawfal    schedule 10.06.2014
comment
@JonSkeet Спасибо. Но эта ссылка говорит о том, что вызвало необходимость утиного ввода в случае foreach и почему структура предпочтительнее ссылочного типа. В нем ничего не говорится о повторном использовании существующих реализаций перечислителя.   -  person nawfal    schedule 10.06.2014
comment
@nawfal Это предполагает, что у вас есть существующая реализация перечислителя для повторного использования. Если да, отлично, если нет, то придется заняться чем-то другим. В этом посте блок итератора будет охватывать почти всех, то есть всех, кроме тех, у кого самые строгие требования к производительности.   -  person Servy    schedule 10.06.2014
comment
@nawfal: в нем говорится о том, почему List<T> имеет свой собственный (структурный) тип перечислителя.   -  person Jon Skeet    schedule 10.06.2014
comment
@Servy да, именно об этом мой вопрос. То есть, если уже существуют реализации перечислителей для повторного использования, есть ли смысл писать новые. Я спросил об этом, потому что постоянно вижу это в классах фреймворка. Но, как отвечает Эрик, производительность — единственная причина.   -  person nawfal    schedule 10.06.2014
comment
Насколько я понимаю, @JonSkeet не говорит о том, почему List‹T› имеет свой собственный (структурный) тип перечислителя. В нем рассказывается о том, почему List‹T› имеет собственный тип перечислителя (struct).   -  person nawfal    schedule 10.06.2014
comment
@nawfal: я совсем не уверен, какую разницу вы пытаетесь указать, если только она не связана с выделением. Учитывая, что перечислителю нужен доступ к закрытым членам списка, он должен быть вложенной структурой... как бы вы предложили реализовать ее иначе?   -  person Jon Skeet    schedule 10.06.2014
comment
@JonSkeet В статье не говорится о неиспользовании GetEnumerator резервного массива (о чем мой вопрос). В нем говорится о том, почему в первую очередь требуется структура. Мой вопрос: если вам нужна структура, то почему бы не использовать повторно уже реализованную структуру резервного массива. Я надеюсь, что это ясно. Не хочу придираться, но, честно говоря, ссылка совсем не проясняет моего замешательства. Ответ Эрика делает.   -  person nawfal    schedule 10.06.2014
comment
@nawfal Кажется, вы упускаете из виду тот факт, что foreach над массивом распознается компилятором и вместо этого преобразуется в цикл for, так как это намного быстрее. Это означает, что на самом деле используются только перечислители упакованных массивов, а действие упаковки также устраняет возможность оптимизации в стиле списка, поэтому массивам не нужно использовать уловку списка, состоящую в том, чтобы иметь структуру в качестве перечислителя (поэтому они нет; их перечислитель - class), потому что у них есть что-то даже лучше, специфичное для них.   -  person Servy    schedule 10.06.2014
comment
@nawfal: если вы вернете IEnumerator<T> массива, вы не сможете обнаружить изменение списка между вызовами, что должно вызвать исключение.   -  person Jon Skeet    schedule 10.06.2014
comment
@JonSkeet Это отличный момент, и он отвечает на мой вопрос! Если бы вы могли сделать это ответом, я был бы признателен (vcsjones ответил на него, но по какой-то причине он его удалил). Но я также думаю, что использование перечислителей чего-либо выше массива (например, List<T>, Queue<T>) в фреймворке — неплохой выбор, учитывая, что они улавливают модификации.   -  person nawfal    schedule 10.06.2014


Ответы (2)


Ответ на ваш первый вопрос: когда доходность не соответствует вашим потребностям.

Ответ на ваш второй вопрос: эти часто используемые типы имеют необычно строгие требования к производительности, поэтому перечислители создаются на заказ. Недавно я написал об этом несколько статей; видеть:

http://ericlippert.com/2014/05/21/enumerator-advance/

http://ericlippert.com/2014/06/04/enumerator-bounds/

person Eric Lippert    schedule 10.06.2014

Просто, чтобы ответить на одну часть:

List<T> имеет собственную реализацию перечислителя по двум причинам:

  • It can't just return the iterator of its backing array, because:
    • It needs to detect structural changes in the list (additions and removals) in order to invalidate the enumerator
    • Массив вполне может быть больше, чем список. (Использование Take исправило бы это за счет другого уровня косвенности)
  • Вышеупомянутое можно было бы выполнить с блоком итератора (хотя сначала должен быть метод без итератора, чтобы захватить «структурную версию» списка во время вызова, а не во время первой итерации), но это относительно неэффективно по сравнению с реальной реализацией высокооптимизированной изменяемой структуры

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

person Jon Skeet    schedule 10.06.2014
comment
Длина массива — еще один отличный момент, который легко упустить. Но я думаю, что возвращать перечислитель встроенных типов из фреймворка, таких как словарь, список, стек, очередь, набор и т. д. (кроме массива, конечно), нормально, учитывая, что их перечислитель обрабатывает эту проблему модификации коллекции. Не так ли? - person nawfal; 10.06.2014
comment
@nawfal: возврат счетчика в каком контексте? Часто это нормально, но иногда это не так. Здесь нет универсального ответа. - person Jon Skeet; 10.06.2014
comment
Я имел в виду в том же случае, что и в примере, который я привел в своем вопросе. Если тип резервной коллекции моего класса SpecialCollection<T> равен List<T>, то, я думаю, возврат List<T>.GetEnumerator из метода SpecialCollection<T>.GetEnumerator будет безошибочным. - person nawfal; 10.06.2014
comment
@nawfal: Да, если у вас нет других особых требований, я думаю, это нормально. - person Jon Skeet; 10.06.2014
comment
Хм, я понимаю. Спасибо. - person nawfal; 10.06.2014