Должен ли Илист быть конечным?
Должен .NET IList быть конечным? Предположим, что я пишу класс FibonacciList, реализующий IList<BigInteger>
- Свойство Item [n] возвращает n-й номер Фибоначчи.
- Свойство IsReadOnly возвращает true.
- Методы IndexOf и Contains мы можем реализовать достаточно легко, потому что последовательность Фибоначчи возрастает - чтобы проверить, является ли число m Фибоначчи, нам нужно только вычислить конечную последовательность чисел Фибоначчи с точностью до m.
- Метод GetEnumerator() делает правильную вещь
Теперь мы реализовали все методы, ожидаемые от ILIS только для чтения, кроме Count().
Это круто или злоупотребление IList?
Числа Фибоначчи быстро становятся непрактично большими (следовательно, IList<BigInteger>
выше). Ограниченная бесконечная последовательность может быть более разумной, она может реализовать IList<long>
или IList<double>
.
Добавление II: последовательность Фибоначчи, возможно, была плохим примером, поскольку вычисление отдаленных значений дорого - для нахождения n-го значения нужно вычислить все более ранние значения. Таким образом, как сказал Мошмондор, можно сделать его IEnumerable и использовать .ElementAt
. Однако существуют другие последовательности, где можно быстро вычислить отдаленные значения без вычисления более ранних значений. (Удивительно, что цифры pi являются такой последовательностью). Эти последовательности более "перепутаны", они действительно поддерживают произвольный доступ.
Изменить: Никто не возражает против бесконечных IEnumerables. Как они обрабатывают Count()?
Ответы
Ответ 1
Для большинства разработчиков IList
и ICollection
подразумевается, что у вас есть предварительно оцененная коллекция в памяти, с которой вы работаете. С помощью IList
в частности, подразумевается неявный контракт операций с постоянным временем Add
* и индексирования. Вот почему LinkedList<T>
не реализует IList<T>
. Я считаю FibonacciList нарушением этого подразумеваемого контракта.
Обратите внимание на следующий абзац из недавней статьи журнала MSDN, в котором обсуждаются причины добавления интерфейсов коллекции только для чтения к .NET 4.5:
IEnumerable<T>
достаточно для большинства сценариев, связанных с наборами типов, но иногда вам требуется больше мощности, чем обеспечивает:
- Материализация:
IEnumerable<T>
не позволяет вам определить, доступна ли коллекция ( "материализована" ) или ее вычисляется каждый раз, когда вы перебираете ее (например, если она представляет запрос LINQ). Когда алгоритм требует множественных итераций по коллекции, это может привести к ухудшению производительности, если вычисление последовательности является дорогостоящим; он также может вызывать тонкие ошибки из-за несоответствия идентичности, когда объекты снова генерируются при последующих проходах.
Как указывали другие, существует также вопрос о том, что вы вернете для .Count
.
Совершенно нормально использовать IEnumerable
или IQueryable
для таких коллекций данных, потому что есть ожидание того, что эти типы можно лениво оценить.
Относительно Edit 1: .Count()
не реализуется интерфейсом IEnumerable<T>
: это метод расширения. Таким образом, разработчики должны ожидать, что это может занять какое-то время, и им нужно избегать вызова в тех случаях, когда им действительно не нужно знать количество элементов. Например, если вы просто хотите знать, есть ли у IEnumerable<T>
какие-либо элементы, лучше использовать .Any()
. Если вы знаете, что существует максимальное количество элементов, с которыми вы хотите иметь дело, вы можете использовать .Take()
. Если в коллекции содержится более чем int.MaxValue
элементов, .Count()
встретит переполнение операции. Таким образом, есть некоторые обходные пути, которые могут помочь уменьшить опасность, связанную с бесконечными последовательностями. Очевидно, что если программисты не учли эти возможности, это все равно может вызвать проблемы.
Относительно Редактировать 2: Если вы планируете реализовать свою последовательность таким образом, чтобы индексирование было постоянным, это очень удобно для моего основного пункта. Однако ответ Sixlettervariables остается верным.
* Очевидно, что более того: Add
ожидается только, если IList.IsFixedSize
возвращает false
. Модификация возможна только в том случае, если IsReadOnly
возвращает false и т.д. IList
был, во-первых, плохо продуманным интерфейсом: факт, который может быть окончательно устранен путем внедрения интерфейсов коллекции только для чтения в .NET 4.5.
Update
Придав этому дополнительную мысль, я пришел к личному мнению, что IEnumerable<>
также не должен быть бесконечным. Помимо методов материализации, таких как .ToList()
, LINQ имеет несколько операций без потоковой передачи, таких как .OrderBy()
, которые должны потреблять весь IEnumerable<>
до того, как будет возвращен первый результат. Поскольку многие методы предполагают, что IEnumerable<>
безопасны для полного прохождения, было бы нарушением Принципа замещения Лискова, чтобы создать IEnumerable<>
, который по своей природе небезопасен для бесконечного прохождения.
Если вы обнаружите, что вашему приложению часто требуются сегменты последовательности Фибоначчи как IEnumerables, я бы предложил создать метод с подписями, похожим на Enumerable.Range(int, int)
, который позволяет пользователю определять начальный и конечный индексы.
Если вы хотите начать проект Gee-Whiz, вы могли бы разработать провайдера IQueryable<>
на базе Фибоначчи, где пользователи могли бы использовать ограниченное подмножество синтаксиса запроса LINQ, например:
// LINQ to Fibonacci!
var fibQuery = from n in Fibonacci.Numbers // (returns an IQueryable<>)
where n.Index > 5 && n.Value < 20000
select n.Value;
var fibCount = fibQuery.Count();
var fibList = fibQuery.ToList();
Так как поставщик запросов будет иметь возможность оценивать предложения where
в виде лямбда-выражений, у вас может быть достаточно управления для реализации методов Count
и .GetEnumerator()
таким образом, чтобы гарантировать, что запрос является достаточно ограничительным для выдавать реальный ответ или вызывать исключение, как только вызывается метод.
Но это пахнет умением и, вероятно, будет действительно плохой идеей для любого реального программного обеспечения.
Ответ 2
Я бы предположил, что соответствующая реализация должна быть конечной, иначе что бы вы вернули для ICollection<T>.Count
?
/// <summary>
/// Gets the number of elements contained in the <see cref="ICollection{T}" />.
/// </summary>
int Count { get; }
Еще одно соображение - CopyTo
, которое при его нормальной перегрузке никогда не останавливалось бы в случае Фибоначчи.
Это означает, что соответствующая реализация последовательности Фибоначчи будет просто IEnumerable<int>
(с использованием шаблона генератора). (Ab) использование IList<T>
может вызвать проблемы.
Ответ 3
В вашем случае я предпочитаю "нарушать" IEnumerable
и иметь свой путь с yield return
.
:)
Ответ 4
Бесконечная коллекция, вероятно, лучше всего будет реализована как IEnumerable<T>
, а не IList<T>
. Вы также можете использовать синтаксис yield return
при реализации, например (игнорировать проблемы с переполнением и т.д.):
public IEnumerable<long> Fib()
{
yield return 1;
yield return 1;
long l1 = 1;
long l2 = 1;
while (true)
{
long t = l1;
l1 = l2;
l2 = t + l1;
yield return l2;
}
}
Ответ 5
ИЗМЕНИТЬ
У меня был день, чтобы задуматься над моим ответом и в свете комментария @StriplingWarrior. Боюсь, я должен сделать разворот. Я начал пробовать это вчера вечером, и теперь я задаюсь вопросом, что бы я потерял, оставив IList
?
Я думаю, что было бы разумнее реализовать только IEnumerable
и объявить локальный метод Count()
, который выдает метод NotSupportedException
, чтобы предотвратить переключение счетчика до тех пор, пока не произойдет OutOfMemoryException
. Я бы по-прежнему добавлял метод IndexOf
и Contains
и свойство Item
indexer, чтобы выявлять более высокие альтернативы производительности, такие как Binet Formula, но я мог бы свободно изменять подписи этих членов, чтобы использовать расширенные типы данных, даже System.Numerics.BigInteger
.
Если бы я выполнял несколько рядов, я бы объявил интерфейс ISeries
для этих членов. Кто знает, возможно, что-то подобное в конечном итоге станет частью рамки.
Я не согласен с тем, что представляется консенсусом. Хотя IList
имеет много членов, которые не могут быть реализованы для бесконечной серии, у него есть член IsReadOnly
. Кажется приемлемым, конечно, в случае ReadOnlyCollection<>
, реализовать большинство членов с помощью NotSupportedException
. После этого прецедента я не понимаю, почему это должно быть неприемлемым, если это побочный эффект какого-либо другого усиления в функции.
В этом конкретном случае серии Fibbonaci установлены algortihms, см. здесь и здесь, для короткого замыкания нормального метода кумулятивного подсчета, который, как я думаю, принесет сигнифицированные преимущества производительности. Предоставление этих преимуществ через IList
представляется мне оправданным.
В идеале .Net будет поддерживать какой-то другой, более подходящий суперкласс интерфейса, несколько ближе к IEnumerable<>
, но пока это не придет в какую-то будущую версию, это должно быть разумным подходом.
Я работаю над реализацией IList<BigInteger>
, чтобы проиллюстрировать
Ответ 6
Как отметил @CodeInChaos в комментариях, свойство Item IList имеет подпись
T this[ int index ] { get; set; }
Мы видим, что ILists индексируются ints, поэтому их длина ограничена Int32.MaxValue. Элементы большего индекса были бы недоступны. Это пришло мне в голову при написании вопроса, но я его оставил, потому что проблема в том, чтобы думать об этом иначе.
Ответ 7
Подводя итог тому, что я видел до сих пор:
Вы можете выполнить 5 из 6, выбрасывая NotSupportedException
на Count()
Я бы сказал, что это, вероятно, достаточно хорошо, чтобы пойти на это, однако, как отмечал servy
, индексщик невероятно неэффективен для любого не рассчитанного и кэшированного числа.
В этом случае я бы сказал, что единственный контракт, который соответствует вашему постоянному потоку вычислений, - IEnumerable.
Другой вариант, который у вас есть, - создать что-то похожее на IList
, но на самом деле это не так.