Почему Queue (T) и Stack (T) не реализуют ICollection (T)? - PullRequest
15 голосов
/ 23 января 2011

Прежде чем я даже задам вопрос, позвольте мне получить очевидный ответ: Интерфейс ICollection<T> включает метод Remove для удаления произвольного элемента, который Queue<T> и Stack<T> не может действительно поддерживает (так как они могут удалить только "конечные" элементы).

Хорошо, я это понимаю. На самом деле, мой вопрос касается не только типов коллекций Queue<T> или Stack<T>; скорее речь идет о проектном решении не реализовывать ICollection<T> для любого универсального типа, который по сути является набором 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>. Они, конечно, do реализуют 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<T> с явной реализацией для членов, которые не имеют идеального соответствия?

Я имею в виду, посмотрите, что ICollection<T> предлагает:

  • Count - Queue<T> и Stack<T> оба имеют это.
  • IsReadOnly - Queue<T> и Stack<T> легко может иметь это.
  • Add - Queue<T> может реализовать это явно (с Enqueue), как и Stack<T>Push).
  • Clear - Проверка.
  • Contains - Проверка.
  • CopyTo - Проверка.
  • GetEnumerator - Проверить (дух).
  • Remove - Это единственный, для которого Queue<T> и Stack<T> не имеют идеального соответствия.

А вот настоящий кикер: ICollection<T>.Remove возвращает bool; поэтому явная реализация для Queue<T> может полностью (например) проверить, является ли удаляемый элемент на самом деле элементом head (с использованием Peek), и, если это так, вызвать Dequeue и вернуть true, в противном случае вернуть false. Stack<T> можно легко получить аналогичную реализацию с Peek и Pop.

Хорошо, теперь, когда я написал около тысячи слов о том, почему Я думаю, что это возможно, я задаю очевидный вопрос: почему не дизайнеры Queue<T> и Stack<T> реализуют этот интерфейс? То есть, какие факторы проектирования (которые я, вероятно, не рассматриваю) привели к решению, что это будет неправильный выбор? Почему вместо этого был реализован ICollection? 1105 *

Мне интересно, есть ли при разработке моих собственных типов какие-либо руководящие принципы, которые я должен учитывать в отношении реализации интерфейса, которые я мог бы не заметить, задавая этот вопрос. Например, считается ли просто плохой практикой явная реализация интерфейсов, которые не полностью поддерживаются в целом (если это так, то это может противоречить, например, List<T> реализации IList)? Существует ли концептуальное разъединение между концепцией очереди / стека и тем, что ICollection<T> должно представлять?

По сути, я чувствую, что должна быть довольно веская причина Queue<T> (например) не реализует ICollection<T>, и я не хочу просто слепо идти вперед, разрабатывая свою собственную несоответствующим образом вводит и реализует интерфейсы, не будучи информированным и полностью продумывая, что я делаю.

Я прошу прощения за сверхдлинный вопрос.

Ответы [ 3 ]

5 голосов
/ 24 января 2011

Я не могу дать ответ «что было на самом деле» - возможно, один из дизайнеров даст нам реальное мышление, и я могу удалить это.

Однако, поставив себя на мысль: «что, если кто-то придет ко мне, чтобы принять это решение», я могу придумать ответ ... позвольте мне проиллюстрировать этот код:

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

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

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

(Конечно, любой может создать плохую реализацию ICollection<T>, но мы ожидаем, что фреймворк подаст пример). Давайте предположим, что стек / очередь реализованы так, как вы указали в вопросе. Так правильный ли код приведен выше, или он содержит ошибки в крайнем случае, потому что ICollection<T>.Remove() должен быть проверен? Если valueA ДОЛЖЕН быть удален, как я могу это исправить, чтобы работать как со стеком , так и Очередью? Есть ответы, но очевидно, что приведенный выше код неверен в этом случае - даже если он пахнет разумно.

Так что обе интерпретации верны, но я согласен с принятым здесь решением - если бы у меня был код, указанный выше, и я знал, что мне может быть передана очередь или стек, которые я мог бы спроектировать, но это было бы легко баг ямы, в которую можно попасть (везде, где вы видите ICollection<T>, помните крайние случаи для удаления!)

1 голос
/ 23 января 2011

В конечном счете, возможно, они просто не идеально подходят; если вы хотите список или коллекцию - используйте List<T> или Collection<T>; p

Re Add - существует неявное предположение, что Add добавляет к end коллекции, что неверно для стека (хотя это для очереди). В некотором смысле меня действительно смущает то, что перечислитель для стека / очереди не удаляет / удаляет, поскольку я в основном ожидаю, что элементы в очереди / стеке будут выбираться один раз (и только один раз).

Может быть, также (опять же, используя мой перечислитель в качестве примера), люди просто не могли прийти к единому мнению, как он должен вести себя в некоторых из этих сценариев, и при отсутствии полного согласия, просто не их реализация была лучшим выбором.

0 голосов
/ 08 июня 2014

Филипп дал хороший ответ (+1).Есть еще одно концептуальное обещание, что Remove сломается для Stack<T>.ICollection<T>.Remove задокументировано как:

Удаляет first вхождение определенного объекта из коллекции ICollection.

Stack<T> - это LIFO, даже если Remove был реализован, ему придется удалить последнее вхождение в случае дублирования объектов.

Если это не имеет смысла для Stack<T>, его лучше избегать для его равного и противоположного кузена.


Мне бы понравилось, если бы MS:

  • не документировал Remove для ICollection<T> таким образом.Удаление где-либо равного объекта имело бы больше смысла, учитывая, насколько сильно различаются внутренние реализации различных структур.Принудительное удаление первого элемента выглядит под влиянием линейного поиска простых структур, таких как массивы.

  • имели интерфейсы для структур очереди.Может быть:

    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();
    }
    
...