Должен ли IEnumerable итератор в очереди выводить из очереди элемент - PullRequest
15 голосов
/ 16 ноября 2010

Я создал настраиваемую универсальную очередь, которая реализует универсальный интерфейс 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();
    }

    ...
}

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

EDIT

Чтобы поместить вещи в контекст.Класс, который я реализую, делает Monitor.Wait при снятии очереди, и в очереди нет элементов.Когда элемент помещается в очередь, появляется 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 является одним из вариантов использования.Возможно, это заслуживает отдельного вопроса?

EDIT

В другом вопросе

поставлен вопрос о реализации метода GetEnumerator без реализации IEnumerable *.

Ответы [ 10 ]

18 голосов
/ 16 ноября 2010

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

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


Отредактируйте, чтобы адресовать ваши новые комментарии:

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

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

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

12 голосов
/ 16 ноября 2010

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

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

5 голосов
/ 16 ноября 2010

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

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

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

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

2 голосов
/ 16 ноября 2010

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

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

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

//queue is full
foreach(var item in queue)
    Console.WriteLine(item.ToString());
//queue is empty!?
1 голос
/ 16 ноября 2010

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

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

0 голосов
/ 16 ноября 2010

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

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

0 голосов
/ 16 ноября 2010

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

0 голосов
/ 16 ноября 2010

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

0 голосов
/ 16 ноября 2010

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

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

...