Зачем реализовывать IEnumerable (T), если я могу просто определить ОДИН GetEnumerator?

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

Так что да, считайте меня убежденным: это была бы плохая идея.

И просто чтобы хоть как-то спасти свое достоинство: я мог бы подыграть это для аргументации, но мне никогда не нравилась эта идея с самого начала - просто любопытно было услышать, что другие говорят по этому поводу. Честный.


Прежде чем вы отметите этот вопрос как абсурдный, прошу вас учесть следующее:

  1. IEnumerable<T> наследуется от * IEnumerable, что означает, что любой тип, реализующий IEnumerable<T>, обычно должен реализовывать и IEnumerable<T>.GetEnumerator , и (явно) IEnumerable.GetEnumerator. По сути, это шаблонный код.
  2. Вы можете foreach использовать любой тип, имеющий GetEnumerator метод, если этот метод возвращает объект некоторого типа с методом MoveNext и свойством Current. Таким образом, если ваш тип определяет один метод с сигнатурой public IEnumerator<T> GetEnumerator(), его можно перечислить с помощью foreach.
  3. Ясно, что существует много кода, для которого требуется интерфейс IEnumerable<T> - например, в основном все методы расширения LINQ. К счастью, перейти от типа, который вы можете foreach к IEnumerable<T>, тривиально, используя автоматическое создание итератора, которое C # предоставляет через ключевое слово yield.

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

public interface IForEachable<T>
{
    IEnumerator<T> GetEnumerator();
}

Затем всякий раз, когда я определяю тип, который должен быть перечислимым, я реализую этот интерфейс вместо IEnumerable<T>, устраняя необходимость реализации двух GetEnumerator методов (один явный). Например:

class NaturalNumbers : IForEachable<int>
{
   public IEnumerator<int> GetEnumerator()
   {
       int i = 1;
       while (i < int.MaxValue)
       {
           yield return (i++);
       }
   }

   // Notice how I don't have to define a method like
   // IEnumerator IEnumerable.GetEnumerator().
}

Наконец, чтобы сделать этот тип совместимым с кодом, который ожидает интерфейса IEnumerable<T>, я могу просто определить метод расширения для перехода от любого IForEachable<T> к IEnumerable<T>, например:

public static class ForEachableExtensions
{
    public static IEnumerable<T> AsEnumerable<T>(this IForEachable<T> source)
    {
        foreach (T item in source)
        {
            yield return item;
        }
    }
}

Мне кажется, что это позволяет мне разрабатывать типы, которые можно использовать во всех смыслах как реализации IEnumerable<T>, но без этой надоедливой явной IEnumerable.GetEnumerator реализации в каждом из них.

Например:

var numbers = new NaturalNumbers();

// I can foreach myself...
foreach (int x in numbers)
{
    if (x > 100)
        break;

    if (x % 2 != 0)
        continue;

    Console.WriteLine(x);
}

// Or I can treat this object as an IEnumerable<T> implementation
// if I want to...
var evenNumbers = from x in numbers.AsEnumerable()
                  where x % 2 == 0
                  select x;

foreach (int x in evenNumbers.TakeWhile(i => i <= 100))
{
    Console.WriteLine(x);
}

Что вы, ребята, думаете об этой идее? Я упустил какую-то причину, по которой это было бы ошибкой?

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

В общем, если я могу написать умеренное количество кода один раз, чтобы избавить себя от необходимости писать небольшой объем кода много раз, для меня это того стоит Это.

* Это правильная терминология? Я всегда не решаюсь сказать, что один интерфейс «наследуется от» другого, поскольку это, кажется, не отражает должным образом отношения между ними. Но, может быть, все в порядке.


person Dan Tao    schedule 17.09.2010    source источник
comment
Ваш маленький * внизу - я считаю, что терминология такова, что один интерфейс требует другого. Но я не на 100% в этом. Как и в случае, IEnumerator требует IDisposable.   -  person Rex M    schedule 17.09.2010
comment
@Rex: Хороший звонок - кажется, лучше.   -  person Dan Tao    schedule 17.09.2010
comment
Нет ничего плохого в том, чтобы задать такой вопрос, см. Мой недавний пост: stackoverflow.com/questions/3428667/   -  person ChaosPandion    schedule 17.09.2010


Ответы (6)


Разве вы не перемещаете шаблон в другое место - от написания IEnumerable.GetEnumeratormethod для каждого класса до вызова вашего AsEnumerable расширения каждый раз, когда ожидается IEnumerable<T>? Обычно я ожидаю, что перечислимый тип будет использоваться для запросов гораздо больше раз, чем написано (то есть ровно один раз). Это будет означать, что этот шаблон в среднем приведет к большему шаблону.

person Ani    schedule 17.09.2010
comment
Ха-ха, да, но тогда это 1 строка вместо 3 ... Хорошо, так что это не самая стоящая идея;) - person Dan Tao; 17.09.2010
comment
Это все равно всего одна строка, если вы просто напишете ее как IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } - person Gabe; 17.09.2010

Вам не хватает одной огромной вещи -

Если вы реализуете свой собственный интерфейс вместо IEnumerable<T>, ваш класс не будет работать с методами фреймворка, ожидающими IEnumerable<T> - в основном вы совершенно не сможете использовать LINQ или использовать свой класс для создания List<T> или многих других полезных абстракций.

Вы можете сделать это, как вы упомянули, с помощью отдельного метода расширения, однако за это придется заплатить. Используя метод расширения для преобразования в IEnumerable<T>, вы добавляете еще один уровень абстракции, необходимый для использования вашего класса (что вы будете делать FAR чаще, чем при создании класса), и вы снижаете производительность (ваш метод расширения будет по сути, внутренне сгенерируйте новую реализацию класса, что на самом деле не нужно). Что наиболее важно, любому другому пользователю вашего класса (или вам позже) придется изучить новый API, который ничего не дает - вы усложняете использование своего класса, не используя стандартные интерфейсы, поскольку это нарушает ожидания пользователя.

person Reed Copsey    schedule 17.09.2010
comment
Но именно для этого и предназначен предложенный мной метод расширения AsEnumerable. - person Dan Tao; 17.09.2010
comment
@Dan, значит, вы должны не забывать вызывать дополнительный метод каждый раз, когда хотите, чтобы ваша собственная версия IEnumerable была полезной? - person Rex M; 17.09.2010
comment
@Dan: Я добавил второй абзац, объясняющий, почему это не так хорошо, как простая реализация IEnumerable ‹T› - person Reed Copsey; 17.09.2010
comment
@Rex M: Ну, это зависит от того, что вам полезно. Полагаю, если вы всегда используете LINQ, то да. Если вы обычно просто планируете использовать foreach самостоятельно, вам не нужно вызывать метод расширения. - person Dan Tao; 17.09.2010
comment
Это действительно самый важный момент. Вам действительно не следует бороться с фреймворком, с которым вы работаете. - person ChaosPandion; 17.09.2010

Вы правы: это действительно кажется слишком сложным решением довольно простой проблемы.

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

Кроме того, хотя ваш метод расширения позволяет преобразовывать любой IForEachable<T> в IEnumerable<T>, это означает, что сам ваш тип не удовлетворяет общему ограничению, подобному этому:

public void Foo<T>(T collection) where T : IEnumerable

или тому подобное. По сути, выполняя преобразование, вы теряете возможность рассматривать отдельный объект как оба как реализацию IEnumerable<T> и реального конкретного типа.

Кроме того, не реализуя IEnumerable, вы лишаетесь инициализаторов коллекции. (Иногда я реализую IEnumerable явно просто для включения инициализации коллекции, вызывая исключение из GetEnumerator().)

Да, и вы также представили дополнительную инфраструктуру, незнакомую никому в мире по сравнению с огромным количеством разработчиков C #, которые уже знают о IEnumerable<T>.

person Jon Skeet    schedule 17.09.2010
comment
Хороший ответ на вопросы об общих ограничениях и инициализаторах коллекций. Как только я получил ответы на свои вопросы, это стало для меня очевидным, но есть масса причин не делать этого! В любом случае, было действительно полезно получить такую ​​немедленную (и сильную) реакцию со стороны некоторых уважаемых SO-людей по этому поводу. Тогда спасибо! - person Dan Tao; 17.09.2010
comment
Для какого типа объекта вы реализуете IEnumerable, чтобы можно было использовать синтаксис инициализатора коллекции без фактической возможности его перечисления? - person Gabe; 17.09.2010
comment
интересный ответ, как всегда. Однако я не понимаю вашего мнения об инициализаторах коллекций: разве вам не нужно реализовывать ICollection, а не IEnumerable? - person Thomas Levesque; 18.09.2010
comment
На самом деле вы можете проигнорировать мой предыдущий комментарий: я только что просмотрел его в MSDN, вам просто нужно реализовать IEnumerable и предоставить метод Add ... - person Thomas Levesque; 18.09.2010
comment
@Gabe: Иногда возможность перебирать что-то просто бесполезна или, по крайней мере, не является частью основного дизайна. Представьте себе программу для выполнения тестов, в которой вы хотите иметь возможность предоставить серию тестов, а затем выполнить их ... извне вам не нужно иметь возможность перечислять их один за другим. Обдумывание вашего вопроса натолкнуло на ряд дополнительных идей, так что смотрите мой блог ... надеюсь, на выходных я найду время, чтобы кое-что написать. - person Jon Skeet; 18.09.2010

В нашей кодовой базе, вероятно, более 100 экземпляров этого точного фрагмента:

IEnumerator IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}

И меня это действительно устраивает. Это крошечная цена за полную совместимость со всеми остальными когда-либо написанными частями .NET-кода;)

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

person Rex M    schedule 17.09.2010
comment
Я думаю, что ты меня покорил. Меня это вполне устраивает. В конце концов, я думаю, мне нужно просто согласиться с тем, что необходимость печатать еще несколько строк вряд ли представляет собой проблему, за которую стоит бороться. - person Dan Tao; 17.09.2010

Гораздо проще использовать IEnumerable для размышлений. Пытаться вызвать общие интерфейсы через отражение - такая боль. Штраф за бокс через IEnumerable теряется из-за накладных расходов на отражение, так зачем вообще использовать общий интерфейс? В качестве примера на ум приходит сериализация.

person ChaosPandion    schedule 17.09.2010
comment
Вы можете подробнее рассказать об этом? Я не оспариваю ваш ответ; Я просто не совсем понимаю, что вы имеете в виду. - person Dan Tao; 17.09.2010
comment
@Dan Tao: Я думаю, он имеет в виду возможность отражать в IEnumerable, а его участников проще, чем отражаться в IEnumerable ‹T› - person Jimmy Hoffa; 17.09.2010

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

person Jimmy Hoffa    schedule 17.09.2010