Подтвердить что ты не робот

Почему Queue (T) и Stack (T) не реализуют ICollection (T)?

Прежде чем я даже спрошу, позвольте мне получить очевидный ответ: интерфейс 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>. Конечно, они реализуют 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 - Проверить (duh).
  • Remove - Это единственное, что Queue<T> и Stack<T> не имеют идеального соответствия.

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

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

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

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

Я извиняюсь за сверхдолговечный вопрос.

4b9b3361

Ответ 1

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

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

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? Есть ответы, но, очевидно, код в этом случае неправильный, хотя он хорошо пахнет.

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

Ответ 2

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

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

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

Ответ 3

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

Удаляет появление первого определенного объекта из 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();
    }