C# Generics: Collection Interfaces

Collection Interfaces

The .NET Framework provides two
sets of standard interfaces for enumerating and comparing
collections: the traditional (nontype-safe) and the new generic
type-safe
collections. This book focuses only on the new, type-safe collection
interfaces as these are far preferable.

You can declare an ICollection of any specific
type by substituting the
actual type (for
example, int or string) for the
generic type in the interface declaration
(<T>).

TIP: C++ programmers note: C# generics are similar in syntax
and usage to C++ templates. However, because the generic types are
expanded to their specific type at runtime, the JIT compiler is able
to share code among different instances, dramatically reducing the
code bloat that you may see when using templates in C++.

Table 9-2. Collection interfaces

Implemented by all collections to provide the
CopyTo() method as well as the
Count, IsSynchronized, and
SyncRoot properties.

IComparer<T>
IComparable<T>

Compares two objects held in a collection so that the collection can
be sorted.

IList<T>

Used by array-indexable collections.

IDictionary<K,V>

Used for key/value-based collections such as
Dictionary.

The IEnumerable<T> Interface

You can support the
foreachstatement in
ListBoxTest by implementing the
IEnumerable<T> interface (see Example 9-11). IEnumerable has only one
method,
GetEnumerator( ),
whose job is to return an implementation of
IEnumerator<T>. The C# language provides
special help in creating the enumerator, using the new keyword
yield.

The program begins in Main( ), creating a new
ListBoxTest object and passing two strings to the
constructor. When the object is created, an array of
Strings is created with enough room for eight
strings. Four more strings are added using the Add
method, and the second string is updated, just as in the previous
example.

The big change in this version of the program is that a
foreach loop is called, retrieving each string
in the listbox. The foreach loop automatically
uses the IEnumerable<T> interface, invoking
GetEnumerator( ).

The GetEnumerator method is declared to return an
IEnumerator of string:

public IEnumerator<string> GetEnumerator( )

The implementation iterates through the array of strings, yielding
each in turn:

foreach ( string s in strings )
{
yield return s;
}

All the bookkeeping for keeping track of which element is next,
resetting the iterator, and so forth, is provided for you by the
framework.

Constraints

There are times when you must ensure
that the elements you add to a generic list meet certain constraints
(e.g., they derive from a given base class, or they implement a
specific interface). In the next example, we implement a simplified
singly linked, sortable list. The list consists of
Nodes, and each Node must be
guaranteed that the types added to it implement
IComparer. You do so with the following statement:

public class Node<T> :
IComparable<Node<T>> where T : IComparable<T>

This defines a generic Node that holds a type,
T. Node of T
implements the IComparable<T> interface,
which means that two Nodes of T
can be compared. The Node class is constrained
(whereT:IComparable<T>) to hold only types that
implement the IComparable interface. Thus, you may
substitute any type for T so long as that type
implements IComparable.

Example 9-12 illustrates the complete implementation,
with analysis to follow.

In this example, you begin by declaring a class that can be placed
into the linked list:

public class Employee : IComparable<Employee>

This declaration indicates that Employee objects
are comparable, and we see that the Employee class
implements the required methods
(CompareTo and Equals). Note
that these methods are type-safe (they know that the parameter passed
to them will be of type Employee). The
LinkedList itself is declared to hold only types
that implement IComparable:

public class LinkedList<T> where T : IComparable<T>

so you are guaranteed to be able to sort the list. The
LinkedList holds an object of type
Node. Node also implements
IComparable and requires that the objects it holds
as data themselves implement IComparable:

public class Node<T> :
IComparable<Node<T>> where T : IComparable<T>

These constraints make it safe and simple to implement the
CompareTo method of Node
because the Node knows it will be comparing other
Nodes whose data is comparable:

public int CompareTo(Node<T> rhs)
{
// this works because of the constraint
return data.CompareTo(rhs.data);
}

Notice that we don't have to test
rhs to see if it implements
IComparable; we've already
constrained Node to hold only data that implements
IComparable.

List<T>

The classic problem with the
Array type is its fixed size. If you don't know in
advance how many objects an array will hold, you run the risk of
declaring either too small an array (and running out of room) or too
large an array (and wasting memory).

Your program might be asking the user for input, or gathering input
from a web site. As it finds objects (strings, books, values, etc.),
you will add them to the array, but you have no idea how many objects
you'll collect in any given session. The classic
fixed-size array is not a good choice, as you can't
predict how large an array you'll need.

The List class is an array whose size is
dynamically increased as required. Lists provide a
number of useful methods and properties for their
manipulation. Some of the most important are shown in Table 9-3.

Table 9-3. List methods and properties

Method or property

Purpose

Capacity

Property to get or set the number of elements the
List can contain. This value is increased
automatically if count exceeds capacity. You might set this value to
reduce the number of reallocations, and you may call
Trim() to reduce this value to
the actual Count.

Count

Property to get the number of elements currently in the array.

Item( )

Gets or sets the element at the specified index. This is the indexer
for the List class.
[4]

Add( )

Public method to add an object to the List.

AddRange( )

Public method that adds the elements of an
ICollection to the end of the
List.

BinarySearch( )

Overloaded public method that uses a binary search to locate a
specific element in a sorted List.

Clear( )

Removes all elements from the List.

Contains( )

Determines if an element is in the List.

CopyTo( )

Overloaded public method that copies a List to a
one-dimensional array.

Exists( )

Determines if an element is in the List.

Find( )

Returns the first occurrence of the element in the
List.

FindAll( )

Returns all the specified elements in the List.

GetEnumerator( )

Overloaded public method that returns an enumerator to iterate
through a List.

GetRange( )

Copies a range of elements to a new List.

IndexOf( )

Overloaded public method that returns the index of the first
occurrence of a value.

Insert( )

Inserts an element into the List.

InsertRange( )

Inserts the elements of a collection into the List.

LastIndexOf( )

Overloaded public method that returns the index of the last
occurrence of a value in the List.

Remove( )

Removes the first occurrence of a specific object.

RemoveAt( )

Removes the element at the specified index.

RemoveRange( )

Removes a range of elements.

Reverse( )

Reverses the order of elements in the List.

Sort( )

Sorts the List.

ToArray( )

Copies the elements of the List to a new array.

TrimToSize( )

Sets the capacity of the actual number of elements in the
List.

When you create a List, you don't
define how many objects it will contain. Add to the
List using the Add() method, and the list takes care of its own
internal bookkeeping, as illustrated in Example 9-13.

With an Array class, you define how many objects
the array will hold. If you try to add more than that, the
Array class will throw an exception. With a
List, you don't declare how many
objects the List will hold. The
List has a property,
Capacity,
which is the number of elements the List is
capable of storing:

public int Capacity { get; set; }

The default capacity is 16. When you add the 17th element, the
capacity is automatically doubled to 32. If you change the
for loop to:

You can manually set the capacity to any number equal to or greater
than the count. If you set it to a number less than the count, the
program will throw an exception of type
ArgumentOutOfRangeException.

Implementing IComparable

Like all collections, the
List implements the Sort( )
method, which allows you to sort any objects that implement
IComparable. In the next example,
you'll modify the Employee object
to implement IComparable:

public class Employee : IComparable<Employee>

To implement the IComparable<Employee>
interface, the Employee object must provide a
CompareTo() method:

The CompareTo( ) method takes an
Employee as a parameter. We know this is an
Employee because this is a type-safe collection.
The current Employee object must compare itself to
the Employee passed in as a parameter and return
-1 if it is smaller than the parameter,
1 if it is greater than the parameter, and
0 if it is equal to the parameter. It is up to
Employee to determine what
smallerthan,
greaterthan, and
equalto mean. In this example,
you delegate the comparison to the empId member.
The empId member is an int and
uses the default CompareTo( ) method for integer
types, which will do an integer comparison of the two values.

TIP:
The System.Int32 class implements
IComparable<Int32>, so you may delegate the
comparison responsibility to integers.

You are now ready to sort
the array list of employees, empList. To see if
the sort is working, you'll need to add integers and
Employee instances to their respective arrays with
random values. To create the random values, you'll
instantiate an object of class Random; to generate
the random values, you'll call the
Next( ) method on the Random
object, which returns a pseudorandom number. The
Next( ) method is overloaded; one version allows
you to pass in an integer that represents the largest random number
you want. In this case, you'll pass in the value
10 to generate a random number between
0 and 10:

Random r = new Random();
r.Next(10);

Example 9-14 creates an integer array and an
Employee array, populates them both with random
numbers, and prints their values. It then sorts both arrays and
prints the new values.

The output shows that the integer array and
Employee array were generated with random numbers.
When sorted, the display shows the values have been ordered
properly.

Implementing IComparer

When you call Sort( ) on
the List, the default implementation of IComparer
is called, which uses QuickSort to call the
IComparable implementation of
CompareTo() on each element in the
List.

You are free to create your own implementation of
IComparer, which you might want to do if you need
control over how the sort ordering is defined. In the next example,
you will add a second field to Employee,
yearsOfSvc. You want to be able to sort the
Employee objects in the List on
either field, empID or
yearsOfSvc.

To accomplish this, create a custom implementation of
IComparer, which you pass to the
Sort() method of the List. This
IComparer class,
EmployeeComparer, knows about
Employee objects and knows how to sort them.

EmployeeComparer has the
WhichComparison property, of type
Employee. EmployeeComparer.ComparisonType:

The complete source for this example is shown in Example 9-15. The integer array has been removed to
simplify the example, and the output of the
employee's ToString( ) method has
been enhanced to enable you to see the effects of the sort.

The first block of output shows the
Employee objects as they are added to the
List. The employee ID values and the years of
service are in random order. The second block shows the results of
sorting by the employee ID, and the third block shows the results of
sorting by years of service.

TIP:
If you are
creating
your own collection, as in Example 9-11, and wish to
implement IComparer, you may need to ensure that
all the types placed in the list implement
IComparer (so that they may be sorted), by using
constraints as described earlier.

Queues

A queue
represents a first-in, first-out (FIFO) collection. The classic
analogy is to a line (or queue if you are British) at a ticket
window. The first person in line ought to be the first person to come
off the line to buy a ticket.

A queue is a good collection to use when you are managing a limited
resource. For example, you might want to send messages to a resource
that can handle only one message at a time. You would then create a
message queue so that you can say to your clients:
"Your message is important to us. Messages are
handled in the order in which they are received."

The Queue class has
a number of member methods and properties, as shown in Table 9-4.

Table 9-4. Queue methods and properties

Method or property

Purpose

Count

Public property that gets the number of elements in the
Queue.

Clear( )

Removes all objects from the Queue.

Contains( )

Determines if an element is in the Queue.

CopyTo( )

Copies the Queue elements to an existing
one-dimensional array.

Dequeue( )

Removes and returns the object at the beginning of the
Queue.

Enqueue( )

Adds an object to the end of the Queue.

GetEnumerator( )

Returns an enumerator for the Queue.

Peek( )

Returns the object at the beginning of the Queue
without removing it.

ToArray( )

Copies the elements to a new array.

Add
elements to your queue with the Enqueue command
and take them off the queue with Dequeue or by
using an enumerator. Example 9-16 illustrates.

In this example the List is replaced by a
Queue. I've dispensed with the
Employee class to save room, but of course you can
Enqueue user-defined objects as well.

The output shows that queuing objects adds them to the
Queue, and calls to Dequeue
return the object and also remove them from the
Queue. The Queue class also
provides a Peek() method that allows you to see,
but not remove, the first element.

Because the Queue class is enumerable, you can
pass it to the
PrintValues method, which is provided as an
IEnumerable interface. The conversion is
implicit. In the PrintValues method you call
GetEnumerator, which you will remember is the
single method of all IEnumerable classes. This
returns an
IEnumerator, which you then use to enumerate all
the objects in the collection.

Stacks

A stack
is a last-in, first-out (LIFO) collection, like a stack of dishes at
a buffet table, or a stack of coins on your desk. A dish added on top
is the first dish you take off the stack.

The principal methods for adding to and removing from a stack are
Push() and Pop();
Stack also offers a Peek()
method, very much like Queue. The significant
methods and properties for
Stack are
shown in Table 9-5.

Table 9-5. Stack methods and properties

Method or property

Purpose

Count

Public property that gets the number of elements in the
Stack.

Clear( )

Removes all objects from the Stack.

Clone( )

Creates a shallow copy.

Contains( )

Determines if an element is in the Stack.

CopyTo( )

Copies the Stack elements to an existing
one-dimensional array.

GetEnumerator( )

Returns an enumerator for the Stack.

Peek( )

Returns the object at the top of the Stack without
removing it.

Pop( )

Removes and returns the object at the top of the
Stack.

Push( )

Inserts an object at the top of the Stack.

ToArray( )

Copies the elements to a new array.

The List, Queue, and
Stack types contain overloaded
CopyTo( ) and ToArray( ) methods
for copying their elements to an array. In the case of a
Stack, the CopyTo( ) method will
copy its elements to an existing one-dimensional array, overwriting
the contents of the array beginning at the index you specify. The
ToArray( ) method returns a new array with the
contents
of the stack's elements. Example 9-17 illustrates.

The
output reflects that the items pushed onto the stack were popped in
reverse order.

The effect of CopyTo() can be
seen by examining the target array before and after calling
CopyTo( ). The array elements are overwritten
beginning with the index specified (6).

Dictionaries

A dictionary
is a collection that associates a key to a value. A
language dictionary, such as Webster's, associates a
word (the key) with its definition (the value).

To see the value of dictionaries, start by imagining that you want to
keep a list of the state capitals. One approach might be to put them
in an array:

string[] stateCapitals = new string[50];

The stateCapitals array will hold 50 state
capitals. Each capital is accessed as an offset into the array. For
example, to access the capital for Arkansas, you need to know that
Arkansas is the fourth state in alphabetical order:

string capitalOfArkansas = stateCapitals[3];

It is inconvenient, however, to access state capitals using array
notation. After all, if I need the capital for Massachusetts, there
is no easy way for me to determine that Massachusetts is the 21st
state alphabetically.

It would be far more convenient to store the capital with the state
name. A dictionary allows you to store a value (in this case, the
capital) with a key (in this case, the name of the state).

A .NET Framework dictionary can associate any kind of key (string,
integer, object, etc.) with any kind of value (string, integer,
object, etc.). Typically, of course, the key is fairly short, the
value fairly complex.

The most important attributes of a good dictionary are that it is
easy to add and quick to retrieve values (see Table 9-6).

Table 9-6. Dictionary methods and properties

Method or property

Purpose

Count

Public property that gets the number of elements in the
Dictionary.

Item( )

The indexer for the Dictionary.

Keys

Public property that gets a collection containing the keys in the
Dictionary (see also Values).

Values

Public property that gets a collection containing the values in the
Dictionary (see also Keys).

Add( )

Adds an entry with a specified Key and
Value.

Clear( )

Removes all objects from the Dictionary.

ContainsKey()

Determines whether the Dictionary has a specified
key.

ContainsValue( )

Determines whether the Dictionary has a specified
value.

GetEnumerator( )

Returns an enumerator for the Dictionary.

GetObjectData()

Implements ISerializable and returns the data
needed to serialize the Dictionary.

Remove( )

Removes the entry with the specified Key.

The key in a Dictionary can be a primitive type,
or it can be an instance of a user-defined type (an object). Objects
used as keys for a Dictionary must implement
GetHashCode() as well as
Equals. In most cases, you can simply use
the inherited implementation from Object.

IDictionary<K,V>

Dictionaries implement the IDictionary<K,V>
interface (where K is the key type and
V is the value type).
IDictionary provides a public property
Item. The Item property
retrieves a value with the specified key. In C#, the declaration for
the Item property is:

V[K key]
{get; set;}

The Item property is implemented in C# with the
index operator ([]). Thus, you access items in any
Dictionary object using the offset syntax, as you
would with an array.

Example 9-18 demonstrates adding items to a
Dictionary and then retrieving them with the
Item property.

Example 9-18 begins by instantiating a new
Dictionary. The type of the key and of the value
is declared to be string.

Add four key/value pairs. In this example, the Social Security number
is tied to the person's full name. (Note that the
Social Security numbers here are intentionally bogus.)

Once the items are added, you access a specific entry in the
dictionary using the Social Security number as
key.

WARNING
If you use a reference type as a key, and the type is
mutable (strings are immutable), you must not change the value of the
key object once you are using it in a dictionary.

If, for example, you use the Employee object as a
key, changing the employee ID creates problems if that property is
used by the Equals or
GetHashCode methods because the dictionary
consults these methods.

Footnotes

[3] For backward compatibility,
C# also provides nongeneric interfaces (e.g.,
ICollection, IEnumerator), but
they aren't considered here because they are
obsolescent.

[4] The idiom in
the FCL is to provide an
Item element for collection
classes which is implemented as an indexer in C#.

Jesse Liberty
is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.