Нужно ли классу реализовывать IEnumerable для использования Foreach

Это на С#, у меня есть класс, который я использую из какой-то другой DLL. Он не реализует IEnumerable, но имеет 2 метода, которые возвращают IEnumerator. Есть ли способ использовать для них цикл foreach. Класс, который я использую, запечатан.


person Brian G    schedule 24.09.2008    source источник


Ответы (11)


foreach не требует IEnumerable, вопреки распространенному мнению. Все, что для этого требуется, — это метод GetEnumerator, возвращающий любой объект, имеющий метод MoveNext и свойство get Current с соответствующими сигнатурами.

/ РЕДАКТИРОВАТЬ: Однако в вашем случае вам не повезло. Однако вы можете тривиально обернуть свой объект, чтобы сделать его перечисляемым:

class EnumerableWrapper {
    private readonly TheObjectType obj;

    public EnumerableWrapper(TheObjectType obj) {
        this.obj = obj;
    }

    public IEnumerator<YourType> GetEnumerator() {
        return obj.TheMethodReturningTheIEnumerator();
    }
}

// Called like this:

foreach (var xyz in new EnumerableWrapper(yourObj))
    …;

/EDIT: следующий метод, предложенный несколькими людьми, не работает, если метод возвращает IEnumerator:

foreach (var yz in yourObj.MethodA())
    …;
person Konrad Rudolph    schedule 24.09.2008

Re: Если foreach не требует явного контракта интерфейса, находит ли он GetEnumerator с помощью отражения?

(Я не могу комментировать, так как у меня недостаточно высокая репутация.)

Если вы подразумеваете отражение времени выполнения, то нет. Он делает все это во время компиляции, еще один менее известный факт заключается в том, что он также проверяет, является ли возвращаемый объект, который может реализовать IEnumerator, одноразовым.

Чтобы увидеть это в действии, рассмотрите этот (выполняемый) фрагмент.


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

namespace ConsoleApplication3
{
    class FakeIterator
    {
        int _count;

        public FakeIterator(int count)
        {
            _count = count;
        }
        public string Current { get { return "Hello World!"; } }
        public bool MoveNext()
        {
            if(_count-- > 0)
                return true;
            return false;
        }
    }

    class FakeCollection
    {
        public FakeIterator GetEnumerator() { return new FakeIterator(3); }
    }

    class Program
    {
        static void Main(string[] args)
        {
            foreach (string value in new FakeCollection())
                Console.WriteLine(value);
        }
    }
}
person Torbjörn Gyllebring    schedule 24.09.2008
comment
Ваш ответ очень хорошо иллюстрирует суть. PS› Похоже, у вас достаточно репутации. - person celerno; 21.07.2014

Согласно MSDN:

foreach (type identifier in expression) statement

где выражение:

Коллекция объектов или выражение массива. Тип элемента коллекции должен быть конвертируемым в тип идентификатора. Не используйте выражение, которое оценивается как null. Вычисляется тип, реализующий IEnumerable, или тип, объявляющий метод GetEnumerator. В последнем случае GetEnumerator должен либо возвращать тип, реализующий IEnumerator, либо объявлять все методы, определенные в IEnumerator.

person Adam Hughes    schedule 24.09.2008

Краткий ответ:

Вам нужен класс с методом с именем GetEnumerator, который возвращает уже имеющийся у вас IEnumerator. Достигните этого с помощью простой оболочки:

class ForeachWrapper
{
  private IEnumerator _enumerator;

  public ForeachWrapper(Func<IEnumerator> enumerator)
  {
    _enumerator = enumerator;
  }

  public IEnumerator GetEnumerator()
  {
    return _enumerator();
  }
}

Использование:

foreach (var element in new ForeachWrapper(x => myClass.MyEnumerator()))
{
  ...
}

Из Спецификации языка C#:

Обработка инструкции foreach во время компиляции сначала определяет тип коллекции, тип перечислителя и тип элемента выражения. Это определение происходит следующим образом:

  • Если тип выражения X является типом массива, то происходит неявное преобразование ссылки из X в интерфейс System.Collections.IEnumerable (поскольку System.Array реализует этот интерфейс). Тип коллекции — интерфейс System.Collections.IEnumerable, тип перечислителя — интерфейс System.Collections.IEnumerator, а тип элемента — тип элемента типа массива X.

  • В противном случае определите, имеет ли тип X соответствующий метод GetEnumerator:

    • Выполните поиск членов для типа X с идентификатором GetEnumerator и без аргументов типа. Если поиск членов не дает совпадения, или он создает неоднозначность, или создает совпадение, которое не является группой методов, проверьте перечисляемый интерфейс, как описано ниже. Рекомендуется, чтобы выдавалось предупреждение, если поиск элементов дает что-либо, кроме группы методов или несоответствия.

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

    • Если возвращаемый тип E метода GetEnumerator не является типом класса, структуры или интерфейса, возникает ошибка и дальнейшие действия не предпринимаются.

    • Поиск членов выполняется на E с идентификатором Current и без аргументов типа. Если поиск элемента не дает совпадений, результатом является ошибка или результатом является что-либо, кроме общедоступного свойства экземпляра, которое разрешает чтение, возникает ошибка и дальнейшие действия не предпринимаются.

    • Поиск элементов выполняется на E с идентификатором MoveNext и без аргументов типа. Если поиск элементов не дает совпадений, результатом является ошибка или результатом является что-либо, кроме группы методов, возникает ошибка и дальнейшие шаги не предпринимаются.

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

    • Тип коллекции — X, тип перечислителя — E, а тип элемента — тип свойства Current.

  • В противном случае проверьте перечисляемый интерфейс:

    • Если существует ровно один тип T, для которого существует неявное преобразование из X в интерфейс System.Collections.Generic.IEnumerable‹T›, то типом коллекции является этот интерфейс, типом перечислителя является интерфейс System.Collections.Generic. IEnumerator‹T›, а тип элемента — T.

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

    • В противном случае, если имеется неявное преобразование из X в интерфейс System.Collections.IEnumerable, то типом коллекции является этот интерфейс, типом перечислителя является интерфейс System.Collections.IEnumerator, а типом элемента является объект.

    • В противном случае выдается ошибка и дальнейшие действия не предпринимаются.

person VVS    schedule 24.09.2008

Не строго. Пока у класса есть необходимые члены GetEnumerator, MoveNext, Reset и Current, он будет работать с foreach.

person Bill the Lizard    schedule 24.09.2008

Нет, нет, и вам даже не нужен метод GetEnumerator, например:

class Counter
{
    public IEnumerable<int> Count(int max)
    {
        int i = 0;
        while (i <= max)
        {
            yield return i;
            i++;
        }
        yield break;
    }
}

который называется так:

Counter cnt = new Counter();

foreach (var i in cnt.Count(6))
{
    Console.WriteLine(i);
}
person Paul van Brenk    schedule 24.09.2008
comment
IEnumerable‹int› имеет метод GetEnumerator. Это тот, который вызывается, когда вы выполняете foreach. - person Hallgrim; 21.10.2008

Вы всегда можете обернуть его, и, кроме того, чтобы быть «доступным для доступа», вам нужно только иметь метод с именем «GetEnumerator» с правильной подписью.


class EnumerableAdapter
{
  ExternalSillyClass _target;

  public EnumerableAdapter(ExternalSillyClass target)
  {
    _target = target;
  }

  public IEnumerable GetEnumerator(){ return _target.SomeMethodThatGivesAnEnumerator(); }

}
person Torbjörn Gyllebring    schedule 24.09.2008

Учитывая класс X с методами A и B, которые оба возвращают IEnumerable, вы можете использовать foreach для класса следующим образом:

foreach (object y in X.A())
{
    //...
}

// or

foreach (object y in X.B())
{
   //...
}

Предположительно, значение перечислимых значений, возвращаемых A и B, хорошо определено.

person Joel Coehoorn    schedule 24.09.2008
comment
Это не то, о чем спрашивали: методы возвращают IEnumerator, а не IEnumerable. - person Konrad Rudolph; 24.09.2008

@Brian: Не уверен, что вы пытаетесь перебрать значение, возвращаемое из вызова метода или самого класса. Если вам нужен класс, то сделайте его массивом, который вы можете использовать с foreach.

person c.sokun    schedule 24.09.2008

Чтобы класс можно было использовать с foeach, все, что ему нужно сделать, это иметь открытый метод, который возвращает, и IEnumerator с именем GetEnumerator(), вот и все:

Возьмите следующий класс, он не реализует IEnumerable или IEnumerator:

public class Foo
{
    private int[] _someInts = { 1, 2, 3, 4, 5, 6 };
    public IEnumerator GetEnumerator()
    {
        foreach (var item in _someInts)
        {
            yield return item;
        }
    }
}

в качестве альтернативы можно было бы написать метод GetEnumerator():

    public IEnumerator GetEnumerator()
    {
        return _someInts.GetEnumerator();
    }

При использовании в foreach (обратите внимание, что оболочка не используется, только экземпляр класса):

    foreach (int item in new Foo())
    {
        Console.Write("{0,2}",item);
    }

печатает:

1 2 3 4 5 6

person Pop Catalin    schedule 25.09.2008
comment
Ему не нужно возвращать IEnumerator — просто тип с правильными элементами. - person Jon Skeet; 26.12.2008

Для этого типа требуется только общедоступный/нестатический/неуниверсальный/безпараметрический метод с именем GetEnumerator, который должен возвращать что-то, что имеет общедоступный метод MoveNext и общедоступное свойство Current. Насколько я помню г-на Эрика Липперта, это было разработано таким образом, чтобы приспособиться к дообобщенной эпохе как для безопасности типов, так и для проблем с производительностью, связанных с упаковкой, в случае типов значений.

Например, это работает:

class Test
{
    public SomethingEnumerator GetEnumerator()
    {

    }
}

class SomethingEnumerator
{
    public Something Current //could return anything
    {
        get { }
    }

    public bool MoveNext()
    {

    }
}

//now you can call
foreach (Something thing in new Test()) //type safe
{

}

Затем это транслируется компилятором в:

var enumerator = new Test().GetEnumerator();
try {
   Something element; //pre C# 5
   while (enumerator.MoveNext()) {
      Something element; //post C# 5
      element = (Something)enumerator.Current; //the cast!
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

Из раздела 8.8.4 спецификации.


Стоит отметить приоритет перечислителя: если у вас есть метод public GetEnumerator, то это выбор foreach по умолчанию, независимо от того, кто его реализует. Например:

class Test : IEnumerable<int>
{
    public SomethingEnumerator GetEnumerator()
    {
        //this one is called
    }

    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {

    }
}

(Если у вас нет общедоступной реализации (т. е. только явная реализация), то приоритет будет следующим: IEnumerator<T> > IEnumerator.)

person nawfal    schedule 01.12.2013