2011-01-23 9 views
15

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

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

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

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

Приносим извинения за сверхтяжелый вопрос.

+4

Еще один пример бедных библиотек сбора МС. Эта проблема могла быть решена, по крайней мере, путем разделения интерфейсов на версии только для чтения и чтения-записи (с версией для чтения и записи, наследующей от версии только для чтения), а также с более мелкозернистыми интерфейсами, фокусируясь только на конкретный аспект коллекции, а не включать нерелевантные свойства и методы. Поэтому ICollection не должен включать в себя Добавить, Очистить или Удалить. Это должно было быть оставлено, например, для IMutableCollection . ICollection может быть реализован более многими классами. – siride

+0

@siride: Ваш взгляд на библиотеки коллекций MS отражает мои. Очень неприятно, что такие вещи, как индексированные свойства, должны быть определены дважды для версий только для чтения и чтения-записи, но это жизнь. – supercat

+0

+1 для инноваций для реализации 'Remove' в одиночку, это было здорово :) И нет, этот супер-длинный q хорошо написан. – nawfal

ответ

5

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

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

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>, помните кейсы для удаления!)

+0

Я думаю, что это отличный пример, который делает рассуждения здесь очень конкретными. Спасибо, что помог мне разобраться с этим. Двигаясь вперед, я буду бросить вызов себе, чтобы придумать примеры, когда реализующие определенный интерфейс «неполно» вызовет весьма неожиданное поведение в таких случаях, я думаю, долго и упорно, прежде чем совершать эти интерфейсы. –

+0

+1 для * рамки для установки примера *. Хорошо сказано. – nawfal

1

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

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

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

+0

@Marc: проблем нет; люди все время смешивают этих двух (я знаю, что у меня есть). Я знаю, что вы имеете в виду перечисление очередей/стеков; кто когда-либо перечисляет очередь, не желая фактически вычеркнуть из нее? Я предполагаю, что часть проблемы заключается в том, что в документации для 'IEnumerator (T)' говорится: «Перечислители могут использоваться для чтения данных в коллекции, но они не могут использоваться для изменения базовой коллекции». Вид отдал себя в угол. –

+0

@ Dan Tao: Решение заключалось бы в том, что IStack и IQueue не реализуют IEnumerable, а вместо этого предоставляют методы DequeueAll или PopAll, которые возвращают IEnumerable. Обратите внимание, что метод PopAll для потокобезопасного IStack может дать разные результаты из-за того, что вручную выталкивает все из стека, поскольку поточноподобный PopAll должен гарантировать, что элемент, перемещенный во время PopAll, будет либо самым верным возвращенным элементом, либо должен не возвращаются, но остаются в стеке; ручная последовательность операций «поп» не имела бы такой гарантии. – supercat

+0

@supercat: Я согласен, конечно, есть прекрасные способы выполнения этого поведения без использования 'IEnumerable ' реализация; что я не соглашусь с тем, что 'Queue ' и 'Stack ' * не должен * реализовывать 'IEnumerable '. Это просто похоже на редкий случай, когда вы хотите перечислить, не удаляя. Лично я использую методы расширения, которые выполняют в основном ту же работу, что и ваши предложения 'DequeueAll' и' PopAll'. –

0

Филип дал хороший ответ (+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(); 
    } 
    

 Смежные вопросы

  • Нет связанных вопросов^_^