我创建了一个自定义通用队列,它实现了一个通用的IQueue接口,该接口使用System.Collections.Generic命名空间中的通用Queue作为私有内部队列。示例已被清除不相关的代码。
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接口继承它来做同样的事情。
我想知道的是,在队列中进行枚举时,每次移动都会使下一个项目出列吗?我使用反射器来看看微软是如何做到的,他们所做的就是逐步完成队列私有阵列,但微软远非绝对不可靠,所以我想获得一般意见。
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();
}
...
}
我有两种想法,一方面我觉得迭代一个集合不应该改变集合状态,但另一方面,特别是对于我的特定实现,它会使用法看起来很干净。
修改
把事情放到上下文中。我正在实现的类在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方法而不在另一个question中实现IEnumerable的问题。
答案 0 :(得分:18)
迭代器应该始终是幂等的,也就是说,不要在迭代时修改队列。
无法保证不会有两个并发迭代...
编辑以解决您的新评论:
当另一个程序员(例如你未来的自己;))出现为代码添加功能时,他们可能不会认为迭代器是一次性使用的。他们可能会添加一个日志语句,在使用它之前列出队列中的内容(oops)。
我刚想到的另一件事是Visual Studio调试器通常会枚举您的类以供显示。这会导致一些非常混乱的错误:)
如果您正在实现IEnumerable的子接口,并且不想支持IEnumerable,则应该抛出NotSupportedException。虽然这不会给你任何编译时警告,但运行时错误将非常清楚,而一个奇怪的IEnumerable实现可能会浪费你的未来几个小时。
答案 1 :(得分:12)
绝对肯定的是,在迭代它时你不应该改变一个集合。迭代器的整个点是它们提供集合的只读非破坏性视图。任何使用你的代码的人都会对它进行改变,这将是非常令人惊讶的。
特别是:您不希望在调试器中检查队列的状态以进行更改。调试器就像任何其他消费者一样调用IEnumerable,如果有副作用,它们就会被执行。
答案 2 :(得分:5)
我建议您可能希望有一个名为DequeueAll的方法,它返回一个类的项,该类具有表示队列中所有内容的GetEnumerator方法,并清除队列(如果在队列项周围添加了队列项)创建iEnumerable的时间,新项目应该出现在AllItemsDequeued的当前所有内容中,而不是在队列中,或队列中,而不是当前调用中。如果此类实现了iEnumerable,则应该以这样的方式构造它,即使在创建和处理枚举器之后返回的对象仍然有效(允许多次枚举它)。如果这样做是不切实际的,那么为类提供一个名称可能是有用的,该名称表明该类的对象不应该被持久化。人们仍然可以这样做
foreach(QueueItem theItem in theQueue.DequeueAll()) {}但不太可能(错误地)将theQueue.DequeueAll的结果保存到iEnumerable。如果想要最大性能同时允许将Queue.DequeueAll的结果用作iEnumerable,则可以定义一个扩展转换,它将获取DequeueAll结果的快照(从而允许丢弃旧项)。
< / p>
答案 3 :(得分:3)
我要离开这个潮流并说是。这似乎是一种合理的方法。虽然我必须提出一个建议。也就是说,不要在GetEnumerator
方法中实现这个消耗迭代器。而是将其称为GetConsumingEnumerator
或类似的东西。这样很明显会发生什么,foreach
机制默认不会使用它。你不会是第一个这样做的人。实际上,Microsoft已经通过BlockingCollection
类在BCL中执行此操作,他们甚至使用GetConsumingEnumerator
作为方法 1 。我想你知道我接下来会建议什么吗? 2
1 您认为我如何提出姓名建议?
2 为什么不使用BlockingCollection
?它可以满足您的所有要求。
答案 4 :(得分:2)
我会说不,第二种方式。
这不仅与内置的Queue类更加一致,而且与IEnumerable<T>
接口是只读的这一事实更加一致。
另外,这对您来说真的很直观吗?:
//queue is full
foreach(var item in queue)
Console.WriteLine(item.ToString());
//queue is empty!?
答案 5 :(得分:1)
严格来说,队列仅提供push-back
,pop-front
,is-empty
和get-size
次操作。您添加的所有内容都不是队列的一部分,因此如果您决定提供其他操作,则无需保留队列的语义。
特别是,迭代器不是标准队列接口的一部分,因此您不需要让它们删除正在迭代的项目当前。 (正如其他人所指出的那样,这也会对迭代器的期望产生矛盾。)
答案 6 :(得分:0)
通过队列迭代NOT
出列。
这旨在查看检查队列的内容不要出列。这也是MSMQ
或MQ Series
的工作原理。
答案 7 :(得分:0)
我认为不应该。它将非常隐含,并且它不会将此意图传达给任何习惯于使用.net框架的人。
答案 8 :(得分:0)
我会说第二个。你的枚举器肯定不应该改变集合的状态。
答案 9 :(得分:0)
我有一个代码,其中一个属性得到的不是幂等的...解码是一件很痛苦的事。请坚持手动Dequeue。
此外,您可能不是唯一一个在队列中工作的人,因此可能会让一个以上的消费者感到麻烦。