Collections and Generics - Advanced C# Programming - C# 6.0 and the .NET 4.6 Framework (2015)

C# 6.0 and the .NET 4.6 Framework (2015)

PART IV

image

Advanced C# Programming

CHAPTER 9

image

Collections and Generics

Any application you create with the .NET platform will need to contend with the issue of maintaining and manipulating a set of data points in memory. These data points can come from any variety of locations including a relational database, a local text file, an XML document, a web service call, or perhaps via user-provided input.

When the .NET platform was first released, programmers frequently used the classes of the System.Collections namespace to store and interact with bits of data used within an application. In .NET 2.0, the C# programming language was enhanced to support a feature termedgenerics; and with this change, a brand new namespace was introduced in the base class libraries: System.Collections.Generic.

This chapter will provide you with an overview of the various collection (generic and nongeneric) namespaces and types found within the .NET base class libraries. As you will see, generic containers are often favored over their nongeneric counterparts because they typically provide greater type safety and performance benefits. After you’ve learned how to create and manipulate the generic items found in the framework, the remainder of this chapter will examine how to build your own generic methods and generic types. As you do this, you will learn about the role ofconstraints (and the corresponding C# where keyword), which allow you to build extremely type-safe classes.

The Motivation for Collection Classes

The most primitive container you could use to hold application data is undoubtedly the array. As you saw in Chapter 4, C# arrays allow you to define a set of identically typed items (including an array of System.Objects, which essentially represents an array of any type of data) of a fixed upper limit. Also recall from Chapter 4 that all C# array variables gather a good deal of functionality from the System.Array class. By way of a quick review, consider the following Main() method, which creates an array of textual data and manipulates its contents in various ways:

static void Main(string[] args)
{
// Make an array of string data.
string[] strArray = {"First", "Second", "Third" };

// Show number of items in array using Length property.
Console.WriteLine("This array has {0} items.", strArray.Length);
Console.WriteLine();

// Display contents using enumerator.
foreach (string s in strArray)
{
Console.WriteLine("Array Entry: {0}", s);
}
Console.WriteLine();

// Reverse the array and print again.
Array.Reverse(strArray);
foreach (string s in strArray)
{
Console.WriteLine("Array Entry: {0}", s);
}

Console.ReadLine();
}

While basic arrays can be useful to manage small amounts of fixed-size data, there are many other times where you require a more flexible data structure, such as a dynamically growing and shrinking container or a container that can hold objects that meet only a specific criteria (e.g., only objects deriving from a specific base class or only objects implementing a particular interface). When you make use of a simple array, always remember they are “fixed size.” If you make an array of three items, you get only three items; therefore, the following code would result in a runtime exception (an IndexOutOfRangeException, to be exact):

static void Main(string[] args)
{
// Make an array of string data.
string[] strArray = { "First", "Second", "Third" };

// Try to add a new item at the end?? Runtime error!
strArray[3] = "new item?";
...
}

Image Note It is actually possible to change the size of an array using the generic Resize()<T> method. However, this will result in a copy of the data into a new array object and could be inefficient.

To help overcome the limitations of a simple array, the .NET base class libraries ship with a number of namespaces containing collection classes. Unlike a simple C# array, collection classes are built to dynamically resize themselves on the fly as you insert or remove items. Moreover, many of the collection classes offer increased type safety and are highly optimized to process the contained data in a memory-efficient manner. As you read over this chapter, you will quickly notice that a collection class can belong to one of two broad categories.

· Nongeneric collections (primarily found in the System.Collections namespace)

· Generic collections (primarily found in the System.Collections.Generic namespace)

Nongeneric collections are typically designed to operate on System.Object types and are, therefore, loosely typed containers (however, some nongeneric collections do operate only on a specific type of data, such as string objects). In contrast, generic collections are much more type safe, given that you must specify the “type of type” they contain upon creation. As you will see, the telltale sign of any generic item is the “type parameter” marked with angled brackets (for example, List<T>). You will examine the details of generics (including the many benefits they provide) a bit later in this chapter. For now, let’s examine some of the key nongeneric collection types in the System.Collections and System.Collections.Specialized namespaces.

The System.Collections Namespace

When the .NET platform was first released, programmers frequently used the nongeneric collection classes found within the System.Collections namespace, which contains a set of classes used to manage and organize large amounts of in-memory data. Table 9-1 documents some of the more commonly used collection classes of this namespace and the core interfaces they implement.

Table 9-1. Useful Types of System.Collections

System.Collections Class

Meaning in Life

Key Implemented Interfaces

ArrayList

Represents a dynamically sized collection of objects listed in sequential order

IList, ICollection, IEnumerable, and ICloneable

BitArray

Manages a compact array of bit values, which are represented as Booleans, where true indicates that the bit is on (1) and false indicates the bit is off (0)

ICollection, IEnumerable, and ICloneable

Hashtable

Represents a collection of key-value pairs that are organized based on the hash code of the key

IDictionary, ICollection, IEnumerable, and ICloneable

Queue

Represents a standard first-in, first-out (FIFO) collection of objects

ICollection, IEnumerable, and ICloneable

SortedList

Represents a collection of key-value pairs that are sorted by the keys and are accessible by key and by index

IDictionary, ICollection, IEnumerable, and ICloneable

Stack

A last-in, first-out (LIFO) stack providing push and pop (and peek) functionality

ICollection, IEnumerable, and ICloneable

The interfaces implemented by these collection classes provide huge insights into their overall functionality. Table 9-2 documents the overall nature of these key interfaces, some of which you worked with firsthand in Chapter 8.

Table 9-2. Key Interfaces Supported by Classes of System.Collections

System.Collections Interface

Meaning in Life

ICollection

Defines general characteristics (e.g., size, enumeration, and thread safety) for all nongeneric collection types

ICloneable

Allows the implementing object to return a copy of itself to the caller

IDictionary

Allows a nongeneric collection object to represent its contents using key-value pairs

IEnumerable

Returns an object implementing the IEnumerator interface (see next table entry)

IEnumerator

Enables foreach style iteration of collection items

IList

Provides behavior to add, remove, and index items in a sequential list of objects

An Illustrative Example: Working with the ArrayList

Based on your experience, you might have some firsthand experience using (or implementing) some of these classic data structures such as stacks, queues, and lists. If this is not the case, I’ll provide some further details on their differences when you examine their generic counterparts a bit later in this chapter. Until then, here is a Main() method making use of an ArrayList object. Notice that you can add (or remove) items on the fly and the container automatically resizes itself accordingly.

// You must import System.Collections to access the ArrayList.
static void Main(string[] args)
{
ArrayList strArray = new ArrayList();
strArray.AddRange(new string[] { "First", "Second", "Third" });

// Show number of items in ArrayList.
Console.WriteLine("This collection has {0} items.", strArray.Count);
Console.WriteLine();

// Add a new item and display current count.
strArray.Add("Fourth!");
Console.WriteLine("This collection has {0} items.", strArray.Count);

// Display contents.
foreach (string s in strArray)
{
Console.WriteLine("Entry: {0}", s);
}
Console.WriteLine();
}

As you would guess, the ArrayList class has many useful members beyond the Count property and AddRange() and Add() methods, so be sure you consult the .NET Framework documentation for full details. On a related note, the other classes of System.Collections(Stack, Queue, and so on) are also fully documented in the .NET help system.

However, it is important to point out that a majority of your .NET projects will most likely not make use of the collection classes in the System.Collections namespace! To be sure, these days it is far more common to make use of the generic counterpart classes found in theSystem.Collections.Generic namespace. Given this point, I won’t comment on (or provide code examples for) the remaining nongeneric classes found in System.Collections.

A Survey of System.Collections.Specialized Namespace

System.Collections is not the only .NET namespace that contains nongeneric collection classes. The System.Collections.Specialized namespace defines a number of (pardon the redundancy) specialized collection types. Table 9-3 documents some of the more useful types in this particular collection-centric namespace, all of which are nongeneric.

Table 9-3. Useful Classes of System.Collections.Specialized

System.Collections.Specialized Type

Meaning in Life

HybridDictionary

This class implements IDictionary by using a ListDictionary while the collection is small and then switching to a Hashtable when the collection gets large.

ListDictionary

This class is useful when you need to manage a small number of items (ten or so) that can change over time. This class makes use of a singly linked list to maintain its data.

StringCollection

This class provides an optimal way to manage large collections of string data.

BitVector32

This class provides a simple structure that stores Boolean values and small integers in 32 bits of memory.

Beyond these concrete class types, this namespace also contains many additional interfaces and abstract base classes that you can use as a starting point for creating custom collection classes. While these “specialized” types might be just what your projects require in some situations, I won’t comment on their usage here. Again, in many cases, you will likely find that the System.Collections.Generic namespace provides classes with similar functionality and additional benefits.

Image Note There are two additional collection-centric namespaces (System.Collections.ObjectModel and System.Collections.Concurrent) in the .NET base class libraries. You will examine the former namespace later in this chapter, after you are comfortable with the topic of generics. System.Collections.Concurrent provides collection classes well-suited to a multithreaded environment (see Chapter 19 for information on multithreading).

The Problems of Nongeneric Collections

While it is true that many successful .NET applications have been built over the years using these nongeneric collection classes (and interfaces), history has shown that use of these types can result in a number of issues.

The first issue is that using the System.Collections and System.Collections.Specialized classes can result in some poorly performing code, especially when you are manipulating numerical data (e.g., value types). As you’ll see momentarily, the CLR must perform a number of memory transfer operations when you store structures in any nongeneric collection class prototyped to operate on System.Objects, which can hurt runtime execution speed.

The second issue is that most of the nongeneric collection classes are not type safe because (again) they were developed to operate on System.Objects, and they could therefore contain anything at all. If a .NET developer needed to create a highly type-safe collection (e.g., a container that can hold objects implementing only a certain interface), the only real choice was to create a new collection class by hand. Doing so was not too labor intensive, but it was a tad on the tedious side.

Before you look at how to use generics in your programs, you’ll find it helpful to examine the issues of nongeneric collection classes a bit closer; this will help you better understand the problems generics intend to solve in the first place. If you want to follow along, create a new Console Application project named IssuesWithNonGenericCollections. Next, make sure you import the System.Collections namespace to the top of your C# code file.

using System.Collections;

The Issue of Performance

As you might recall from Chapter 4, the .NET platform supports two broad categories of data: value types and reference types. Given that .NET defines two major categories of types, you might occasionally need to represent a variable of one category as a variable of the other category. To do so, C# provides a simple mechanism, termed boxing, to store the data in a value type within a reference variable. Assume that you have created a local variable of type int in a method called SimpleBoxUnboxOperation(). If, during the course of your application, you were to represent this value type as a reference type, you would box the value, as follows:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable.
int myInt = 25;

// Box the int into an object reference.
object boxedInt = myInt;
}

Boxing can be formally defined as the process of explicitly assigning a value type to a System.Object variable. When you box a value, the CLR allocates a new object on the heap and copies the value type’s value (25, in this case) into that instance. What is returned to you is a reference to the newly allocated heap-based object.

The opposite operation is also permitted through unboxing. Unboxing is the process of converting the value held in the object reference back into a corresponding value type on the stack. Syntactically speaking, an unboxing operation looks like a normal casting operation. However, the semantics are quite different. The CLR begins by verifying that the receiving data type is equivalent to the boxed type, and if so, it copies the value back into a local stack-based variable. For example, the following unboxing operations work successfully, given that the underlying type of theboxedInt is indeed an int:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable.
int myInt = 25;

// Box the int into an object reference.
object boxedInt = myInt;

// Unbox the reference back into a corresponding int.
int unboxedInt = (int)boxedInt;
}

When the C# compiler encounters boxing/unboxing syntax, it emits CIL code that contains the box/unbox op codes. If you were to examine your compiled assembly using ildasm.exe, you would find the following:

.method private hidebysig static void SimpleBoxUnboxOperation() cil managed
{
// Code size 19 (0x13)
.maxstack 1
.locals init ([0] int32 myInt, [1] object boxedInt, [2] int32 unboxedInt)
IL_0000: nop
IL_0001: ldc.i4.s 25
IL_0003: stloc.0
IL_0004: ldloc.0
IL_0005: box [mscorlib]System.Int32
IL_000a: stloc.1
IL_000b: ldloc.1
IL_000c: unbox.any [mscorlib]System.Int32
IL_0011: stloc.2
IL_0012: ret
} // end of method Program::SimpleBoxUnboxOperation

Remember that unlike when performing a typical cast, you must unbox into an appropriate data type. If you attempt to unbox a piece of data into the incorrect data type, an InvalidCastException exception will be thrown. To be perfectly safe, you should wrap each unboxing operation in try/catch logic; however, this would be quite labor intensive to do for every unboxing operation. Consider the following code update, which will throw an error because you’re attempting to unbox the boxed int into a long:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable.
int myInt = 25;

// Box the int into an object reference.
object boxedInt = myInt;

// Unbox in the wrong data type to trigger
// runtime exception.
try
{
long unboxedInt = (long)boxedInt;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
}

At first glance, boxing/unboxing might seem like a rather uneventful language feature that is more academic than practical. After all, you will seldom need to store a local value type in a local object variable, as shown here. However, it turns out that the boxing/unboxing process is quite helpful because it allows you to assume everything can be treated as a System.Object, while the CLR takes care of the memory-related details on your behalf.

Let’s look at a practical use of these techniques. Assume you have created a nongeneric System.Collections.ArrayList to hold onto a batch of numeric (stack-allocated) data. If you were to examine the members of ArrayList, you would find they are prototyped to operate on System.Object data. Now consider the Add(), Insert(), and Remove() methods, as well as the class indexer.

public class ArrayList : object,
IList, ICollection, IEnumerable, ICloneable
{
...
public virtual int Add(object value);
public virtual void Insert(int index, object value);
public virtual void Remove(object obj);
public virtual object this[int index] {get; set; }
}

ArrayList has been built to operate on objects, which represent data allocated on the heap, so it might seem strange that the following code compiles and executes without throwing an error:

static void WorkWithArrayList()
{
// Value types are automatically boxed when
// passed to a method requesting an object.
ArrayList myInts = new ArrayList();
myInts.Add(10);
myInts.Add(20);
myInts.Add(35);
}

Although you pass in numerical data directly into methods requiring an object, the runtime automatically boxes the stack-based data on your behalf. Later, if you want to retrieve an item from the ArrayList using the type indexer, you must unbox the heap-allocated object into a stack-allocated integer using a casting operation. Remember that the indexer of the ArrayList is returning System.Objects, not System.Int32s.

static void WorkWithArrayList()
{
// Value types are automatically boxed when
// passed to a member requesting an object.
ArrayList myInts = new ArrayList();
myInts.Add(10);
myInts.Add(20);
myInts.Add(35);

// Unboxing occurs when an object is converted back to
// stack-based data.
int i = (int)myInts[0];

// Now it is reboxed, as WriteLine() requires object types!
Console.WriteLine("Value of your int: {0}", i);
}

Again, note that the stack-allocated System.Int32 is boxed prior to the call to ArrayList.Add(), so it can be passed in the required System.Object. Also note that the System.Object is unboxed back into a System.Int32 once it is retrieved from the ArrayList via the casting operation, only to be boxed again when it is passed to the Console.WriteLine() method, as this method is operating on System.Object variables.

Boxing and unboxing are convenient from a programmer’s viewpoint, but this simplified approach to stack/heap memory transfer comes with the baggage of performance issues (in both speed of execution and code size) and a lack of type safety. To understand the performance issues, ponder the steps that must occur to box and unbox a simple integer.

1. A new object must be allocated on the managed heap.

2. The value of the stack-based data must be transferred into that memory location.

3. When unboxed, the value stored on the heap-based object must be transferred back to the stack.

4. The now unused object on the heap will (eventually) be garbage collected.

Although this particular WorkWithArrayList() method won’t cause a major bottleneck in terms of performance, you could certainly feel the impact if an ArrayList contained thousands of integers that your program manipulates on a somewhat regular basis. In an ideal world, you could manipulate stack-based data in a container without any performance issues. Ideally, it would be nice if you did not have to have to bother plucking data from this container using try/catch scopes (this is exactly what generics let you achieve).

The Issue of Type Safety

I touched on the issue of type safety when covering unboxing operations. Recall that you must unbox your data into the same data type it was declared as before boxing. However, there is another aspect of type safety you must keep in mind in a generic-free world: the fact that a majority of the classes of System.Collections can typically hold anything whatsoever because their members are prototyped to operate on System.Objects. For example, this method builds an ArrayList of random bits of unrelated data:

static void ArrayListOfRandomObjects()
{
// The ArrayList can hold anything at all.
ArrayList allMyObjects = new ArrayList();
allMyObjects.Add(true);
allMyObjects.Add(new OperatingSystem(PlatformID.MacOSX, new Version(10, 0)));
allMyObjects.Add(66);
allMyObjects.Add(3.14);
}

In some cases, you will require an extremely flexible container that can hold literally anything (as shown here). However, most of the time you desire a type-safe container that can operate only on a particular type of data point. For example, you might need a container that can hold only database connections, bitmaps, or IPointy-compatible objects.

Prior to generics, the only way you could address this issue of type safety was to create a custom (strongly typed) collection class manually. Assume you want to create a custom collection that can contain only objects of type Person.

public class Person
{
public int Age {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}

public Person(){}
public Person(string firstName, string lastName, int age)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}

public override string ToString()
{
return string.Format("Name: {0} {1}, Age: {2}",
FirstName, LastName, Age);
}
}

To build a collection that can hold only Person objects, you could define a System.Collections.ArrayList member variable within a class named PersonCollection and configure all members to operate on strongly typed Person objects, rather than onSystem.Object types. Here is a simple example (a production-level custom collection could support many additional members and might extend an abstract base class from the System.Collections or System.Collections.Specialized namespace):

public class PersonCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList();

// Cast for caller.
public Person GetPerson(int pos)
{ return (Person)arPeople[pos]; }

// Insert only Person objects.
public void AddPerson(Person p)
{ arPeople.Add(p); }

public void ClearPeople()
{ arPeople.Clear(); }

public int Count
{ get { return arPeople.Count; } }

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator()
{ return arPeople.GetEnumerator(); }
}

Notice that the PersonCollection class implements the IEnumerable interface, which allows a foreach-like iteration over each contained item. Also notice that your GetPerson() and AddPerson() methods have been prototyped to operate only on Person objects, not bitmaps, strings, database connections, or other items. With these types defined, you are now assured of type safety, given that the C# compiler will be able to determine any attempt to insert an incompatible data type.

static void UsePersonCollection()
{
Console.WriteLine("***** Custom Person Collection *****\n");
PersonCollection myPeople = new PersonCollection();
myPeople.AddPerson(new Person("Homer", "Simpson", 40));
myPeople.AddPerson(new Person("Marge", "Simpson", 38));
myPeople.AddPerson(new Person("Lisa", "Simpson", 9));
myPeople.AddPerson(new Person("Bart", "Simpson", 7));
myPeople.AddPerson(new Person("Maggie", "Simpson", 2));

// This would be a compile-time error!
// myPeople.AddPerson(new Car());

foreach (Person p in myPeople)
Console.WriteLine(p);
}

While custom collections do ensure type safety, this approach leaves you in a position where you must create an (almost identical) custom collection for each unique data type you want to contain. Thus, if you need a custom collection that can operate only on classes deriving from theCar base class, you need to build a highly similar collection class.

public class CarCollection : IEnumerable
{
private ArrayList arCars = new ArrayList();

// Cast for caller.
public Car GetCar(int pos)
{ return (Car) arCars[pos]; }

// Insert only Car objects.
public void AddCar(Car c)
{ arCars.Add(c); }

public void ClearCars()
{ arCars.Clear(); }

public int Count
{ get { return arCars.Count; } }

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator()
{ return arCars.GetEnumerator(); }
}

However, a custom collection class does nothing to solve the issue of boxing/unboxing penalties. Even if you were to create a custom collection named IntCollection that you designed to operate only on System.Int32 items, you would have to allocate some type of object to hold the data (e.g., System.Array and ArrayList).

public class IntCollection : IEnumerable
{
private ArrayList arInts = new ArrayList();

// Get an int (performs unboxing!).
public int GetInt(int pos)
{ return (int)arInts[pos]; }

// Insert an int (performs boxing)!
public void AddInt(int i)
{ arInts.Add(i); }

public void ClearInts()
{ arInts.Clear(); }

public int Count
{ get { return arInts.Count; } }

IEnumerator IEnumerable.GetEnumerator()
{ return arInts.GetEnumerator(); }
}

Regardless of which type you might choose to hold the integers, you cannot escape the boxing dilemma using nongeneric containers.

A First Look at Generic Collections

When you use generic collection classes, you rectify all the previous issues, including boxing/unboxing penalties and a lack of type safety. Also, the need to build a custom (generic) collection class becomes quite rare. Rather than having to build unique classes that can contain people, cars, and integers, you can use a generic collection class and specify the type of type.

Consider the following method, which uses the generic List<T> class (in the System.Collections.Generic namespace) to contain various types of data in a strongly typed manner (don’t fret the details of generic syntax at this time):

static void UseGenericList()
{
Console.WriteLine("***** Fun with Generics *****\n");

// This List<> can hold only Person objects.
List<Person> morePeople = new List<Person>();
morePeople.Add(new Person ("Frank", "Black", 50));
Console.WriteLine(morePeople[0]);

// This List<> can hold only integers.
List<int> moreInts = new List<int>();
moreInts.Add(10);
moreInts.Add(2);
int sum = moreInts[0] + moreInts[1];

// Compile-time error! Can’t add Person object
// to a list of ints!
// moreInts.Add(new Person());
}

The first List<T> object can contain only Person objects. Therefore, you do not need to perform a cast when plucking the items from the container, which makes this approach more type safe. The second List<T> can contain only integers, all of which are allocated on the stack; in other words, there is no hidden boxing or unboxing as you found with the nongeneric ArrayList. Here is a short list of the benefits generic containers provide over their nongeneric counterparts:

· Generics provide better performance because they do not result in boxing or unboxing penalties when storing value types.

· Generics are type safe because they can contain only the type of type you specify.

· Generics greatly reduce the need to build custom collection types because you specify the “type of type” when creating the generic container.

Image Source Code You can find the IssuesWithNonGenericCollections project in the Chapter 9 subdirectory.

The Role of Generic Type Parameters

You can find generic classes, interfaces, structures, and delegates throughout the .NET base class libraries, and these might be part of any .NET namespace. Also be aware that generics have far more uses than simply defining a collection class. To be sure, you will see many different generics used in the remainder of this book for various reasons.

Image Note Only classes, structures, interfaces, and delegates can be written generically; enum types cannot.

When you see a generic item listed in the .NET Framework documentation or the Visual Studio object browser, you will notice a pair of angled brackets with a letter or other token sandwiched within. Figure 9-1 shows the Visual Studio object browser displaying a number of generic itemslocated within the System.Collections.Generic namespace, including the highlighted List<T> class.

image

Figure 9-1. Generic items supporting type parameters

Formally speaking, you call these tokens type parameters; however, in more user-friendly terms, you can simply call them placeholders. You can read the symbol <T> as “of T.” Thus, you can read IEnumerable<T> “as IEnumerable of T” or, to say it another way, “IEnumerableof type T.”

Image Note The name of a type parameter (placeholder) is irrelevant, and it is up to the developer who created the generic item. However, typically T is used to represent types, TKey or K is used for keys, and TValue or V is used for values.

When you create a generic object, implement a generic interface, or invoke a generic member, it is up to you to supply a value to the type parameter. You’ll see many examples in this chapter and throughout the remainder of the text. However, to set the stage, let’s see the basics of interacting with generic types and members.

Specifying Type Parameters for Generic Classes/Structures

When you create an instance of a generic class or structure, you specify the type parameter when you declare the variable and when you invoke the constructor. As you saw in the preceding code example, UseGenericList() defined two List<T> objects.

// This List<> can hold only Person objects.
List<Person> morePeople = new List<Person>();

You can read the preceding snippet as “a List<> of T, where T is of type Person.” Or, more simply, you can read it as “a list of person objects.” After you specify the type parameter of a generic item, it cannot be changed (remember, generics are all about type safety). When you specify a type parameter for a generic class or structure, all occurrences of the placeholder(s) are now replaced with your supplied value.

If you were to view the full declaration of the generic List<T> class using the Visual Studio object browser, you would see that the placeholder T is used throughout the definition of the List<T> type. Here is a partial listing (note the items in bold):

// A partial listing of the List<T> class.
namespace System.Collections.Generic
{
public class List<T> :
IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T>
IList, ICollection, IEnumerable
{
...
public void Add(T item);
public ReadOnlyCollection<T> AsReadOnly();
public int BinarySearch(T item);
public bool Contains(T item);
public void CopyTo(T[] array);
public int FindIndex(System.Predicate<T> match);
public T FindLast(System.Predicate<T> match);
public bool Remove(T item);
public int RemoveAll(System.Predicate<T> match);
public T[] ToArray();
public bool TrueForAll(System.Predicate<T> match);
public T this[int index] { get; set; }
}
}

When you create a List<T> specifying Person objects, it is as if the List<T> type were defined as follows:

namespace System.Collections.Generic
{
public class List<Person> :
IList<Person>, ICollection<Person>, IEnumerable<Person>, IReadOnlyList<Person>
IList, ICollection, IEnumerable
{
...
public void Add(Person item);
public ReadOnlyCollection<Person> AsReadOnly();
public int BinarySearch(Person item);
public bool Contains(Person item);
public void CopyTo(Person[] array);
public int FindIndex(System.Predicate<Person> match);
public Person FindLast(System.Predicate<Person> match);
public bool Remove(Person item);
public int RemoveAll(System.Predicate<Person> match);
public Person[] ToArray();
public bool TrueForAll(System.Predicate<Person> match);
public Person this[int index] { get; set; }
}
}

Of course, when you create a generic List<T> variable, the compiler does not literally create a new implementation of the List<T> class. Rather, it will address only the members of the generic type you actually invoke.

Specifying Type Parameters for Generic Members

It is fine for a nongeneric class or structure to support a handful of generic members (e.g., methods and properties). In these cases, you would also need to specify the placeholder value at the time you invoke the method. For example, System.Array supports a several generic methods. Specifically, the nongeneric static Sort() method now has a generic counterpart named Sort<T>(). Consider the following code snippet, where T is of type int:

int[] myInts = { 10, 4, 2, 33, 93 };

// Specify the placeholder to the generic
// Sort<>() method.
Array.Sort<int>(myInts);

foreach (int i in myInts)
{
Console.WriteLine(i);
}

Specifying Type Parameters for Generic Interfaces

It is common to implement generic interfaces when you build classes or structures that need to support various framework behaviors (e.g., cloning, sorting, and enumeration). In Chapter 8, you learned about a number of nongeneric interfaces, such as IComparable, IEnumerable,IEnumerator, and IComparer. Recall that the nongeneric IComparable interface was defined like this:

public interface IComparable
{
int CompareTo(object obj);
}

In Chapter 8, you also implemented this interface on your Car class to enable sorting in a standard array. However, the code required several runtime checks and casting operations because the parameter was a general System.Object.

public class Car : IComparable
{
...
// IComparable implementation.
int IComparable.CompareTo(object obj)
{
Car temp = obj as Car;
if (temp != null)
{
if (this.CarID > temp.CarID)
return 1;
if (this.CarID < temp.CarID)
return -1;
else
return 0;
}
else
throw new ArgumentException("Parameter is not a Car!");
}
}

Now assume you use the generic counterpart of this interface.

public interface IComparable<T>
{
int CompareTo(T obj);
}

In this case, your implementation code will be cleaned up considerably.

public class Car : IComparable<Car>
{
...
// IComparable<T> implementation.
int IComparable<Car>.CompareTo(Car obj)
{
if (this.CarID > obj.CarID)
return 1;
if (this.CarID < obj.CarID)
return -1;
else
return 0;
}
}

Here, you do not need to check whether the incoming parameter is a Car because it can only be a Car! If someone were to pass in an incompatible data type, you would get a compile-time error. Now that you have a better handle on how to interact with generic items, as well as the role of type parameters (a.k.a. placeholders), you’re ready to examine the classes and interfaces of the System.Collections.Generic namespace.

The System.Collections.Generic Namespace

When you are building a .NET application and need a way to manage in-memory data, the classes of System.Collections.Generic will most likely fit the bill. At the opening of this chapter, I briefly mentioned some of the core nongeneric interfaces implemented by the nongeneric collection classes. Not too surprisingly, the System.Collections.Generic namespace defines generic replacements for many of them.

In fact, you can find a number of the generic interfaces that extend their nongeneric counterparts. This might seem odd; however, by doing so, implementing classes will also support the legacy functionally found in their nongeneric siblings. For example, IEnumerable<T> extendsIEnumerable. Table 9-4 documents the core generic interfaces you’ll encounter when working with the generic collection classes.

Table 9-4. Key Interfaces Supported by Classes of System.Collections.Generic

System.Collections.Generic Interface

Meaning in Life

ICollection<T>

Defines general characteristics (e.g., size, enumeration, and thread safety) for all generic collection types

IComparer<T>

Defines a way to compare to objects

IDictionary<TKey, TValue>

Allows a generic collection object to represent its contents using key-value pairs

IEnumerable<T>

Returns the IEnumerator<T> interface for a given object

IEnumerator<T>

Enables foreach-style iteration over a generic collection

IList<T>

Provides behavior to add, remove, and index items in a sequential list of objects

ISet<T>

Provides the base interface for the abstraction of sets

The System.Collections.Generic namespace also defines several classes that implement many of these key interfaces. Table 9-5 describes some commonly used classes of this namespace, the interfaces they implement, and their basic functionality.

Table 9-5. Classes of System.Collections.Generic

Generic Class

Supported Key Interfaces

Meaning in Life

Dictionary<TKey, TValue>

ICollection<T>, IDictionary<TKey, TValue>, IEnumerable<T>

This represents a generic collection of keys and values.

LinkedList<T>

ICollection<T>, IEnumerable<T>

This represents a doubly linked list.

List<T>

ICollection<T>, IEnumerable<T>,IList<T>

This is a dynamically resizable sequential list of items.

Queue<T>

ICollection (Not a typo! This is the nongeneric collection interface), IEnumerable<T>

This is a generic implementation of a first-in, first-out (FIFO) list.

SortedDictionary<TKey, TValue>

ICollection<T>, IDictionary<TKey, TValue>, IEnumerable<T>

This is a generic implementation of a sorted set of key-value pairs.

SortedSet<T>

ICollection<T>, IEnumerable<T>, ISet<T>

This represents a collection of objects that is maintained in sorted order with no duplication.

Stack<T>

ICollection (Not a typo! This is the nongeneric collection interface), IEnumerable<T>

This is a generic implementation of a last-in, first-out (LIFO) list.

The System.Collections.Generic namespace also defines many auxiliary classes and structures that work in conjunction with a specific container. For example, the LinkedListNode<T> type represents a node within a generic LinkedList<T>, theKeyNotFoundException exception is raised when attempting to grab an item from a container using a nonexistent key, and so forth.

It is also worth pointing out that mscorlib.dll and System.dll are not the only assemblies that add new types to the System.Collections.Generic namespace. For example, System.Core.dll adds the HashSet<T> class to the mix. Be sure to consult the .NET Framework documentation for full details of the System.Collections.Generic namespace.

In any case, your next task is to learn how to use some of these generic collection classes. Before you do, however, allow me to illustrate a C# language feature (first introduced in .NET 3.5) that simplifies the way you populate generic (and nongeneric) collection containers with data.

Understanding Collection Initialization Syntax

In Chapter 4, you learned about object initialization syntax, which allows you to set properties on a new variable at the time of construction. Closely related to this is collection initialization syntax. This C# language feature makes it possible to populate many containers (such as ArrayListor List<T>) with items by using syntax similar to what you use to populate a basic array.

Image Note You can apply collection initialization syntax only to classes that support an Add() method, which is formalized by the ICollection<T>/ICollection interfaces.

Consider the following examples:

// Init a standard array.
int[] myArrayOfInts = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Init a generic List<> of ints.
List<int> myGenericList = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Init an ArrayList with numerical data.
ArrayList myList = new ArrayList { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

If your container is managing a collection of classes or a structure, you can blend object initialization syntax with collection initialization syntax to yield some functional code. You might recall the Point class from Chapter 5, which defined two properties named X and Y. If you wanted to build a generic List<T> of Point objects, you could write the following:

List<Point> myListOfPoints = new List<Point>
{
new Point { X = 2, Y = 2 },
new Point { X = 3, Y = 3 },
new Point(PointColor.BloodRed){ X = 4, Y = 4 }
};

foreach (var pt in myListOfPoints)
{
Console.WriteLine(pt);
}

Again, the benefit of this syntax is that you save yourself numerous keystrokes. While the nested curly brackets can become difficult to read if you don’t mind your formatting, imagine the amount of code that would be required to fill the following List<T> of Rectangles if you did not have collection initialization syntax (you might recall from Chapter 4 that you created a Rectangle class that contained two properties encapsulating Point objects).

List<Rectangle> myListOfRects = new List<Rectangle>
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 },
BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 },
BottomRight = new Point { X = 90, Y = 75}}
};

foreach (var r in myListOfRects)
{
Console.WriteLine(r);
}

Working with the List<T> Class

Create a new Console Application project named FunWithGenericCollections. Note that your initial C# code file already imports the System.Collections.Generic namespace.

The first generic class you will examine is List<T>, which you’ve already seen once or twice in this chapter. The List<T> class is bound to be your most frequently used type in the System.Collections.Generic namespace because it allows you to resize the contents of the container dynamically. To illustrate the basics of this type, ponder the following method in your Program class, which leverages List<T> to manipulate the set of Person objects shown earlier in this chapter; you might recall that these Person objects defined three properties (Age,FirstName, and LastName) and a custom ToString() implementation:

static void UseGenericList()
{
// Make a List of Person objects, filled with
// collection/object init syntax.
List<Person> people = new List<Person>()
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47},
new Person {FirstName= "Marge", LastName="Simpson", Age=45},
new Person {FirstName= "Lisa", LastName="Simpson", Age=9},
new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};

// Print out # of items in List.
Console.WriteLine("Items in list: {0}", people.Count);

// Enumerate over list.
foreach (Person p in people)
Console.WriteLine(p);

// Insert a new person.
Console.WriteLine("\n->Inserting new person.");
people.Insert(2, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 });
Console.WriteLine("Items in list: {0}", people.Count);

// Copy data into a new array.
Person[] arrayOfPeople = people.ToArray();
for (int i = 0; i < arrayOfPeople.Length; i++)
{
Console.WriteLine("First Names: {0}", arrayOfPeople[i].FirstName);
}
}

Here, you use initialization syntax to populate your List<T> with objects, as a shorthand notation for calling Add() multiple times. After you print out the number of items in the collection (as well as enumerate over each item), you invoke Insert(). As you can see, Insert()allows you to plug a new item into the List<T> at a specified index.

Finally, notice the call to the ToArray() method, which returns an array of Person objects based on the contents of the original List<T>. From this array, you loop over the items again using the array’s indexer syntax. If you call this method from within Main(), you get the following output:

***** Fun with Generic Collections *****

Items in list: 4
Name: Homer Simpson, Age: 47
Name: Marge Simpson, Age: 45
Name: Lisa Simpson, Age: 9
Name: Bart Simpson, Age: 8

->Inserting new person.
Items in list: 5
First Names: Homer
First Names: Marge
First Names: Maggie
First Names: Lisa
First Names: Bart

The List<T> class defines many additional members of interest, so be sure to consult the .NET Framework documentation for more information. Next, let’s look at a few more generic collections, specifically Stack<T>, Queue<T>, and SortedSet<T>. This should get you in a great position to understand your basic choices regarding how to hold your custom application data.

Working with the Stack<T> Class

The Stack<T> class represents a collection that maintains items using a last-in, first-out manner. As you might expect, Stack<T> defines members named Push() and Pop() to place items onto or remove items from the stack. The following method creates a stack of Person objects:

static void UseGenericStack()
{
Stack<Person> stackOfPeople = new Stack<Person>();
stackOfPeople.Push(new Person
{ FirstName = "Homer", LastName = "Simpson", Age = 47 });
stackOfPeople.Push(new Person
{ FirstName = "Marge", LastName = "Simpson", Age = 45 });
stackOfPeople.Push(new Person
{ FirstName = "Lisa", LastName = "Simpson", Age = 9 });

// Now look at the top item, pop it, and look again.

Console.WriteLine("First person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop());
Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop());
Console.WriteLine("\nFirst person item is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop());

try
{
Console.WriteLine("\nnFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop());
}
catch (InvalidOperationException ex)
{
Console.WriteLine("\nError! {0}", ex.Message);
}
}

Here, you build a stack that contains three people, added in the order of their first names: Homer, Marge, and Lisa. As you peek into the stack, you will always see the object at the top first; therefore, the first call to Peek() reveals the third Person object. After a series of Pop() andPeek() calls, the stack eventually empties, at which time additional Peek() and Pop() calls raise a system exception. You can see the output for this here:

***** Fun with Generic Collections *****

First person is: Name: Lisa Simpson, Age: 9
Popped off Name: Lisa Simpson, Age: 9

First person is: Name: Marge Simpson, Age: 45
Popped off Name: Marge Simpson, Age: 45

First person item is: Name: Homer Simpson, Age: 47
Popped off Name: Homer Simpson, Age: 47

Error! Stack empty.

Working with the Queue<T> Class

Queues are containers that ensure items are accessed in a first-in, first-out manner. Sadly, we humans are subject to queues all day long: lines at the bank, lines at the movie theater, and lines at the morning coffeehouse. When you need to model a scenario in which items are handled on a first-come, first- served basis, you will find the Queue<T> class fits the bill. In addition to the functionality provided by the supported interfaces, Queue defines the key members shown in Table 9-6.

Table 9-6. Members of the Queue<T> Type

Select Member of Queue<T>

Meaning in Life

Dequeue()

Removes and returns the object at the beginning of the Queue<T>

Enqueue()

Adds an object to the end of the Queue<T>

Peek()

Returns the object at the beginning of the Queue<T> without removing it

Now let’s put these methods to work. You can begin by leveraging your Person class again and building a Queue<T> object that simulates a line of people waiting to order coffee. First, assume you have the following static helper method:

static void GetCoffee(Person p)
{
Console.WriteLine("{0} got coffee!", p.FirstName);
}

Now assume you have this additional helper method, which calls GetCoffee() internally:

static void UseGenericQueue()
{
// Make a Q with three people.
Queue<Person> peopleQ = new Queue<Person>();
peopleQ.Enqueue(new Person {FirstName= "Homer",
LastName="Simpson", Age=47});
peopleQ.Enqueue(new Person {FirstName= "Marge",
LastName="Simpson", Age=45});
peopleQ.Enqueue(new Person {FirstName= "Lisa",
LastName="Simpson", Age=9});

// Peek at first person in Q.
Console.WriteLine("{0} is first in line!", peopleQ.Peek().FirstName);

// Remove each person from Q.
GetCoffee(peopleQ.Dequeue());
GetCoffee(peopleQ.Dequeue());
GetCoffee(peopleQ.Dequeue());
// Try to de-Q again?
try
{
GetCoffee(peopleQ.Dequeue());
}
catch(InvalidOperationException e)
{
Console.WriteLine("Error! {0}", e.Message);
}
}

Here, you insert three items into the Queue<T> class using its Enqueue() method. The call to Peek() allows you to view (but not remove) the first item currently in the Queue. Finally, the call to Dequeue() removes the item from the line and sends it into the GetCoffee()helper function for processing. Note that if you attempt to remove items from an empty queue, a runtime exception is thrown. Here is the output you receive when calling this method:

***** Fun with Generic Collections *****

Homer is first in line!
Homer got coffee!
Marge got coffee!
Lisa got coffee!
Error! Queue empty.

Working with the SortedSet<T> Class

The SortedSet<T> class is useful because it automatically ensures that the items in the set are sorted when you insert or remove items. However, you do need to inform the SortedSet<T> class exactly how you want it to sort the objects, by passing in as a constructor argument an object that implements the generic IComparer<T> interface.

Begin by creating a new class named SortPeopleByAge, which implements IComparer<T>, where T is of type Person. Recall that this interface defines a single method named Compare(), where you can author whatever logic you require for the comparison. Here is a simple implementation of this class:

class SortPeopleByAge : IComparer<Person>
{
public int Compare(Person firstPerson, Person secondPerson)
{
if (firstPerson.Age > secondPerson.Age)
return 1;
if (firstPerson.Age < secondPerson.Age)
return -1;
else
return 0;
}
}

Now update your Program class with the following new method, which I assume you will call from Main():

static void UseSortedSet()
{
// Make some people with different ages.
SortedSet<Person> setOfPeople = new SortedSet<Person>(new SortPeopleByAge())
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47},
new Person {FirstName= "Marge", LastName="Simpson", Age=45},
new Person {FirstName= "Lisa", LastName="Simpson", Age=9},
new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};

// Note the items are sorted by age!
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
Console.WriteLine();

// Add a few new people, with various ages.
setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age = 1 });
setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 });

// Still sorted by age!
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
}

When you run your application, the listing of objects is now always ordered based on the value of the Age property, regardless of the order you inserted or removed objects.

***** Fun with Generic Collections *****

Name: Bart Simpson, Age: 8
Name: Lisa Simpson, Age: 9
Name: Marge Simpson, Age: 45
Name: Homer Simpson, Age: 47

Name: Saku Jones, Age: 1
Name: Bart Simpson, Age: 8
Name: Lisa Simpson, Age: 9
Name: Mikko Jones, Age: 32
Name: Marge Simpson, Age: 45
Name: Homer Simpson, Age: 47

Working with the Dictionary<TKey, TValue> Class

Another handy generic collection is the Dictionary<TKey,TValue> type, which allows you to hold any number of objects that may be referred to via a unique key. Thus, rather than obtaining an item from a List<T> using a numerical identifier (for example, “Give me the second object”), you could use the unique text key (for example, “Give me the object I keyed as Homer”).

Like other collection objects, you can populate a Dictionary<TKey,TValue> by calling the generic Add() method manually. However, you can also fill a Dictionary<TKey,TValue> using collection initialization syntax. Do be aware that when you are populating this collection object, key names must be unique. If you mistakenly specify the same key multiple times, you will receive a runtime exception.

Consider the following method that fills a Dictionary<K,V> with various objects. Notice when you create the Dictionary<TKey,TValue> object, you specify the key type (TKey) and underlying object type (TValue) as constructor arguments. Here, you are using a stringdata type as the key (although this is not required; the key can be any type) and a Person type as the value.

private static void UseDictionary()
{
// Populate using Add() method
Dictionary<string, Person> peopleA = new Dictionary<string, Person>();
peopleA.Add("Homer", new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 });
peopleA.Add("Marge", new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 });
peopleA.Add("Lisa", new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 });

// Get Homer.
Person homer = peopleA["Homer"];
Console.WriteLine(homer);

// Populate with initialization syntax.
Dictionary<string, Person> peopleB = new Dictionary<string, Person>()
{
{ "Homer", new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 } },
{ "Marge", new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 } },
{ "Lisa", new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 } }
};

// Get Lisa.
Person lisa = peopleB["Lisa"];
Console.WriteLine(lisa);
}

It is also possible to populate a Dictionary<TKey,TValue> using a related initialization syntax introduced with the current version of .NET that is specific to this type of container (not surprisingly termed dictionary initialization). Similar to the syntax used to populate thepersonB object in the previous code example, you still define an initialization scope for the collection object; however, you can use the indexer to specify the key and assign this to a new object as so:

// Populate with dictionary initialization syntax.
Dictionary<string, Person> peopleC = new Dictionary<string, Person>()
{
["Homer"] = new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 },
["Marge"] = new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 },
["Lisa"] = new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 }
};

Image Note You can find the FunWithGenericCollections project in the Chapter 9 subdirectory.

The System.Collections.ObjectModel Namespace

Now that you understand how to work with the major generic classes, you can briefly examine an additional collection-centric namespace, System.Collections.ObjectModel. This is a relatively small namespace, which contains a handful of classes. Table 9-7 documents the two classes that you should most certainly be aware of.

Table 9-7. Useful Members of System.Collections.ObjectModel

System.Collections.ObjectModel Type

Meaning in Life

ObservableCollection<T>

Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed

ReadOnlyObservableCollection<T>

Represents a read-only version of ObservableCollection<T>

The ObservableCollection<T> class is useful in that it has the ability to inform external objects when its contents have changed in some way (as you might guess, working with ReadOnlyObservableCollection<T> is similar but read-only in nature).

Working with ObservableCollection<T>

Create a new Console Application project named FunWithObservableCollection and import the System.Collections.ObjectModel namespace into your initial C# code file. In many ways, working with ObservableCollection<T> is identical to working with List<T>, given that both of these classes implement the same core interfaces. What makes the ObservableCollection<T> class unique is that this class supports an event named CollectionChanged. This event will fire whenever a new item is inserted, a current item is removed (or relocated), or the entire collection is modified.

Like any event, CollectionChanged is defined in terms of a delegate, which in this case is NotifyCollectionChangedEventHandler. This delegate can call any method that takes an object as the first parameter and takes a NotifyCollectionChangedEventArgs as the second. Consider the following Main() method, which populates an observable collection containing Person objects and wires up the CollectionChanged event:

class Program
{
static void Main(string[] args)
{
// Make a collection to observe and add a few Person objects.
ObservableCollection<Person> people = new ObservableCollection<Person>()
{
new Person{ FirstName = "Peter", LastName = "Murphy", Age = 52 },
new Person{ FirstName = "Kevin", LastName = "Key", Age = 48 },
};

// Wire up the CollectionChanged event.
people.CollectionChanged += people_CollectionChanged;
}

static void people_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
throw new NotImplementedException();
}
}

The incoming NotifyCollectionChangedEventArgs parameter defines two important properties, OldItems and NewItems, which will give you a list of items that were currently in the collection before the event fired and the new items that were involved in the change. However, you will want to examine these lists only under the correct circumstances. Recall that the CollectionChanged event can fire when items are added, removed, relocated, or reset. To discover which of these actions triggered the event, you can use the Action property ofNotifyCollectionChangedEventArgs. The Action property can be tested against any of the following members of the NotifyCollectionChangedAction enumeration:

public enum NotifyCollectionChangedAction
{
Add = 0,
Remove = 1,
Replace = 2,
Move = 3,
Reset = 4,
}

Here is an implementation of the CollectionChanged event handler that will traverse the old and new sets when an item has been inserted or removed to the collection at hand:

static void people_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// What was the action that caused the event?
Console.WriteLine("Action for this event: {0}", e.Action);

// They removed something.
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
Console.WriteLine("Here are the OLD items:");
foreach (Person p in e.OldItems)
{
Console.WriteLine(p.ToString());
}
Console.WriteLine();
}

// They added something.
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
// Now show the NEW items that were inserted.
Console.WriteLine("Here are the NEW items:");
foreach (Person p in e.NewItems)
{
Console.WriteLine(p.ToString());
}
}
}

Now, assuming you have updated your Main() method to add and remove an item, you will see output similar to the following:

Action for this event: Add
Here are the NEW items:
Name: Fred Smith, Age: 32

Action for this event: Remove
Here are the OLD items:
Name: Peter Murphy, Age: 52

That wraps up the examination of the various collection-centric namespaces in the .NET base class libraries. To conclude the chapter, you will now examine how you can build your own custom generic methods and custom generic types.

Image Source Code You can find the FunWithObservableCollection project in the Chapter 9 subdirectory.

Creating Custom Generic Methods

While most developers typically use the existing generic types within the base class libraries, it is also possible to build your own generic members and custom generic types. Let’s look at how to incorporate custom generics into your own projects. The first step is to build a generic swap method. Begin by creating a new console application named CustomGenericMethods.

When you build custom generic methods, you achieve a supercharged version of traditional method overloading. In Chapter 2, you learned that overloading is the act of defining multiple versions of a single method, which differ by the number of, or type of, parameters.

While overloading is a useful feature in an object-oriented language, one problem is that you can easily end up with a ton of methods that essentially do the same thing. For example, assume you need to build some methods that can switch two pieces of data using a simple swap routine. You might begin by authoring a new method that can operate on integers, like this:

// Swap two integers.
static void Swap(ref int a, ref int b)
{
int temp;
temp = a;
a = b;
b = temp;
}

So far, so good. But now assume you also need to swap two Person objects; this would require authoring a new version of Swap().

// Swap two Person objects.
static void Swap(ref Person a, ref Person b)
{
Person temp;
temp = a;
a = b;
b = temp;
}

No doubt, you can see where this is going. If you also needed to swap floating-point numbers, bitmaps, cars, buttons, and whatnot, you would have to build even more methods, which would become a maintenance nightmare. You could build a single (nongeneric) method that operated onobject parameters, but then you face all the issues you examined earlier in this chapter, including boxing, unboxing, a lack of type safety, explicit casting, and so on.

Whenever you have a group of overloaded methods that differ only by incoming arguments, this is your clue that generics could make your life easier. Consider the following generic Swap<T> method that can swap any two Ts:

// This method will swap any two items.
// as specified by the type parameter <T>.
static void Swap<T>(ref T a, ref T b)
{
Console.WriteLine("You sent the Swap() method a {0}",
typeof(T));
T temp;
temp = a;
a = b;
b = temp;
}

Notice how a generic method is defined by specifying the type parameters after the method name but before the parameter list. Here, you state that the Swap<T>() method can operate on any two parameters of type <T>. To spice things up a bit, you also print out the type name of the supplied placeholder to the console using C#’s typeof() operator. Now consider the following Main() method, which swaps integers and strings:

static void Main(string[] args)
{
Console.WriteLine("***** Fun with Custom Generic Methods *****\n");

// Swap 2 ints.
int a = 10, b = 90;
Console.WriteLine("Before swap: {0}, {1}", a, b);
Swap<int>(ref a, ref b);
Console.WriteLine("After swap: {0}, {1}", a, b);
Console.WriteLine();

// Swap 2 strings.
string s1 = "Hello", s2 = "There";
Console.WriteLine("Before swap: {0} {1}!", s1, s2);
Swap<string>(ref s1, ref s2);
Console.WriteLine("After swap: {0} {1}!", s1, s2);

Console.ReadLine();
}

The output looks like this:

***** Fun with Custom Generic Methods *****

Before swap: 10, 90
You sent the Swap() method a System.Int32
After swap: 90, 10

Before swap: Hello There!
You sent the Swap() method a System.String
After swap: There Hello!

The major benefit of this approach is that you have only one version of Swap<T>() to maintain, yet it can operate on any two items of a given type in a type-safe manner. Better yet, stack-based items stay on the stack, while heap-based items stay on the heap!

Inference of Type Parameters

When you invoke generic methods such as Swap<T>, you can optionally omit the type parameter if (and only if) the generic method requires arguments because the compiler can infer the type parameter based on the member parameters. For example, you could swap twoSystem.Boolean values by adding the following code to Main():

// Compiler will infer System.Boolean.
bool b1 = true, b2 = false;
Console.WriteLine("Before swap: {0}, {1}", b1, b2);
Swap(ref b1, ref b2);
Console.WriteLine("After swap: {0}, {1}", b1, b2);

Even though the compiler is able to discover the correct type parameter based on the data type used to declare b1 and b2, you should get in the habit of always specifying the type parameter explicitly.

Swap<string>(ref b1, ref b2);

This makes it clear to your fellow programmers that this method is indeed generic. Moreover, inference of type parameters works only if the generic method has at least one parameter. For example, assume you have the following generic method in your Program class:

static void DisplayBaseClass<T>()
{
// BaseType is a method used in reflection,
// which will be examined in Chapter 15
Console.WriteLine("Base class of {0} is: {1}.",
typeof(T), typeof(T).BaseType);
}

In this case, you must supply the type parameter upon invocation.

static void Main(string[] args)
{
...
// Must supply type parameter if
// the method does not take params.
DisplayBaseClass<int>();
DisplayBaseClass<string>();

// Compiler error! No params? Must supply placeholder!
// DisplayBaseClass();
Console.ReadLine();
}

Currently, the generic Swap<T> and DisplayBaseClass<T> methods are defined within the application’s Program class. Of course, as with any method, you are free to define these members in a separate class type (MyGenericMethods) if you would prefer to do it that way.

public static class MyGenericMethods
{
public static void Swap<T>(ref T a, ref T b)
{
Console.WriteLine("You sent the Swap() method a {0}",
typeof(T));
T temp;
temp = a;
a = b;
b = temp;
}

public static void DisplayBaseClass<T>()
{
Console.WriteLine("Base class of {0} is: {1}.",
typeof(T), typeof(T).BaseType);
}
}

The static Swap<T> and DisplayBaseClass<T> methods have been scoped within a new static class type, so you need to specify the type’s name when invoking either member, as in this example:

MyGenericMethods.Swap<int>(ref a, ref b);

Of course, generic methods do not need to be static. If Swap<T> and DisplayBaseClass<T> were instance level (and defined in a nonstatic class), you would simply make an instance of MyGenericMethods and invoke them using the object variable.

MyGenericMethods c = new MyGenericMethods();
c.Swap<int>(ref a, ref b);

Image Source Code You can find the CustomGenericMethods project in the Chapter 9 subdirectory.

Creating Custom Generic Structures and Classes

Now that you understand how to define and invoke generic methods, it’s time to turn your attention to the construction of a generic structure (the process of building a generic class is identical) within a new Console Application project named GenericPoint. Assume you have built a genericPoint structure that supports a single type parameter that represents the underlying storage for the (x, y) coordinates. The caller can then create Point<T> types as follows:

// Point using ints.
Point<int> p = new Point<int>(10, 10);

// Point using double.
Point<double> p2 = new Point<double>(5.4, 3.3);

Here is the complete definition of Point<T>, with some analysis to follow:

// A generic Point structure.
public struct Point<T>
{
// Generic state date.
private T xPos;
private T yPos;

// Generic constructor.
public Point(T xVal, T yVal)
{
xPos = xVal;
yPos = yVal;
}

// Generic properties.
public T X
{
get { return xPos; }
set { xPos = value; }
}

public T Y
{
get { return yPos; }
set { yPos = value; }
}

public override string ToString()
{
return string.Format("[{0}, {1}]", xPos, yPos);
}

// Reset fields to the default value of the
// type parameter.
public void ResetPoint()
{
xPos = default(T);
yPos = default(T);
}
}

The default Keyword in Generic Code

As you can see, Point<T> leverages its type parameter in the definition of the field data, constructor arguments, and property definitions. Notice that, in addition to overriding ToString(), Point<T> defines a method named ResetPoint() that uses some new syntax you have not yet seen.

// The "default" keyword is overloaded in C#.
// When used with generics, it represents the default
// value of a type parameter.
public void ResetPoint()
{
X = default(T);
Y = default(T);
}

With the introduction of generics, the C# default keyword has been given a dual identity. In addition to its use within a switch construct, it can also be used to set a type parameter to its default value. This is helpful because a generic type does not know the actual placeholders up front, which means it cannot safely assume what the default value will be. The defaults for a type parameter are as follows:

· Numeric values have a default value of 0.

· Reference types have a default value of null.

· Fields of a structure are set to 0 (for value types) or null (for reference types).

For Point<T>, you can set the X and Y values to 0 directly because it is safe to assume the caller will supply only numerical data. However, you can also increase the overall flexibility of the generic type by using the default(T) syntax. In any case, you can now exercise the methods of Point<T>.

static void Main(string[] args)
{
Console.WriteLine("***** Fun with Generic Structures *****\n");

// Point using ints.
Point<int> p = new Point<int>(10, 10);
Console.WriteLine("p.ToString()={0}", p.ToString());
p.ResetPoint();
Console.WriteLine("p.ToString()={0}", p.ToString());
Console.WriteLine();

// Point using double.
Point<double> p2 = new Point<double>(5.4, 3.3);
Console.WriteLine("p2.ToString()={0}", p2.ToString());
p2.ResetPoint();
Console.WriteLine("p2.ToString()={0}", p2.ToString());
Console.ReadLine();
}

Here is the output:

***** Fun with Generic Structures *****

p.ToString()=[10, 10]
p.ToString()=[0, 0]

p2.ToString()=[5.4, 3.3]
p2.ToString()=[0, 0]

Image Source Code You can find the GenericPoint project in the Chapter 9 subdirectory.

Constraining Type Parameters

As this chapter illustrates, any generic item has at least one type parameter that you need to specify at the time you interact with the generic type or member. This alone allows you to build some type-safe code; however, the .NET platform allows you to use the where keyword to get extremely specific about what a given type parameter must look like.

Using this keyword, you can add a set of constraints to a given type parameter, which the C# compiler will check at compile time. Specifically, you can constrain a type parameter as described in Table 9-8.

Table 9-8. Possible Constraints for Generic Type Parameters

Generic Constraint

Meaning in Life

where T : struct

The type parameter <T> must have System.ValueType in its chain of inheritance (i.e., <T> must be a structure).

where T : class

The type parameter <T> must not have System.ValueType in its chain of inheritance (i.e., <T> must be a reference type).

where T : new()

The type parameter <T> must have a default constructor. This is helpful if your generic type must create an instance of the type parameter because you cannot assume you know the format of custom constructors. Note that this constraint must be listed last on a multiconstrained type.

where T : NameOfBaseClass

The type parameter <T> must be derived from the class specified by NameOfBaseClass.

where T : NameOfInterface

The type parameter <T> must implement the interface specified by NameOfInterface. You can separate multiple interfaces as a comma-delimited list.

Unless you need to build some extremely type-safe custom collections, you might never need to use the where keyword in your C# projects. Regardless, the following handful of (partial) code examples illustrate how to work with the where keyword.

Examples Using the where Keyword

Begin by assuming that you have created a custom generic class, and you want to ensure that the type parameter has a default constructor. This could be useful when the custom generic class needs to create instances of the T because the default constructor is the only constructor that is potentially common to all types. Also, constraining T in this way lets you get compile-time checking; if T is a reference type, the programmer remembered to redefine the default in the class definition (you might recall that the default constructor is removed in classes when you define your own).

// MyGenericClass derives from object, while
// contained items must have a default ctor.
public class MyGenericClass<T> where T : new()
{
...
}

Notice that the where clause specifies which type parameter is being constrained, followed by a colon operator. After the colon operator, you list each possible constraint (in this case, a default constructor). Here is another example:

// MyGenericClass derives from object, while
// contained items must be a class implementing IDrawable
// and must support a default ctor.
public class MyGenericClass<T> where T : class, IDrawable, new()
{
...
}

In this case, T has three requirements. It must be a reference type (not a structure), as marked with the class token. Second, T must implement the IDrawable interface. Third, it must also have a default constructor. Multiple constraints are listed in a comma-delimited list; however, you should be aware that the new() constraint must always be listed last! Thus, the following code will not compile:

// Error! new() constraint must be listed last!
public class MyGenericClass<T> where T : new(), class, IDrawable
{
...
}

If you ever create a custom generic collection class that specifies multiple type parameters, you can specify a unique set of constraints for each, using separate where clauses.

// <K> must extend SomeBaseClass and have a default ctor,
// while <T> must be a structure and implement the
// generic IComparable interface.
public class MyGenericClass<K, T> where K : SomeBaseClass, new()
where T : struct, IComparable<T>
{
...
}

You will rarely encounter cases where you need to build a complete custom generic collection class; however, you can use the where keyword on generic methods, as well. For example, if you want to specify that your generic Swap<T>() method can operate only on structures, you would update the method like this:

// This method will swap any structure, but not classes.
static void Swap<T>(ref T a, ref T b) where T : struct
{
...
}

Note that if you were to constrain the Swap() method in this manner, you would no longer be able to swap string objects (as is shown in the sample code) because string is a reference type.

The Lack of Operator Constraints

I want to make one more comment about generic methods and constraints as this chapter draws to a close. It might come as a surprise to you to find out that when creating generic methods, you will get a compiler error if you apply any C# operators (+, -, *, ==, etc.) on the type parameters. For example, imagine the usefulness of a class that can add, subtract, multiply, and divide generic types.

// Compiler error! Cannot apply
// operators to type parameters!
public class BasicMath<T>
{
public T Add(T arg1, T arg2)
{ return arg1 + arg2; }
public T Subtract(T arg1, T arg2)
{ return arg1 - arg2; }
public T Multiply(T arg1, T arg2)
{ return arg1 * arg2; }
public T Divide(T arg1, T arg2)
{ return arg1 / arg2; }
}

Unfortunately, the preceding BasicMath class will not compile. While this might seem like a major restriction, you need to remember that generics are generic. Of course, the numerical data can work just fine with the binary operators of C#. However, for the sake of argument, if <T>were a custom class or structure type, the compiler could assume the class supports the +, -, *, and / operators. Ideally, C# would allow a generic type to be constrained by supported operators, as in this example:

// Illustrative code only!
public class BasicMath<T> where T : operator +, operator -,
operator *, operator /
{
public T Add(T arg1, T arg2)
{ return arg1 + arg2; }
public T Subtract(T arg1, T arg2)
{ return arg1 - arg2; }
public T Multiply(T arg1, T arg2)
{ return arg1 * arg2; }
public T Divide(T arg1, T arg2)
{ return arg1 / arg2; }
}

Alas, operator constraints are not supported under the current version of C#. However, it is possible (albeit it requires a bit more work) to achieve the desired effect by defining an interface that supports these operators (C# interfaces can define operators!) and then specifying an interface constraint of the generic class. In any case, this wraps up this book’s initial look at building custom generic types. In Chapter 10, I will pick up the topic of generics once again in the course of examining the .NET delegate type.

Summary

This chapter began by examining the nongeneric collection types of System.Collections and System.Collections.Specialized, including the various issues associated with many nongeneric containers, including a lack of type safety and the runtime overhead of boxing and unboxing operations. As mentioned, for these very reasons, modern-day .NET programs will typically make use of the generic collection classes found in System.Collections.Generic and System.Collections.ObjectModel.

As you have seen, a generic item allows you to specify placeholders (type parameters) that you specify at the time of object creation (or invocation, in the case of generic methods). While you will most often simply use the generic types provided in the .NET base class libraries, you will also be able to create your own generic types (and generic methods). When you do so, you have the option of specifying any number of constraints (using the where keyword) to increase the level of type safety and ensure that you perform operations on types of a known quantity that are guaranteed to exhibit certain basic capabilities.

As a final note, remember that generics are found in numerous locations within the .NET base class libraries. Here, you focused specifically on generic collections. However, as you work through the remainder of this book (and when you dive into the platform on your own terms), you will certainly find generic classes, structures, and delegates located in a given namespace. As well, be on the lookout for generic members of a nongeneric class!