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

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

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

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

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


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

  • IEnumerable<T> наследует от * IEnumerable, что означает, что любой тип, реализующий IEnumerable<T>, как правило, должен реализовывать как IEnumerable<T>.GetEnumerator, так и (явно) IEnumerable.GetEnumerator. Это в основном составляет код шаблона.
  • Вы можете foreach по любому типу, который имеет метод GetEnumerator, если этот метод возвращает объект некоторого типа с помощью метода MoveNext и свойства Current. Поэтому, если ваш тип определяет один метод с подписью public IEnumerator<T> GetEnumerator(), он должен перечислить его с помощью foreach.
  • Очевидно, что существует много кода, который требует интерфейса IEnumerable<T> - например, в основном все методы расширения LINQ. К счастью, переход от типа, который вы можете foreach к IEnumerable<T>, тривиально, используя автоматическое генерирование итератора, которое С# предоставляет с помощью ключевого слова 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); но это просто появилось у меня в голове, и я не вижу никаких очевидных проблем, которые этот подход будет представлять.

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

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

4b9b3361

Ответ 1

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

Ответ 6

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