Мой класс, который реализует IEnumerator и IEnumerable, не переходит к оператору foreach

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

public class GroupCollection : IEnumerable, IEnumerator
{
    public List<string> Groups { get; set; }
    public int Count { get { return Groups.Count; } }
    int position = -1;
}

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

public object Current
{
    get
    {
        try
        {
            return new Group(Groups[position]);
        }
        catch (IndexOutOfRangeException)
        {
            throw new InvalidOperationException();
        }
    }
}

public bool MoveNext()
{
    position++;
    return position < Groups.Count;
}

public void Reset()
{
    position = 0;
}

Я повторяю переменную GroupCollection дважды:

foreach (GroupCollection.Group in groups) // where groups is a GroupCollection
{
}

foreach (GroupCollection.Group in groups)
{
}

// where Group is a nested class in GroupCollection.

Когда он находится в первом foreach, он работает хорошо (количество в настоящее время равно 1). Я ничего не изменяю, и когда он переходит ко второму foreach, он не входит в цикл. Я просмотрел код построчно в режиме отладки и обнаружил, что reset не вызывается после первого foreach. Так должен ли я вручную вызвать reset после foreach? Разве нет более приятного способа сделать это?


person Mitulát báti    schedule 07.04.2015    source источник
comment
Наличие класса, реализующего оба, является ошибкой. Счетчик должен сам отслеживать свое текущее положение. Никто не устанавливает position обратно в 0 в вашем коде, что является основной причиной, по которой он не работает правильно.   -  person Hans Passant    schedule 07.04.2015
comment
Я заметил, что вы возвращаете новую группу при каждом вызове Current. Это правильное ожидание для вашего варианта использования? Как правило, вызывающие объекты ожидают, что результаты повторения коллекции дважды приведут к одним и тем же результатам. Если Group является чем-то вроде неизменяемого типа значения, где разные экземпляры могут считаться одинаковыми, то, вероятно, это не проблема.   -  person Steve Mitcham    schedule 07.04.2015
comment
@SteveMitcham да, это предназначено для использования. Группы создаются динамически, с путями к папкам, списком пользователей, которые могут получить к ним доступ, и тому подобным), и я считаю это наиболее подходящим решением проблемы. И из исходников тут я, конечно, исключил много лишнего). Спасибо, что заметили это.   -  person Mitulát báti    schedule 16.04.2015


Ответы (2)


я ничего не изменяю

Да, вы делаете - ваш MoveNext() изменяет состояние класса. Вот почему вы не должны реализовывать IEnumerable и IEnumerator в одном классе. (Компилятор C# делает это для блоков итераторов, но это особый случай.) Вы должны иметь возможность вызвать GetEnumerator() дважды и получить два полностью независимых итератора. Например:

foreach (var x in collection)
{
    foreach (var y in collection)
    {
        Console.WriteLine("{0}, {1}", x, y);
    }
}

... должен дать вам все возможные пары элементов в коллекции. Но это только работает, когда итераторы независимы.

Я просмотрел код построчно в режиме отладки и обнаружил, что reset не вызывается после первого foreach.

Почему вы ожидаете этого? Я не верю, что в спецификации что-то сказано о вызове foreach Reset, и это хорошая работа, так как многие реализации на самом деле не реализуют это (вместо этого они выдают исключение).

По сути, вы должны заставить свой метод GetEnumerator() возвращать новый объект, который сохраняет изменяемое состояние «курсора» над вашими данными. Обратите внимание, что самый простой способ реализации итератора в C# — это обычно использование блока итератора (yield return и т. д.).

Я также настоятельно рекомендую вам реализовывать универсальные интерфейсы, а не только неуниверсальные; таким образом, ваш тип может быть гораздо проще использован в коде LINQ, переменная итератора в операторе foreach может быть неявно типизирована соответствующим образом и т. д.

person Jon Skeet    schedule 07.04.2015
comment
Знаете, почему мне нравятся ваши ответы? Это не просто правильные ответы. Они учат! Спасибо. - person Mitulát báti; 16.04.2015

Reset не вызывается в конце цикла foreach — вы можете сделать это в вызове GetEnumerator или просто вернуть перечислитель для List:

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

Обратите внимание, что с ключевым словом yield почти нет необходимости явно реализовывать IEnumerator или IEnumerable:

public IEnumerator<string> GetEnumerator()
{
    foreach(string s in Groups)
        yield return s;
}
person D Stanley    schedule 07.04.2015
comment
Не совсем - ОП создает копию Group в Current, так что это может вести себя по-другому. - person Jon Skeet; 07.04.2015