Зачем реализовывать IEnumerable (T), если я могу просто определить ОДИН GetEnumerator? - PullRequest
13 голосов
/ 17 сентября 2010

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

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

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


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

  1. IEnumerable<T> наследуется от * IEnumerable, что означает, что любойтип, который реализует IEnumerable<T>, обычно должен реализовывать и IEnumerable<T>.GetEnumerator и (явно) IEnumerable.GetEnumerator.В основном это сводится к стандартному коду.
  2. Вы можете foreach для любого типа, имеющего метод GetEnumerator, при условии, что этот метод возвращает объект некоторого типа с помощью метода MoveNext и * 1035.* имущество.Поэтому, если ваш тип определяет один метод с сигнатурой 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);
}

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

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

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

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

Ответы [ 6 ]

13 голосов
/ 17 сентября 2010

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

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

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

9 голосов
/ 17 сентября 2010

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

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

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

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

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

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

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

5 голосов
/ 17 сентября 2010

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

5 голосов
/ 17 сентября 2010

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

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

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

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

1 голос
/ 17 сентября 2010

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

0 голосов
/ 17 сентября 2010

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

...