【问题标题】:Why do Queue(T) and Stack(T) not implement ICollection(T)?为什么 Queue(T) 和 Stack(T) 没有实现 ICollection(T)?
【发布时间】:2011-06-14 03:45:20
【问题描述】:

在我问之前,让我得到一个明显的答案:ICollection<T> 接口包含一个 Remove 删除任意元素的方法,Queue<T>Stack<T> 不能真的支持(因为他们只能删除“结束”元素)。

好的,我意识到这一点。实际上,我的问题并不是专门针对 Queue<T>Stack<T> 集合类型;相反,它是关于不为 any 本质上是 T 值集合的泛型类型实现 ICollection<T> 的设计决策。

这就是我觉得奇怪的地方。假设我有一个接受T 的任意集合的方法,并且出于我正在编写的代码的目的,知道集合的大小会很有用。例如(下面的代码很简单,仅供说明!):

// Argument validation omitted for brevity.
static IEnumerable<T> FirstHalf<T>(this ICollection<T> source)
{
    int i = 0;
    foreach (T item in source)
    {
        yield return item;
        if ((++i) >= (source.Count / 2))
        {
            break;
        }
    }
}

现在,除了这些类型没有实现 ICollection&lt;T&gt; 之外,没有理由不能在 Queue&lt;T&gt;Stack&lt;T&gt; 上运行此代码。他们确实实现了ICollection,当然——我猜主要是为了Count属性——但这会导致像这样奇怪的优化代码:

// OK, so to accommodate those bastard Queue<T> and Stack<T> types,
// we will just accept any IEnumerable<T>...
static IEnumerable<T> FirstHalf<T>(this IEnumerable<T> source)
{
    int count = CountQuickly<T>(source);
    /* ... */
}

// Then, assuming we've got a collection type with a Count property,
// we'll use that...
static int CountQuickly<T>(IEnumerable collection)
{
    // Note: I realize this is basically what Enumerable.Count already does
    // (minus the exception); I am just including it for clarity.
    var genericColl = collection as ICollection<T>;
    if (genericColl != null)
    {
        return genericColl.Count;
    }

    var nonGenericColl = collection as ICollection;
    if (nonGenericColl != null)
    {
        return nonGenericColl.Count;
    }

    // ...or else we'll just throw an exception, since this collection
    // can't be counted quickly.
    throw new ArgumentException("Cannot count this collection quickly!");
}

完全放弃ICollection 接口不是更有意义(当然,我不是说放弃实现,因为这将是一个重大变化;我只是说,停止使用它),并简单地为没有完美匹配的成员实现 ICollection&lt;T&gt; 并显式实现?

我的意思是,看看ICollection&lt;T&gt; 提供什么:

  • Count -- Queue&lt;T&gt;Stack&lt;T&gt; 都有这个。
  • IsReadOnly -- Queue&lt;T&gt;Stack&lt;T&gt; 很容易可以拥有这个。
  • Add -- Queue&lt;T&gt; 可以显式实现这一点(Enqueue),Stack&lt;T&gt;Push)也可以。
  • Clear -- 检查。
  • Contains -- 检查。
  • CopyTo -- 检查。
  • GetEnumerator -- 检查 (duh)。
  • Remove -- 这是唯一一个 Queue&lt;T&gt;Stack&lt;T&gt; 没有完美匹配的。

这是真正的关键:ICollection&lt;T&gt;.Remove 返回一个 bool;因此Queue&lt;T&gt; 的显式实现可以完全(例如)检查要删除的项目是否实际上是头元素(使用Peek),如果是,则调用Dequeue并返回true,否则返回false。使用PeekPop 可以轻松地为Stack&lt;T&gt; 提供类似的实现。

好吧,既然我已经写了大约一千字来说明为什么认为这是可能的,我提出一个显而易见的问题:为什么没有 em> Queue&lt;T&gt;Stack&lt;T&gt; 的设计者实现了这个接口? 也就是说,是什么设计因素(我可能没有考虑)导致决定这是错误的选择?为什么改为实现ICollection

我想知道,在设计我自己的 类型时,我是否应该考虑关于接口实现的任何指导原则,而我在提出这个问题时可能会忽略这些指导原则。例如,显式实现通常不完全支持的接口是否被认为是不好的做法(如果是这样,这似乎与List&lt;T&gt;实现IList相冲突)?队列/堆栈的概念与ICollection&lt;T&gt; 所代表的含义之间是否存在概念脱节?

基本上,我觉得Queue&lt;T&gt;(例如)没有实现ICollection&lt;T&gt;,我不想只是盲目地向前设计我的自己的类型和以不适当的方式实现接口,而没有被告知并充分考虑我在做什么。

对于这个超长的问题,我深表歉意。

【问题讨论】:

  • 这只是 MS 糟糕的收藏库的另一个例子。这个问题可以通过至少将接口分为只读和读写版本(读写版本从只读版本继承)以及更细粒度的接口来解决,只关注一个集合的特定方面,而不是包含不相关的属性和方法。所以 ICollection 不应该包含 Add、Clear 或 Remove。这应该留给例如 IMutableCollection。 ICollection 可以由更多类实现。
  • @siride:您对 MS 馆藏库的看法与我的相同。令人讨厌的是,索引属性之类的东西必须为只读和读写版本定义两次,但这就是生活。
  • +1 用于实现 Remove 的创新,这很酷 :) 不,这个超长的 q 写得很好。

标签: .net interface stack queue icollection


【解决方案1】:

Philip 给出了一个很好的答案 (+1)。还有另一个概念上的承诺,Remove 将因Stack&lt;T&gt; 而失效。 ICollection&lt;T&gt;.Remove 记录为:

从 ICollection 中移除特定对象的第一次

Stack&lt;T&gt; 是 LIFO,即使实现了Remove,它也必须删除最后一次出现的重复对象。

如果它对Stack&lt;T&gt; 没有意义,最好避免它的平等和相反的表亲。


如果 MS:我会更喜欢它:

  • 没有像这样记录 RemoveICollection&lt;T&gt;。考虑到各种结构的内部实现有多么不同,在某处删除一个相等的对象会更有意义。强制删除第一项似乎受到了数组等简单结构的线性搜索的影响。

  • 有队列结构的接口。可能是:

    public interface IPeekable<T> : IEnumerable<T> // or IInOut<T> or similar
    {
        int Count { get; }
    
        bool Contains(T item);
        T Peek();
        void Add(T item);
        bool Remove();
        void Clear();
    }
    

【讨论】:

    【解决方案2】:

    我无法给出“实际想法是什么”的答案 - 也许其中一位设计师会给我们真实的想法,我可以删除它。

    但是,让自己陷入“如果有人来找我做这个决定怎么办”的心态,我可以想到一个答案..让我用这段代码来说明:

    public void SomeMethod<T>( ICollection<T> collection, T valueA, T valueB)
    {
    
      collection.Add( valueA);
      collection.Add( valueB);
    
      if( someComplicatedCondition())
      {
        collection.Remove(valueA);
      }
    }
    

    (当然,任何人都可以创建 ICollection&lt;T&gt; 的错误实现,但我们希望框架能够树立榜样)。让我们假设您在问题中陈述的 Stack/Queue 实现。那么上面的代码是正确的,还是因为应该检查ICollection&lt;T&gt;.Remove()而存在边缘情况错误?如果valueA 必须被删除,我该如何解决这个问题以同时使用堆栈队列?有答案,但显然上面的代码在这种情况下是错误的——即使它闻起来很合理。

    所以这两种解释都是有效的,但我对这里做出的决定很满意——如果我有上面的代码并且知道我可以传递一个可以围绕它设计的队列或堆栈,但这肯定会很容易要掉进的 bug 坑(随处可见 ICollection&lt;T&gt;,记住要删除的边缘情况!)

    【讨论】:

    • 我认为这是一个很好的例子,它使这里的推理非常具体。感谢您帮助我解决这个问题。展望未来,我将挑战自己,想出“不完全”实现某个接口会导致非常意外的行为的示例——在这种情况下,我会在提交这些接口之前仔细考虑。
    【解决方案3】:

    最终,也许他们只是不合适;如果你想要一个列表或集合 - 使用 List&lt;T&gt;Collection&lt;T&gt; ;p

    关于Add - 有一个隐含的假设,即Add 添加到集合的end,这不适用于堆栈(尽管它适用于队列)。在某些方面,堆栈/队列的枚举器出列/弹出实际上让我感到困惑,因为我主要希望队列/堆栈中的项目每次(且仅一次)获取一次。

    也许还有(再次,以我的枚举器为例)人们根本无法就它在某些场景中的行为如何达成一致,并且缺乏完全一致,只是 不实施它们是更好的选择。

    【讨论】:

    • @Marc:没问题;人们一直把这两者混为一谈(我知道我有)。我知道您对枚举队列/堆栈的意思;谁曾枚举一个队列却不想真正从队列中出队?我想部分问题是IEnumerator(T) 的文档说:“枚举器可用于读取集合中的数据,但不能用于修改基础集合。”有点把自己放在一个角落里。
    • @Dan Tao:解决方案是不让 IStack 和 IQueue 实现 IEnumerable,而是让它们提供返回 IEnumerable 的 DequeueAll 或 PopAll 方法。请注意,线程安全的 IStack 的 PopAll 方法可能会产生与手动从堆栈中弹出所有内容不同的结果,因为线程安全的 PopAll 应该保证在 PopAll 时间前后推送的项目要么是返回的最顶层项目,要么应该不返回但保留在堆栈中;手动的“pop”操作序列没有这样的保证。
    • @supercat:我同意,在不使用IEnumerable&lt;T&gt; 实现的情况下,确实有一些很好的方法可以完成此行为;也就是说,我不同意Queue&lt;T&gt;Stack&lt;T&gt; 不应该 实现IEnumerable&lt;T&gt;。这似乎是一种罕见的情况,您想在不删除的情况下进行枚举。就个人而言,我确实使用了与您的DequeueAllPopAll 建议执行基本相同的工作的扩展方法。
    • @Dan Tao:Queue 和 Stack 实现 IEnumerable 并没有错,但是让 IQueue 和 IStack 这样做限制了可以实现此类接口的类的设计。如果您希望能够在不更改队列或堆栈的情况下查看它,则应使用明确定义这样做语义的接口,并让该接口继承自 IQueue(Of T) 或 IStack(Of T)。
    • @supercat:好的,你说的是集合库中的hypothetical IQueue&lt;T&gt;IStack&lt;T&gt; 接口,对吧?我可以同意你的观点。不过,AFAIK 在 BCL 中没有这样的接口。
    猜你喜欢
    • 2011-03-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-24
    • 1970-01-01
    • 1970-01-01
    • 2015-09-25
    • 2012-05-22
    相关资源
    最近更新 更多