如果队列上的IEnumerable迭代器应该使项目出列
我创建了一个自定义通用队列,它实现了一个通用的IQueue接口,该接口使用System.Collections.Generic命名空间中的通用Queue作为私有内部队列。 示例已清除不相关的代码。
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-back
, pop-front
, is-empty
以及可能get-size
操作。 您添加的所有内容都不是队列的一部分,因此如果您决定提供其他操作,则无需保留队列的语义。
特别是,迭代器不是标准队列接口的一部分,因此您不需要让它们删除正在迭代的项目当前。 (正如其他人所指出的那样,这也会对迭代器的期望产生矛盾。)
遍历队列不应该出列队列。
这旨在查看检查队列的内容不要出列。 这也是MSMQ
或MQ Series
工作原理。
我认为不应该。 它将是非常隐含的,并且它不会将此意图传达给任何习惯于使用.net框架的人。
我会说第二个。 您的枚举器绝对不应该更改集合的状态。
我有一些代码,其中一个属性得到的并不是幂等的…解码是如此痛苦。 请坚持手动Dequeue。
你也可能不是唯一一个在队列中工作的人,所以它可能会让一个以上的消费者感到麻烦。