Точка необходимости пользовательских счетчиков

При чтении поста были даны некоторые моменты без примера :

Для реализации IEnumerable/IEnumerable необходимо предоставить перечислитель:

• Если класс "оборачивает" другую коллекцию, возвращая перечислитель обернутой коллекции.

• Через итератор с использованием yield return.

• Путем создания собственной реализации IEnumerator/IEnumerator

(Мой детский разум интерпретирует это как)

(пункт 1)

    If the class is "wrapping" another collection, by returning the
wrapped collection's enumerator.

Будет ли это означать..

class  StringCollections
{

 //A class is wrapping another collection
  string[]  names=new string {“Jon Skeet”,”Hamish Smith”,
                  ”Marc Gravell”,”Jrista”,”Joren”};

//by returning the wrapped collection’s enumerator

 public IEnumerator GetEnumerator( )
  {
      // What should I return here ?
       //like the following ?
        yield return names[0];
        yield return names[1];
        yield return names[2];
      ....

       (or)
        foreach(string str in names)
        {
            yield return str;
         }                  

  }

}

(пункт 2)

•Via an iterator using yield return.(This point was well explained 
 by Marc Gravell)

Точка 3

By instantiating your own IEnumerator/IEnumerator<T> implementation*

Что здесь представляет пункт 3? Поскольку нет примера, я не получил его. Означает ли это, что я могу создать собственный счетчик .. (правильно?). Мой вопрос здесь в том, что когда предварительных счетчиков/перечислителей достаточно (как новичок, я не должен слепо подтверждать это) для итераций, почему я должен заботиться о пользовательском? Хорошо пример прояснит мои сомнения.

Спасибо за чтение этой длинной истории и добрый ответ.


person user193276    schedule 20.10.2009    source источник


Ответы (4)


(пункт 1) Вы можете связать вызов GetEnumerator с другим перечислителем коллекции:

public class PrimeNumbers : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        var primes = new List<int> { 2, 3, 5, 7, 11 };
        return primes.GetEnumerator();
    }
}

(пункт 2) Аналогично примеру в вашем коде

public IEnumerator GetEnumerator()
{
    var primes = new List<int> { 2, 3, 5, 7, 11 };
    foreach (var number in primes)
    {
        yield return number;
    }
}

Или поместите перечислитель с логикой:

public class PrimeNumbers : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        for(int i=2; ;i++)
        {
            if(IsPrime(i))
            {
                yield return i;
            }
        }
    }
}

(пункт 3) Существует не так много случаев, когда вы хотите реализовать свой собственный Enumerator, но, например, вы можете искать бесконечный набор значений, например. простые числа:

public class PrimeNumbers : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        return new MyEnumerator();
    }
}

public class MyEnumerator : IEnumerator
{
    private int lastPrimeNumber = 1;

    public bool MoveNext()
    {
        lastPrimeNumber = /* some logic that find the next prime */;
        return true; // There is always next prime
    }

    public void Reset()
    {
        lastPrimeNumber = 1;
    }

    public object Current
    {
        get { return lastPrimeNumber; }
    }
}

Пример использования может быть:

public void PrintAllPrimes()
{
    var numbers = new PrimeNumbers();

    // This will never end but it'll print all primes until my PC crash
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

Плюсы и минусы, о которых я могу думать:

  • Пункт 1. Это самый простой способ перечисления элементов, но он требует знания всех элементов заранее.
  • Пункт 2: когда в перечислении есть какая-то логика, это читабельно, а также лениво, поэтому нет необходимости вычислять элемент до тех пор, пока он не будет действительно запрошен.
  • Точка 3: повторное использование проще, но менее читабельно (вычислить элемент в MoveNext, но фактически вернуть его из свойства Current).
person Elisha    schedule 20.10.2009
comment
Спасибо, Елисей, за информацию - person user193276; 21.10.2009

Вот пример: (ПРИМЕЧАНИЕ: это упрощенный, а не потокобезопасный пример)

public class PersonCollection : IEnumerable
{
    private ArrayList alPers = new ArrayList();
    public IEnumerator GetEnumerator() { return new myTypeEnumerator(this); }
    public class myTypeEnumerator : IEnumerator
    {
        int nIndex;
        PersonCollection pers;
        private int count { get { return pers.alPers.Count; } }
        public myTypeEnumerator(PersonCollection myTypes) 
         { pers = myTypes; nIndex = -1; }
        public bool MoveNext() { return nIndex <= count && ++nIndex < count; }
        // MovePrev() not strictly required
        public bool MovePrev() { return (nIndex > -1 && --nIndex > 0 ); }
        public object Current { get { return (pers[nIndex]); } }
        public void Reset() { nIndex = -1; }
    }
}

РЕДАКТИРОВАТЬ: чтобы исправить проблему, поднятую @Joren ниже, связанную с тем, что Move Previous принимает значение индекса ниже -1. При вызове фреймворком как части реализации foreach, MoveNext() не нуждается в этом исправлении, потому что в этом случае, если MoveNext() возвращает false, экземпляр перечислителя завершится. Однако, если клиент вручную вызовет MoveNext(), перечислитель не завершится, поэтому он также нуждается в исправлении.

Также ПРИМЕЧАНИЕ: то, как вы реализуете детали внутри этой вещи, зависит от вас и будет зависеть от того, как она внутренне управляет состоянием. Например, если внутреннее «ведро», содержащее ссылки, было связанным списком, а не ArrayList, то MoveNext() можно было бы реализовать, просто изменив поле Current, чтобы оно указывало на свойство nextItem старого текущего...

person Charles Bretana    schedule 20.10.2009
comment
Спасибо, что потратили свое время. :) - person user193276; 21.10.2009
comment
Ваши MoveNext и MovePrev странные. Если я создам этот перечислитель, сделаю MovePrev() дважды и MoveNext() один раз, nIndex станет -1, но вызов вернет true. Если я не ошибаюсь, оба метода должны изменить nIndex, а затем вернуть 0 <= nIndex && nIndex < pers.alPers.Count. - person Joren; 21.10.2009
comment
@Joren, ошибка, исправлено. MovePrev должен сравниваться с нулем, а не со счетом. Но вам не нужен верхний индекс сравнения с другим концом диапазона... Он никогда не может быть меньше -1 или больше, чем count... как только он возвращает false, перечисление заканчивается... - person Charles Bretana; 21.10.2009
comment
@ Джорен, если подумать, я вижу, что вы явно вызываете методы Move ... и да, в этом случае вы правы. Но этот шаблон предназначен для использования платформой, когда клиентский код использует foreach... и в этом случае сгенерированный инфраструктурой код IL вызывает MoveNext()... и то, что вы описываете, не может произойти... Но если MovePrev() находится там, он должен быть закодирован для обработки вашего сценария... - person Charles Bretana; 21.10.2009
comment
Я думаю, что код перечислителя должен быть достаточно надежным, чтобы справиться с этим случаем. MovePrev вообще никогда не будет вызываться foreach. Если вы включаете его, он должен работать должным образом. - person Joren; 21.10.2009

То, что вы сказали для 1, на самом деле будет для 2.

Для 1 это было бы больше похоже на

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

Для пункта 3 это означало бы заставить GetEnumerator проделать некоторую реальную работу: реализовать вручную Enumerator для определенной вами коллекции.

person Tordek    schedule 20.10.2009

«Если класс «обертывает» другую коллекцию, возвращая перечислитель обернутой коллекции» означает:

class StringCollections{
    //A class is wrapping another collection
    string[] names=new string {“Jon Skeet”,”Hamish Smith”,
                                ”Marc Gravell”,”Jrista”,”Joren”};
    public IEnumerator GetEnumerator() { return names.GetEnumerator(); }
}
person erikkallen    schedule 20.10.2009