Pro C#10 CHAPTER 10 Collections and Generics

CHAPTER 10

Collections and Generics

Any application you create with the .NET Core 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 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 termed generics; with this change, a 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 Core 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 have 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 of constraints (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 code, which creates an array of textual data and manipulates its contents in various ways:

// 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);
}

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_10

387

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 criterion (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 created with a “fixed size.” If you make an array of three items, you get only three items; therefore, the following code will result in a runtime exception (an IndexOutOfRangeException, to be exact):

// 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?";

■ Note It is actually possible to change the size of an array using the generic Resize() 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 Core 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 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 on only 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 (e.g., List). 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 10-1 documents some of the more commonly used collection classes of this namespace and the core interfaces they implement.

Table 10-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 10-2 documents the overall nature of these key interfaces, some of which you worked with firsthand in Chapter 8.

Table 10-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 will provide some further details on their differences when you examine their generic counterparts a bit later in this chapter. Until then, here is example code using an ArrayList object:

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

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

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

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

Notice that you can add (or remove) items on the fly and the container automatically resizes itself accordingly.
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 Core documentation for full details. On a related note, the other classes of System.Collections (Stack, Queue, etc.) are also fully documented in the
.NET Core help system.
However, it is important to point out that a majority of your .NET Core 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 the System.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 Core namespace that contains nongeneric collection classes. The System.Collections.Specialized namespace defines a number of (pardon the redundancy) specialized collection types. Table 10-3 documents some of the more useful types in this particular collection-centric namespace, all of which are nongeneric.

Table 10-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.

■ Note there are two additional collection-centric namespaces (System.Collections.ObjectModel and System.Collections.Concurrent) in the .net Core 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 15 for information on multithreading).

The Problems of Nongeneric Collections
While it is true that many successful .NET and .NET Core applications have been built over the years using these nongeneric collection classes (and interfaces), history has shown that using 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 CoreCLR 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 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 intended 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 the Program.cs file and clear out the rest of the code.

using System.Collections;

The Issue of Performance
As you might recall from Chapter 4, the .NET Core platform supports two broad categories of data: value types and reference types. Given that .NET Core 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 CoreCLR 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 CoreCLR 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 the boxedInt 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 assembly hidebysig static
void ‘<

$>g SimpleBoxUnboxOperation|0_0′() cil managed
{
.maxstack 1
.locals init (int32 V_0, object V_1, int32 V_2) IL_0000: nop
IL_0001: ldc.i4.s 25 IL_0003: stloc.0 IL_0004: ldloc.0
IL_0005: box [System.Runtime]System.Int32 IL_000a: stloc.1
IL_000b: ldloc.1
IL_000c: unbox.any [System.Runtime]System.Int32 IL_0011: stloc.2
IL_0012: ret
} // end of method ‘$’::'<
$>g SimpleBoxUnboxOperation|0_0′

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 unboxedLong = (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 CoreCLR takes care of the memory- related details on your behalf.

Let’s look at a practical use of these techniques. We will examine the System.Collections.ArrayList class and use it to hold onto a batch of numeric (stack-allocated) data. The relevant members of the ArrayList class are listed as follows. Notice that 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 : IList, 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 these 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 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.

namespace IssuesWithNonGenericCollections; 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 $"Name: {FirstName} {LastName}, Age: {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 on System.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):

using System.Collections;
namespace IssuesWithNonGenericCollections; public class PersonCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList();

// Cast for caller.
public Person GetPerson(int pos) => (Person)arPeople[pos];

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

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

public int Count => arPeople.Count;

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator() => 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, nor 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. Update the using statements in Program.cs to the following and add the UserPersonCollection() method to the end of your current code:

using System.Collections;
using IssuesWithNonGenericCollections;
//Top level statements in Program.cs 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 the Car base class, you need to build a highly similar collection class.

using System.Collections;
public class CarCollection : IEnumerable
{
private ArrayList arCars = new ArrayList();

// Cast for caller.
public Car GetCar(int pos) => (Car) arCars[pos];

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

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

public int Count => arCars.Count;

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator() => 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).

using System.Collections;
public class IntCollection : IEnumerable
{
private ArrayList arInts = new ArrayList();

// Get an int (performs unboxing!).
public int GetInt(int pos) => (int)arInts[pos];

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

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

public int Count => arInts.Count;

IEnumerator IEnumerable.GetEnumerator() => 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 CollectionsT
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 (added to the bottom of Program.cs), which uses the generic List 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 morePeople = new List(); morePeople.Add(new Person ("Frank", "Black", 50)); Console.WriteLine(morePeople[0]);

// This List<> can hold only integers. List moreInts = new List(); 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 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 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.

The Role of Generic Type Parameters
You can find generic classes, interfaces, structures, and delegates throughout the .NET Core base class libraries, and these might be part of any .NET Core 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.

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

When you see a generic item listed in the .NET Core documentation or the Visual Studio Object Browser, you will notice a pair of angled brackets with a letter or other token sandwiched within. Figure 10-1 shows the Visual Studio Object Browser displaying a number of generic items located within the System.
Collections.Generic namespace, including the highlighted List class.

Figure 10-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 as “of T.” Thus, you can read IEnumerable as “IEnumerable of T” or, to say it another way, “IEnumerable of type T.”

■ 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 objects.

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

You can read the first line in 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 class using the Visual Studio Object Browser, you would see that the placeholder T is used throughout the definition of the List type. Here is a partial listing:

// A partial listing of the List class. namespace System.Collections.Generic;
public class List : IList, IList, IReadOnlyList
{

public void Add(T item);
public void AddRange(IEnumerable collection); public ReadOnlyCollection AsReadOnly(); public int BinarySearch(T item);
public bool Contains(T item); public void CopyTo(T[] array);
public int FindIndex(System.Predicate match); public T FindLast(System.Predicate match); public bool Remove(T item);
public int RemoveAll(System.Predicate match); public T[] ToArray();
public bool TrueForAll(System.Predicate match); public T this[int index] { get; set; }
}

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

namespace System.Collections.Generic; public class List
: IList, IList, IReadOnlyList
{

public void Add(Person item);
public void AddRange(IEnumerable collection); public ReadOnlyCollection AsReadOnly(); public int BinarySearch(Person item);
public bool Contains(Person item); public void CopyTo(Person[] array);
public int FindIndex(System.Predicate match); public Person FindLast(System.Predicate match); public bool Remove(Person item);
public int RemoveAll(System.Predicate match); public Person[] ToArray();

public bool TrueForAll(System.Predicate match); public Person this[int index] { get; set; }
}

Of course, when you create a generic List variable, the compiler does not literally create a new implementation of the List 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 generic 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 several generic methods. Specifically, the nongeneric static Sort() method now has a generic counterpart named Sort(). 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(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)
{
if (obj is Car temp)

{
return this.CarID.CompareTo(temp.CarID);
}
throw new ArgumentException("Parameter is not a Car!");
}
}

Now assume you use the generic counterpart of this interface.

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

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

public class Car : IComparable
{

// IComparable implementation.
int IComparable.CompareTo(Car obj)
{
if (this.CarID > obj.CarID)
{
return 1;
}
if (this.CarID < obj.CarID)
{
return -1;
}
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 (aka 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 Core 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 extends IEnumerable. Table 10-4 documents the core generic interfaces you’ll encounter when working with the generic collection classes.

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

System.Collections.Generic
Interface Meaning in Life
ICollection Defines general characteristics (e.g., size, enumeration, and thread safety) for all generic collection types.
IComparer Defines a way to compare to objects.
IDictionary<TKey, TValue> Allows a generic collection object to represent its contents using key- value pairs.
IEnumerable/ IAsyncEnumerable Returns the IEnumerator interface for a given object.
IAsyncEnumerable (new in C# 8.0) is covered in Chapter 15.
IEnumerator Enables foreach-style iteration over a generic collection.
IList Provides behavior to add, remove, and index items in a sequential list of objects.
ISet 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 10-5 describes some commonly used classes of this namespace, the interfaces they implement, and their basic functionality.

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

Generic Class Supported Key Interfaces Meaning in Life
Dictionary<TKey, TValue> ICollection, IDictionary<TKey, TValue>, IEnumerable This represents a generic collection of keys and values.
LinkedList ICollection, IEnumerable This represents a doubly linked list.
List ICollection, IEnumerable, IList This is a dynamically resizable sequential list of items.
Queue ICollection (not a typo; this is the nongeneric collection interface), IEnumerable This is a generic implementation of a first-in, first-out list.
SortedDictionary<TKey, TValue> ICollection, IDictionary<TKey, TValue>, IEnumerable This is a generic implementation of a sorted set of key-value pairs.
SortedSet ICollection, IEnumerable, ISet This represents a collection of objects that is maintained in sorted order with no duplication.
Stack ICollection (not a typo; this is the nongeneric collection interface), IEnumerable This is a generic implementation of a last-in, first-out 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 type represents a node within a generic LinkedList, the KeyNotFoundException exception is raised when attempting to grab an

item from a container using a nonexistent key, and so forth. Be sure to consult the .NET Core 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 ArrayList or List) with items by using syntax similar to what you use to populate a basic array. Create a new .NET Core Console application named FunWithCollectionInitialization. Clear out the generated code in Program.cs and add the following using statements:

using System.Collections; using System.Drawing;

■ Note You can apply collection initialization syntax only to classes that support an Add() method, which is formalized by the ICollection/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 myGenericList = new List { 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 of Point objects, you could write the following:

List myListOfPoints = new List
{
new Point { X = 2, Y = 2 }, new Point { X = 3, Y = 3 }, new Point { 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 of Rectangles if you did not have collection initialization syntax.

List myListOfRects = new List
{
new Rectangle {
Height = 90, Width = 90,
Location = new Point { X = 10, Y = 10 }}, new Rectangle {
Height = 50,Width = 50,
Location = new Point { X = 2, Y = 2 }},
};
foreach (var r in myListOfRects)
{
Console.WriteLine(r);
}

Working with the List Class
Create a new Console Application project named FunWithGenericCollections. Add a new file, named
Person.cs, and add the following code (which is the same code as the previous Person class):

namespace FunWithGenericCollections; 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 $"Name: {FirstName} {LastName}, Age: {Age}";
}
}

Clear out the generated code in Program.cs and add the following using statement:

using FunWithGenericCollections;

The first generic class you will examine is List, which you’ve already seen once or twice in this chapter. The List 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.cs file, which leverages List 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 people = new List()
{
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(); foreach (Person p in arrayOfPeople)
{
Console.WriteLine("First Names: {0}", p.FirstName);
}
}

Here, you use collection initialization syntax to populate your List 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 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. From this array, you loop over the items again using the array’s indexer syntax. If you call this method from your top-level statements, 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 class defines many additional members of interest, so be sure to consult the documentation for more information. Next, let’s look at a few more generic collections, specifically Stack, Queue, and SortedSet. 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 Class
The Stack class represents a collection that maintains items using a last-in, first-out manner. As you might expect, Stack 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 stackOfPeople = new();
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() and Peek() 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 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 class fits the bill. In addition to the functionality provided by the supported interfaces, Queue defines the key members shown in Table 10-6.

Table 10-6. Members of the Queue Type

Select Member of Queue Meaning in Life
Dequeue() Removes and returns the object at the beginning of the Queue
Enqueue() Adds an object to the end of the Queue
Peek() Returns the object at the beginning of the Queue without removing it

Now let’s put these methods to work. You can begin by leveraging your Person class again and building a Queue object that simulates a line of people waiting to order coffee.

static void UseGenericQueue()
{
// Make a Q with three people. Queue peopleQ = new();
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);
}
//Local helper function
static void GetCoffee(Person p)
{
Console.WriteLine("{0} got coffee!", p.FirstName);
}
}

Here, you insert three items into the Queue 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 PriorityQueue<TElement, TPriority> Class (New 10)
Introduced in .NET 6/C# 10, the PriorityQueue works just like the Queue except that each queued item is given a priority. When items are dequeued, they are removed from lowest to highest priority. The following updates the previous Queue example to use a PriorityQueue:

static void UsePriorityQueue()
{
Console.WriteLine(" Fun with Generic Priority Queues \n");

PriorityQueue<Person, int> peopleQ = new();
peopleQ.Enqueue(new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 }, 1); peopleQ.Enqueue(new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 }, 3); peopleQ.Enqueue(new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 }, 3); peopleQ.Enqueue(new Person { FirstName = "Bart", LastName = "Simpson", Age = 12 }, 2);

while (peopleQ.Count > 0)
{
Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays Lisa Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays Bart Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays either Marge or Homer Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays the other priority 3 item
}
}

If more than one item is set to the current lowest priority, the order of dequeuing is not guaranteed. As shown in code sample, the third call to Dequeue() will return either Homer or Marge, as they are both set to a priority of three. The fourth call will then return the other person. If exact order matters, you must ensure values for each priority are unique.

Working with the SortedSet Class
The SortedSet 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 class exactly how you
want it to sort the objects, by passing in as a constructor argument an object that implements the generic
IComparer interface.
Begin by creating a new class named SortPeopleByAge, which implements IComparer, 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:

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

Now add the following new method that demonstrates using SortedSet:

static void UseSortedSet()
{
// Make some people with different ages.
SortedSet setOfPeople = new SortedSet(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 using a numerical identifier (e.g., “Give me the second object”), you could use the unique text key (e.g., “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. In this example, you are using a string data type as the key and
a Person type as the value. Also note that you can combine object initialization syntax with collection initialization syntax.

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 that is specific to this type of container (not surprisingly termed dictionary initialization). Similar to the syntax used to populate the personB 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 }
};

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

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

System.Collections.
ObjectModel Type Meaning in Life
ObservableCollection Represents a dynamic data collection that provides notifications when items get added, when items get removed, or when the whole list is refreshed
ReadOnlyObservable Collection Represents a read-only version of ObservableCollection

The ObservableCollection 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

is similar but read-only in nature).

Working with ObservableCollection
Create a new Console Application project named FunWithObservableCollections and import the System.Collections.ObjectModel namespace into your initial C# code file. In many ways, working with ObservableCollection is identical to working with List, given that both of these classes implement the same core interfaces. What makes the ObservableCollection 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 code, which populates an observable collection containing Person objects and wires up the CollectionChanged event:

using System.Collections.ObjectModel; using System.Collections.Specialized; using FunWithObservableCollections;

// Make a collection to observe
//and add a few Person objects.
ObservableCollection people = new ObservableCollection()
{
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 of NotifyCollectionChangedEventArgs. 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 into or removed from the collection at hand (notice the using for System.Collections.Specialized):

using System.Collections.Specialized;

static void people_CollectionChanged(object sender, 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 == 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 == 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, update your calling code to add and remove an item.

// Now add a new item.
people.Add(new Person(“Fred”, “Smith”, 32));
// Remove an item. people.RemoveAt(0);

When you run the program, 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. To conclude the chapter, you will now examine how you can build your own custom generic methods and custom generic types.

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 static class with a method that can operate on integers, like this:

namespace CustomGenericMethods; static class SwapFunctions
{
// Swap two integers.
static void Swap(ref int a, ref int b)
{
int 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 = 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, etc., you would have to build even more methods, which would become a maintenance nightmare. You could build a single (nongeneric) method that operated on object 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() method that can swap any two Ts:

// This method will swap any two items.
// as specified by the type parameter . static void Swap(ref T a, ref T b)
{
Console.WriteLine(“You sent the Swap() method a {0}”, typeof(T)); T 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() method can operate on any two parameters of type . 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 calling code, which swaps integers and strings:

Console.WriteLine(“***** Fun with Custom Generic Methods *****\n”);

// Swap 2 ints.
int a = 10, b = 90;
Console.WriteLine(“Before swap: {0}, {1}”, a, b); SwapFunctions.Swap(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); SwapFunctions.Swap(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() 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, 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 two System.Boolean values by adding the following code to your top-level statements:

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

Even though the compiler can 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.

SwapFunctions.Swap(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.cs file:

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

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


// Must supply type parameter if
// the method does not take params. DisplayBaseClass(); DisplayBaseClass();

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

Of course, generic methods do not need to be static as they are in these examples. All rules and options for nongeneric methods also apply.

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 generic Point structure that supports a single type parameter that represents the underlying storage for the (x, y) coordinates. The caller can then create Point types as follows:

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

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

// Point using strings.
Point p3 = new Point(“””,””3″”);

Creating a point using strings might seem a bit odd at first, but consider the case of imaginary numbers. Then it might make sense to use strings for the values of X and Y of a point. Regardless, it demonstrates the power of generics. Here is the complete definition of Point:

namespace GenericPoint;
// A generic Point structure. public struct Point

{
// Generic state data. private T _xPos; private T _yPos;

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

// Generic properties. public T X
{
get => _xPos;
set => _xPos = value;
}

public T Y
{
get => _yPos;
set => _yPos = value;
}

public override string ToString() => $”[{_xPos}, {_yPos}]”;
}

As you can see, Point leverages its type parameter in the definition of the field data, constructor arguments, and property definitions.

Default Value Expressions with Generics
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 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).
To reset an instance of Point, you could set the X and Y values to 0 directly. This assumes the caller will supply only numerical data. What about the string version? This is where the default(T) syntax comes in handy. The default keyword resets a variable to the default value for the variable’s data type. Add a method called ResetPoint() as follows:

// Reset fields to the default value of the type parameter.
// The “default” keyword is overloaded in C#.
// When used with generics, it represents the default

// value of a type parameter. public void ResetPoint()
{
_xPos = default(T);
_yPos = default(T);
}

Now that you have the ResetPoint() method in place, you can fully exercise the methods of
Point struct. using GenericPoint;
Console.WriteLine(“***** Fun with Generic Structures *****\n”);
// Point using ints.
Point p = new Point(10, 10); Console.WriteLine(“p.ToString()={0}”, p.ToString()); p.ResetPoint(); Console.WriteLine(“p.ToString()={0}”, p.ToString()); Console.WriteLine();

// Point using double.
Point p2 = new Point(5.4, 3.3); Console.WriteLine(“p2.ToString()={0}”, p2.ToString()); p2.ResetPoint(); Console.WriteLine(“p2.ToString()={0}”, p2.ToString()); Console.WriteLine();

// Point using strings.
Point p3 = new Point(“i”, “3i”); Console.WriteLine(“p3.ToString()={0}”, p3.ToString()); p3.ResetPoint(); Console.WriteLine(“p3.ToString()={0}”, p3.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]

p3.ToString()=[i, 3i] p3.ToString()=[, ]

Default Literal Expressions (New 7.1)
In addition to setting the default value of a property, C# 7.1 introduced default literal expressions. This eliminates the need for specifying the type of the variable in the default statement. Update the ResetPoint() method to the following:

public void ResetPoint()
{
_xPos = default;
_yPos = default;
}

The default expression isn’t limited to simple variables but can also be applied to complex types. For example, to create and initialize the Point structure, you can write the following:

Point p4 = default; Console.WriteLine(“p4.ToString()={0}”, p4.ToString()); Console.WriteLine();
Point p5 = default; Console.WriteLine(“p5.ToString()={0}”, p5.ToString());

Pattern Matching with Generics (New 7.1)
Another update in C# 7.1 is the ability to pattern match on generics. Take the following method, which checks the Point instance for the data type that it is based on (arguably incomplete, but enough to show the concept):

static void PatternMatching(Point p)
{
switch (p)
{
case Point pString: Console.WriteLine(“Point is based on strings”); return;
case Point pInt:
Console.WriteLine(“Point is based on ints”); return;
}
}

To exercise the pattern matching code, update the top-level statements to the following:

Point p4 = default; Point p5 = default; PatternMatching(p4); PatternMatching(p5);

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, you can also 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 10-8.

Table 10-8. Possible Constraints for Generic Type Parameters

Generic Constraint Meaning in Life
where T : struct The type parameter must have System.ValueType in its chain of inheritance (i.e.,
must be a structure).
where T : class The type parameter must not have System.ValueType in its chain of inheritance (i.e., must be a reference type).
where T : new() The type parameter 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 must be derived from the class specified by
NameOfBaseClass.
where T : NameOfInterface The type parameter 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 of 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 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 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 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.

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

}

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() method can operate only on structures, you will update the method like this:

// This method will swap any structure, but not classes. static void Swap(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 ends. 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
{
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 with the binary operators of C#. However, for the sake of argument, if 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 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 12, I will pick up the topic of generics once again while examining the 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, such as 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) 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!

发表评论