如果队列上的IEnumerable迭代器应该使项目出列

我创建了一个自定义通用队列,它实现了一个通用的IQueue接口,该接口使用System.Collections.Generic命名空间中的通用Qu​​eue作为私有内部队列。 示例已清除不相关的代码。

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

我希望保持与核心实现的一致性,并注意到核心Queue实现了IEnumerable,所以我将通过在类上显式实现IEnumerable或使用IQueue接口inheritance它来做同样的事情。

我想知道的是,当列举队列时,如果每次移动都会使下一个项目出列? 我已经使用reflection器来了解微软是如何做到的,他们所做的就是逐步完成队列私有arrays,但微软远非绝对不可靠,所以我想获得一般意见。

 public class CustomQueue : IQueue, IEnumerable { ... public IEnumerator GetEnumerator() { while (queue.Count > 0) { yield return Dequeue(); } } //Or public IEnumerator GetEnumerator() { return queue.GetEnumerator(); } ... } 

我有两个想法,一方面我觉得迭代一个集合不应该改变集合状态,另一方面,特别是对于我的特定实现,它会使用法看起来干净。

编辑

把事情放到上下文中。 我正在实现的类在Dequeuing时执行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语言支持duck typing,foreach是其中一种用途。 也许这值得拥有它自己的问题?

编辑

提出了实现GetEnumerator方法而不在另一个问题中实现IEnumerable的问题 。

迭代器应该始终是幂等的,也就是说,在迭代它时不要修改队列。

无法保证不会有两个并发迭代……


编辑以解决您的新评论:

当另一个程序员(例如你未来的自己;))出现为代码添加function时,他们可能不会认为迭代器是一次性使用的。 他们可能会添加一个日志语句,列出使用它之前队列中的内容(oops)。

我刚才想到的另一件事是,visual studio调试器通常会枚举你的类以供显示。 这会导致一些非常混乱的错误:)

如果您正在实现IEnumerable的子接口,并且不想支持IEnumerable,则应该抛出NotSupportedException。 虽然这不会给你任何编译时警告,但运行时错误将非常清楚,而一个奇怪的IEnumerable实现可能会浪费你的未来几个小时。

绝对肯定的是,在迭代它时你不应该改变一个集合。 迭代器的重点在于它们提供了集合的只读非破坏性视图。 任何使用你的代码的人都会发现它会改变它,这将是非常令人惊讶的。

特别是: 您不希望在调试器中检查队列的状态以进行更改 。 调试器像任何其他消费者一样调用IEnumerable,如果有副作用,则执行它们。

我建议您可能希望有一个名为DequeueAll的方法,它返回一个类的项,该类具有表示队列中所有内容的GetEnumerator方法,并清除队列(如果在iEnumerable时间周围添加了一个队列项)如果创建了新项,则新项应该出现在AllItemsDequeued中,而不是在队列中,或队列中,而不是当前调用中。 如果此类实现了iEnumerable,则应该以这样的方式构造它,使得即使在创建和处理枚举器之后返回的对象仍然有效(允许多次枚举它)。 如果这样做是不切实际的,那么为类赋予一个名称可能是有用的,该名称表明该类的对象不应该被持久化。 人们仍然可以这样做

  foreach(QueueItem theItem inQueue.DequeueAll()){} 

但不太可能(错误地)将theQueue.DequeueAll的结果保存到iEnumerable。 如果想要最大性能同时允许将Queue.DequeueAll的结果用作iEnumerable,则可以定义一个扩展的转换,它将获取DequeueAll结果的快照(从而允许丢弃旧项)。

我要离开这个潮流并说是的 。 这似乎是一种合理的方法。 虽然我必须提出一个建议。 也就是说,不要在GetEnumerator方法中实现这个消耗迭代器。 而是将其GetConsumingEnumerator或类似的东西。 这样很明显会发生什么, foreach机制默认不会使用它。 你不会是第一个这样做的人。 实际上,Microsoft已经通过BlockingCollection类在BCL中执行此操作,他们甚至使用GetConsumingEnumerator作为方法1 。 我想你知道接下来我会建议什么吗? 2

1 你觉得我怎么想出名字的建议? 2 为什么不直接使用BlockingCollection 它可以满足您的所有需求。

我会说不,第二种方式。

这不仅与内置的Queue类更加一致,而且与IEnumerable接口意图是只读的事实更加一致。

另外,这对你来说真的很直观吗?:

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

严格来说,队列只提供push-backpop-frontis-empty以及可能get-size操作。 您添加的所有内容都不是队列的一部分,因此如果您决定提供其他操作,则无需保留队列的语义。

特别是,迭代器不是标准队列接口的一部分,因此您不需要让它们删除正在迭代的项目当前。 (正如其他人所指出的那样,这也会对迭代器的期望产生矛盾。)

遍历队列不应该出列队列。

这旨在查看检查队列的内容不要出列。 这也是MSMQMQ Series工作原理。

我认为不应该。 它将是非常隐含的,并且它不会将此意图传达给任何习惯于使用.net框架的人。

我会说第二个。 您的枚举器绝对不应该更改集合的状态。

我有一些代码,其中一个属性得到的并不是幂等的…解码是如此痛苦。 请坚持手动Dequeue。

你也可能不是唯一一个在队列中工作的人,所以它可能会让一个以上的消费者感到麻烦。