为什么 Queue(T) 和 Stack(T) 没有实现 ICollection(T) 接口?

17

在我提问之前,让我先说一个显而易见的答案:ICollection<T>接口包含一个Remove方法来删除任意元素,而Queue<T>Stack<T>不能真正支持这一点(因为它们只能删除“末尾”元素)。

好的,我明白了。实际上,我的问题不是特别针对Queue<T>Stack<T>集合类型;相反,它关于不为任何本质上是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;
        }
    }
}

现在,这段代码完全可以运行在一个Queue<T>或者Stack<T>上,只是这些类型没有实现ICollection<T>。当然,它们实现了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中没有完全匹配的成员。看看ICollection提供了什么:Count--Queue和Stack都有这个;IsReadOnly--Queue和Stack很容易也可以有这个;Add--Queue可以显式实现它(使用Enqueue),Stack也可以(使用Push);Clear、Contains和CopyTo都有;GetEnumerator--当然有;Remove--这是唯一一个Queue和Stack没有完全匹配的。ICollection.Remove返回一个bool,所以Queue可以显式实现Remove并检查要删除的项是否真的是头元素(使用Peek),如果是,则调用Dequeue并返回true,否则返回false。Stack可以使用Peek和Pop轻松地给出类似的实现。既然我已经写了大约一千个字解释为什么我认为这是可能的,那么我要问的显然问题是:为什么Queue和Stack的设计者没有实现这个接口?也就是说,是哪些设计因素(我可能没有考虑到的)导致了这个决定,认为这是错误的选择?为什么要实现ICollection呢?
我想知道在设计自己的类型时,是否有任何指导原则需要考虑与接口实现有关的问题,我可能在提出这个问题时忽略了一些东西。例如,明确实现通常不支持的接口是否只是被认为是不好的实践(如果是这样,那么这似乎就与List<T>实现IList相矛盾)?队列/堆栈的概念和ICollection<T>所代表的概念是否存在概念上的断开?
基本上,我感觉肯定有一个很好的理由,例如Queue<T>没有实现ICollection<T>,我不想在未经通知和充分考虑的情况下盲目地设计自己的类型并以不合适的方式实现接口。
我很抱歉这个问题太长了。

4
微软又一次展现了其不佳的集合库。这个问题至少可以通过将接口分成只读和读写版本来解决(其中读写版本继承自只读版本),并且还应该有更细致的接口,只关注集合的特定方面而不包括无关的属性和方法。所以ICollection<T>不应该包含Add、Clear或Remove等方法。这些方法应该留给例如IMutableCollection<T>这样的接口。ICollection<T>可以被更多类实现。 - siride
@siride:你对MS集合库的看法与我相似。令人恼火的是,像索引属性这样的东西必须为只读和读写版本分别定义两次,但这就是生活。 - supercat
+1 对于独立实现“Remove”的创新,很棒 :) 不过,这个超长的问题确实写得很好。 - nawfal
3个回答

6

我无法给出“实际思考是什么”的答案 - 或许其中一位设计师会给我们一个真正的想法,那时我可以删除这个回答。

然而,如果有人来请教我如何做出这个决定,我可以考虑一下。让我用这段代码来说明:

public void SomeMethod<T>( ICollection<T> collection, T valueA, T valueB)
{

  collection.Add( valueA);
  collection.Add( valueB);

  if( someComplicatedCondition())
  {
    collection.Remove(valueA);
  }
}

(当然,任何人都可以创建一个糟糕的 ICollection<T> 实现,但我们期望框架树立榜样)。让我们假设 Stack/Queue 的实现与您在问题中所述相同。那么上面的代码是正确的,还是因为必须检查 ICollection<T>.Remove() 而存在边缘情况漏洞?如果必须删除 valueA,我该如何修复以使其适用于 Stack 和 Queue?有答案,但显然上面的代码在这种情况下是错误的 - 即使它看起来合理。

因此,两种解释都是有效的,但我认可这里做出的决定 - 如果我有上面的代码,并知道可能会传递给我的是一个我可以设计的 Queue 或 Stack,但肯定会很容易掉入 bug 陷阱(每次看到 ICollection<T>,请记住 remove 的边缘情况!)


我认为这是一个很好的例子,使得这里的推理非常具体。感谢您帮助我理解这个问题。未来,我将挑战自己提出一些例子,其中实现某个接口“不完整”会导致高度意外的行为,在这种情况下,我会仔细考虑再三才会承诺使用这些接口。 - Dan Tao

1

最终可能它们只是不是理想的选择;如果您需要列表或集合,请使用 List<T> 或者 Collection<T> ;p

关于 Add - 它有一个隐含的假设,即 Add 添加到集合的末尾,但对于栈来说这并不正确(虽然对于队列来说是正确的)。在某种程度上,我觉得栈/队列的枚举器 不会 出队/弹出,这实际上让我感到困惑,因为我通常期望从队列/栈中获取每个项一次(仅一次)。

也许,人们在某些情况下根本就不能同意它应该如何行事,缺少完全的协议,不实现它们是更好的选择(再次以我的枚举器为例)。


@Marc:没问题;人们经常混淆这两个(我也是)。我知道你所说的枚举队列/栈的意思;谁会在不想从中出队的情况下枚举队列呢?我想问题的一部分在于 IEnumerator(T) 的文档说:“枚举器可用于读取集合中的数据,但不能用于修改基础集合。” 这有点把自己限制住了。 - Dan Tao
@Dan Tao:解决方案是不让IStack和IQueue实现IEnumerable,而是提供DequeueAll或PopAll方法,该方法将返回IEnumerable。请注意,对于线程安全的IStack,PopAll方法可能会产生与手动弹出堆栈中所有内容不同的结果,因为线程安全的PopAll应保证在PopAll周围推送的项目要么是返回的最顶部项目,要么不应返回但仍留在堆栈中;手动的“pop”操作序列没有这样的保证。 - supercat
@supercat:我同意,确实有很好的方法可以在不使用IEnumerable<T>实现的情况下完成这种行为;尽管如此,我不认为Queue<T>Stack<T>不应该实现IEnumerable<T>。这似乎只是一个罕见的情况,你想要枚举而不删除。就我个人而言,我使用扩展方法执行基本上与您的DequeueAllPopAll建议相同的工作。 - Dan Tao
@Dan Tao:队列和栈实现IEnumerable没有问题,但是让IQueue和IStack也这样做限制了能够实现这些接口的类的设计。如果想要在不改变队列或栈的情况下查看其内容,应该使用显式定义此语义的接口,并让该接口继承自IQueue(Of T)或IStack(Of T)。 - supercat
@supercat:好的,所以你在谈论一个假设的集合库中的 IQueue <T>IStack <T> 接口对吧?我同意你的观点。据我所知,在 BCL 中并没有这样的接口。 - Dan Tao
@Dan Tao:嗯...我记得我看过IStack和IQueue的实现,但可能只是其他人编写的接口,而不是微软的。我仍然不太喜欢像堆栈这样的东西自己实现IEnumerable,因为我认为一个名为PeekAsEnumerable的方法会有更自然的语义。 - supercat

0

Philip 给出了一个很好的答案(+1)。还有另一个概念上的承诺,即 Remove 将会对 Stack<T> 产生影响。ICollection<T>.Remove 的文档如下:

从 ICollection 中移除特定对象的第一个匹配项。

Stack<T> 是后进先出,即使实现了 Remove,在存在重复对象的情况下也必须删除最后一次出现的对象。

如果对于 Stack<T> 没有意义,那么最好避免使用它的相等和相反的表亲。


如果微软能这样做,我会更喜欢它:

  • 没有为ICollection<T>记录Remove,这样做并不明智。考虑到各种结构的内部实现有多么不同,删除某个相等的对象似乎更有意义。强制删除第一个项目看起来像是受到了线性搜索简单结构(如数组)的影响。

  • 对于队列结构,应该有接口。可能是:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接