Если IEnumerable iterator в очереди перечеркивает элемент

Я создал пользовательскую общую очередь, которая реализует общий интерфейс IQueue, который использует общую очередь из пространства имен System.Collections.Generic как частную внутреннюю очередь. Пример был очищен от нерелевантного кода.

public interface IQueue<TQueueItem>
{
    void Enqueue(TQueueItem queueItem);
    TQueueItem Dequeue();
}

public class CustomQueue<TQueueItem> : IQueue<TQueueItem>
{
    private readonly Queue<TQueueItem> queue = new Queue<TQueueItem>();
    ...
    public void Enqueue(TQueueItem queueItem)
    {
        ...
        queue.Enqueue( queueItem );
        ...
    }

    public TQueueItem Dequeue()
    {
        ...
        return queue.Dequeue();
        ...
    }
}

Я хочу, чтобы все было согласовано с основными реализациями, и заметили, что основная Queue реализует IEnumerable, поэтому я буду делать то же самое, явно используя IEnumerable в классе или наследуя его с помощью интерфейса IQueue.

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

public class CustomQueue<TQueueItem> : IQueue<TQueueItem>, IEnumerable<TQueueItem>
{
    ...

    public IEnumerator<TQueueItem> GetEnumerator()
    {
        while (queue.Count > 0)
        {
            yield return Dequeue();
        }
    }

    //Or

    public IEnumerator<TQueueItem> GetEnumerator()
    {
        return queue.GetEnumerator();
    }

    ...
}

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

ИЗМЕНИТЬ

Вставить вещи в контекст. Класс, который я реализую, выполняет Monitor.Wait, когда Dequeuing и нет элементов в очереди. Когда элемент помещается в очередь, появляется Monitor.Pulse. Это позволяет одному потоку подталкивать материал в очередь, а другой - "смотреть" очередь.

С точки зрения кодирования я пытаюсь решить, какой из них выглядит более чистым:

foreach(QueueItem item in queue)
{
    DoSomethingWithThe(item);
}

//Or

while(systemIsRunning)
{
    DoSomethingWithThe(queue.Dequeue());
}

Для моей конкретной реализации не имело бы значения, было ли несколько объектов, связанных с процессом. Поскольку это очередь, они могут оба выбрать элемент, так как ни один элемент не должен обрабатываться более одного раза, следовательно, использование очереди.

ИЗМЕНИТЬ

Интересно, что я нашел сообщение в блоге, где кто-то сделал именно это.

http://blogs.msdn.com/b/toub/archive/2006/04/12/blocking-queues.aspx

ИЗМЕНИТЬ

Один последний удар в этом, прежде чем я закрою это. Как люди чувствуют, что класс не реализует IEnumerable, но имеет метод IEnumerator GetEnumerator(), который отменяет элементы? Язык .net поддерживает утиную печать, foreach является одним из видов использования. Возможно, это заслуживает собственного вопроса?

ИЗМЕНИТЬ

Возник вопрос о внедрении метода GetEnumerator без реализации IEnumerable в другом question.

Ответы

Ответ 1

Итераторы всегда должны быть идемпотентными, то есть не изменять очередь, когда вы перебираете ее.

Нет никаких гарантий, что не будет двух одновременных итераций...


Изменить, чтобы отправлять новые комментарии:

Когда другой программист (например, ваш будущий self;)) приходит, чтобы добавить функции в код, они не могут считать, что итераторы одноразовые. Они могут добавить оператор журнала, который перечисляет то, что находится в очереди перед его использованием (oops).

Еще одна вещь, о которой я только подумал, заключается в том, что отладчик визуальной студии часто перечисляет ваши классы для отображения. Это вызовет некоторые чрезвычайно запутывающие ошибки:)

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

Ответ 2

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

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

Ответ 3

Я бы предположил, что вам может понадобиться метод, называемый DequeueAll, который возвращает элемент класса, который использует метод GetEnumerator, который представляет все в очереди, и очищает очередь (если элемент очереди добавляется вокруг время создания iEnumerable, новый элемент должен либо отображаться в настоящем все в AllItemsDequeued, но не в очереди или в очереди, но не в текущем вызове). Если этот класс реализует iEnumerable, он должен быть сконструирован таким образом, чтобы возвращаемый объект оставался в силе даже после того, как был создан и удален нумератор (позволяя его перечислять несколько раз). Если это было бы непрактично, может быть полезно дать классу имя, которое предполагает, что объекты класса не должны сохраняться. Можно все еще

foreach(QueueItem theItem in theQueue.DequeueAll()) {}
, но будет менее вероятным (неправомерно) сохранить результат Queue.DequeueAll в iEnumerable. Если вам нужна максимальная производительность, позволяя результат QQueue.DequeueAll использоваться как iEnumerable, можно было бы определить расширяющийся отрисовку, в которой был бы снимок результата DequeueAll (таким образом, чтобы старые элементы были отброшены).

Ответ 4

Я собираюсь оставить победителя и сказать да. Это кажется разумным подходом. Хотя я должен сделать одно предложение. То есть не реализуйте этот потребляющий итератор в методе GetEnumerator. Вместо этого назовите его GetConsumingEnumerator или что-то похожее. Таким образом, очевидно, что произойдет, и механизм foreach не будет использовать его по умолчанию. Вы не будете первым, кто это сделает. Фактически, Microsoft уже делает это в BCL через класс BlockingCollection, и они даже использовали GetConsumingEnumerator как метод 1. Я думаю, вы знаете, что я предлагаю в следующий раз? 2

1 Как вы думаете, я придумал предложение названия? 2 Почему бы просто не использовать BlockingCollection? Он делает все, что вы просили, и многое другое.

Ответ 5

Я бы сказал нет, сделайте это вторым способом.

Это не только совместимо со встроенным классом Queue, но также более согласуется с тем, что интерфейс IEnumerable<T> предназначен только для чтения.

Кроме того, действительно ли это кажется вам интуитивным?:

//queue is full
foreach(var item in queue)
    Console.WriteLine(item.ToString());
//queue is empty!?

Ответ 6

Строго говоря, очередь предоставляет только операции push-back, pop-front, is-empty и, возможно, get-size. Все, что вы добавляете рядом с этим, не является частью очереди, поэтому, если вы решите предоставить дополнительные операции, вам не нужно сохранять семантику очереди.

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

Ответ 7

Итерация через очередь должна NOT dequeue.

Это предназначено для просмотра содержимого очереди, чтобы не деактивировать. Это также работает MSMQ или MQ Series.

Ответ 8

Я не думаю, что это нужно. Он был бы очень неявным и не передавал бы это намерение любому, кто используется для использования .net framework.

Ответ 9

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

Ответ 10

У меня был код, в котором приспешность не была идемпотентной... Это было болью для декодирования. Пожалуйста, придерживайтесь руководства Dequeue.

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