Although the enumeration interfaces provide a protocol for forward-only iteration
over a collection, they don’t provide a mechanism to determine the size of the collection,
access a member by index, search, or modify the collection. For such functionality,
the .NET Framework defines the ICollection, IList, and IDictionary
interfaces. Each comes in both generic and nongeneric versions; however, the nongeneric
versions exist mostly for legacy.
The inheritance hierarchy for these interfaces was shown in Figure 7-1. The easiest
way to summarize them is as follows:
IEnumerable<T> (and IEnumerable)
Provides minimum functionality (enumeration only)
ICollection<T> (and ICollection)
Provides medium functionality (e.g., the Count property)
IList <T>/IDictionary <K,V> and their nongeneric versions
Provide maximum functionality (including “random” access by index/key)
It’s rare that you’ll need to implement any of these interfaces. In
nearly all cases when you need to write a collection class, you
can instead subclass Collection<T> (see “Customizable Collections
and Proxies” on page 298). LINQ provides yet another
option that covers many scenarios.
The generic and nongeneric versions differ in ways over and above what you might
expect, particularly in the case of ICollection. The reasons for this are mostly historical:
because generics came later, the generic interfaces were developed with the
benefit of hindsight. For this reason, ICollection<T> does not extend ICollection,
IList<T> does not extend IList, and IDictionary<TKey, TValue> does not extend
IDictionary. Of course, a collection class itself is free to implement both versions of
an interface if beneficial (which, often, it is).
Another, subtler reason for IList<T> not extending IList is that
casting to IList<T> would then return an interface with both
Add(T) and Add(object) members. This would effectively defeat
static type safety, because you could call Add with an object of
any type.
There is no consistent rationale in the way the words collection
and list are applied throughout the .NET Framework. For
instance, since IList<T> is a more functional version of
ICollection<T>, you might expect the class List<T> to be correspondingly
more functional than the class Collection<T>.
This is not the case. It’s best to consider the terms collection and
list as broadly synonymous, except when a specific type is
involved.
ICollection<T> and ICollection
ICollection<T> is the standard interface for countable collections of objects. It provides
the ability to determine the size of a collection (Count), determine whether an
item exists in the collection (Contains), copy the collection into an array (ToArray),
and determine whether the collection is read-only (IsReadOnly). For writable collections,
you can also Add, Remove, and Clear items from the collection. And since it
extends IEnumerable<T>, it can also be traversed via the foreach statement:
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
bool Contains (T item);
void CopyTo (T[] array, int arrayIndex);
bool IsReadOnly { get; }
void Add(T item);
bool Remove (T item);
void Clear();
}
The nongeneric ICollection is similar in providing a countable collection,
but doesn’t provide functionality for altering the list or checking for element
membership:
public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo (Array array, int index);
}
The nongeneric interface also defines properties to assist with synchronization
(Chapter 21)—these were dumped in the generic version because thread safety is no
longer considered intrinsic to the collection.
Both interfaces are fairly straightforward to implement. If implementing a
read-only ICollection<T>, the Add, Remove, and Clear methods should throw a
NotSupportedException.
These interfaces are usually implemented in conjunction with either the IList or the
IDictionary interface.
IList<T> and IList
IList<T> is the standard interface for collections indexable by position. In addition
to the functionality inherited from ICollection<T> and IEnumerable<T>, it provides
the ability to read or write an element by position (via an indexer) and insert/remove
by position:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this [int index] { get; set; }
int IndexOf (T item);
void Insert (int index, T item);
void RemoveAt (int index);
}
The IndexOf methods perform a linear search on the list, returning −1 if the specified
item is not found.
The nongeneric version of IList has more members because it inherits less from
ICollection:
public interface IList : ICollection, IEnumerable
{
object this [int index] { get; set }
bool IsFixedSize { get; }
bool IsReadOnly { get; }
int Add (object value);
void Clear();
bool Contains (object value);
int IndexOf (object value);
void Insert (int index, object value);
void Remove (object value);
void RemoveAt (int index);
}
The Add method on the nongeneric IList interface returns an integer—this is the
index of the newly added item. In contrast, the Add method on ICollection<T> has
a void return type.
The general-purpose List<T> class is the quintessential implementation of both
IList<T> and IList. C# arrays also implement both the generic and nongeneric
ILists (although the methods that add or remove elements are hidden via explicit
interface implementation and throw a NotSupportedException if called).