Must .NET's IList be finite? Suppose I write a class FibonacciList implementing IList<BigInteger>
- The property Item[n] returns the nth Fibonacci number.
- The property IsReadOnly returns true.
- The methods IndexOf and Contains we can implement easily enough because the Fibonacci sequence is increasing - to test if the number m is Fibonacci, we need only to compute the finite sequence of Fibonacci numbers up to m.
- The method GetEnumerator() doing the right thing
We've now implemented all the methods expected of read-only ILists except Count().
Is this cool, or an abuse of IList?
Fibonacci numbers get impractically big quickly (hence IList<BigInteger>
above) . A bounded infinite sequence might be more sensible, it could implement IList<long>
or IList<double>
.
Addendum II: Fibonacci sequence may have been a bad example, because computing distant values is expensive - to find the nth value one has to compute all earlier values. Thus as Mošmondor said, one might as well make it an IEnumerable and use .ElementAt
. However there exist other sequences where one can compute distant values quickly without computing earlier values. (Surprisingly the digits of pi are such a sequence). These sequences are more 'listy', they truly support random access.
Edit: No-one argues against infinite IEnumerables. How do they handle Count()?
EDIT
Having had a day to reflect on my answer and, in light of @StriplingWarrior's comment. I fear I have to make a reversal. I started trying this out last night and now I wonder what would I really lose by abandoning
IList
?I think it would wiser to implement just
IEnumerable
and, declare a localCount()
method that throws aNotSupportedException
method to prevent the enumerator running until anOutOfMemoryException
occurs. I would still add anIndexOf
andContains
method andItem
indexer property to expose higher performance alternatives like Binet's Formula but, I'd be free to change the signatures of these members to use extended datatypes potentially, evenSystem.Numerics.BigInteger
.If I were implementing multiple series I would declare an
ISeries
interface for these members. Who know's, perhaps somthing like this will eventually be part of the framework.I disagree with what appears to be a consensus view. Whilst
IList
has many members that cannot be implemented for an infinite series it does have anIsReadOnly
member. It seems acceptable, certainly in the case ofReadOnlyCollection<>
, to implement the majority of members with aNotSupportedException
. Following this precedent, I don't see why this should be unacceptable if it is a side effect of some other gain in function.In this specific Fibbonaci series case, there are established algortihms, see here and here, for shortcircuiting the normal cumalitive enumeration approach which I think would yield siginifcant performance benefits. Exposing these benefits through
IList
seems warranted to me.Ideally, .Net would support some other, more appropriate super class of interface, somewhat closer to
IEnumerable<>
but, until that arrives in some future version, this has got to be a sensible approach.I'm working on an implementation of
IList<BigInteger>
to illustrateTo most developers,
IList
andICollection
imply that you have a pre-evaluated, in-memory collection to work with. WithIList
specifically, there is an implicit contract of constant-timeAdd
* and indexing operations. This is whyLinkedList<T>
does not implementIList<T>
. I would consider a FibonacciList to be a violation of this implied contract.Note the following paragraph from a recent MSDN Magazine article discussing the reasons for adding read-only collection interfaces to .NET 4.5:
As others have pointed out, there is also the question of what you would return for
.Count
.It's perfectly fine to use
IEnumerable
orIQueryable
in for such collections of data, because there is an expectation that these types can be lazily evaluated.Regarding Edit 1:
.Count()
is not implemented by theIEnumerable<T>
interface: it is an extension method. As such, developers need to expect that it can take any amount of time, and they need to avoid calling it in cases where they don't actually need to know the number of items. For example, if you just want to know whether anIEnumerable<T>
has any items, it's better to use.Any()
. If you know that there's a maximum number of items you want to deal with, you can use.Take()
. If a collection has more thanint.MaxValue
items in it,.Count()
will encounter an operation overflow. So there are some workarounds that can help to reduce the danger associated with infinite sequences. Obviously if programmers haven't taken these possibilities into account, it can still cause problems, though.Regarding Edit 2: If you're planning to implement your sequence in a way that indexing is constant-time, that addresses my main point pretty handily. Sixlettervariables's answer still holds true, though.
*Obviously there's more to this:
Add
is only expected to work ifIList.IsFixedSize
returnsfalse
. Modification is only possible ifIsReadOnly
returns false, etc.IList
was a poorly-thought-out interface in the first place: a fact which may finally be remedied by the introduction of read-only collection interfaces in .NET 4.5.Update
Having given this some additional thought, I've come to the personal opinion that
IEnumerable<>
s should not be infinite either. In addition to materializing methods like.ToList()
, LINQ has several non-streaming operations like.OrderBy()
which must consume the entireIEnumerable<>
before the first result can be returned. Since so many methods assumeIEnumerable<>
s are safe to traverse in their entirety, it would be a violation of the Liskov Substitution Principle to produce anIEnumerable<>
that is inherently unsafe to traverse indefinitely.If you find that your application often requires segments of the Fibonacci sequence as IEnumerables, I'd suggest creating a method with a signature similar to
Enumerable.Range(int, int)
, which allows the user to define a starting and ending index.If you'd like to embark on a Gee-Whiz project, you could conceivably develop a Fibonacci-based
IQueryable<>
provider, where users could use a limited subset of LINQ query syntax, like so:Since your query provider would have the power to evaluate the
where
clauses as lambda expressions, you could have enough control to implementCount
methods and.GetEnumerator()
in a way as to ensure that the query is restrictive enough to produce a real answer, or throw an exception as soon as the method is called.But this reeks of being clever, and would probably be a really bad idea for any real-life software.
As @CodeInChaos pointed out in the comments, the Item property of IList has signature
We see ILists are indexed by ints, so their length is bounded by Int32.MaxValue . Elements of greater index would be inaccessible. This occurred to me when writing the question, but I left it out, because the problem is fun to think about otherwise.
Summarising what I've seen so far:
You can fulfil 5 out of 6, throwing a
NotSupportedException
onCount()
I would have said this is probably good enough to go for it, however as
servy
has pointed out, the indexer is incredibly inefficient for any non-calculated and cached number.In this case, I would say the only contract that fits your continual stream of calculations is IEnumerable.
The other option you have is to create something that looks a lot like an
IList
but isn't actually.An infinite collection would probably best be implemented as an
IEnumerable<T>
, not anIList<T>
. You could also make use of theyield return
syntax when implementing, like so (ignore overflow issues, etc.):I would imagine that a conforming implementation must be finite, otherwise what would you return for
ICollection<T>.Count
?Another consideration is
CopyTo
, which under its normal overload would never stop in a Fibonacci case.What this means is an appropriate implementation of a Fibonacci Sequence would be simply
IEnumerable<int>
(using a generator pattern). (Ab)use of anIList<T>
would just cause problems.