Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]-1

.pdf
Скачиваний:
23
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

312 CHAPTER 10 COLLECTIONS AND GENERICS

int Add(object value); void Clear();

bool Contains(object value); int IndexOf(object value);

void Insert(int index, object value); void Remove(object value);

void RemoveAt(int index);

}

The Class Types of System.Collections

As explained in the previous chapter, interfaces by themselves are not very useful until they are implemented by a given class or structure. Table 10-2 provides a rundown of the core classes in the System.Collections namespace and the key interfaces they support.

Table 10-2. Classes of System.Collections

System.Collections Class

Meaning in Life

Key Implemented Interfaces

ArrayList

Represents a dynamically sized array

IList, ICollection,

 

of objects.

IEnumerable, and

 

 

ICloneable

Hashtable

Represents a collection of objects

IDictionary, ICollection,

 

identified by a numerical key.

IEnumerable, and

 

Custom types stored in a Hashtable

ICloneable

 

should always override System.

 

 

Object.GetHashCode().

 

Queue

Represents a standard first-in,

ICollection, ICloneable,

 

first-out (FIFO) queue.

and IEnumerable

SortedList

Like a dictionary; however, the

IDictionary, ICollection,

 

elements can also be accessed

IEnumerable, and

 

by ordinal position (e.g., index).

ICloneable

Stack

A last-in, first-out (LIFO) queue

ICollection, ICloneable,

 

providing push and pop (and peek)

and IEnumerable

 

functionality.

 

 

 

 

In addition to these key types, System.Collections defines some minor players (at least in terms of their day-to-day usefulness) such as BitArray, CaseInsensitiveComparer, and CaseInsensitiveHashCodeProvider. Furthermore, this namespace also defines a small set of abstract base classes (CollectionBase, ReadOnlyCollectionBase, and DictionaryBase) that can be used to build strongly typed containers.

As you begin to experiment with the System.Collections types, you will find they all tend to share common functionality (that’s the point of interface-based programming). Thus, rather than listing out the members of each and every collection class, the next task of this chapter is to illustrate how to interact with three common collection types: ArrayList, Queue, and Stack.

Once you understand the functionality of these types, gaining an understanding of the remaining collection classes (such as the Hashtable) should naturally follow; especially since each of the types is fully documented within the .NET Framework 3.5 documentation.

CHAPTER 10 COLLECTIONS AND GENERICS

313

Working with the ArrayList Type

To illustrate working with these collection types, create a new Console Application project named CollectionTypes. Our ArrayList will maintain a set of simple Car objects, defined as follows:

class Car

{

//Public fields for simplicity. public string PetName;

public int Speed;

//Constructors.

public Car(){}

public Car(string name, int currentSpeed) { PetName = name; Speed = currentSpeed;}

}

Next, update your project’s initial C# file to specify you are using the System.Collections namespace:

using System.Collections;

The ArrayList type is bound to be your most frequently used type in the System.Collections namespace in that it allows you to dynamically resize the contents at your whim. To illustrate the basics of this type, ponder the following method to your Program class, which leverages the ArrayList to manipulate a set of Car objects:

static void ArrayListTest()

{

Console.WriteLine("\n=> ArrayList Test:\n");

//Create ArrayList and fill with some initial values.

ArrayList carArList = new ArrayList(); carArList.AddRange(new Car[] { new Car("Fred", 90, 10),

new Car("Mary", 100, 50), new Car("MB", 190, 11)});

//Print out # of items in ArrayList.

Console.WriteLine("Items in carArList: {0}", carArList.Count);

//Print out current values.

foreach(Car c in carArList)

Console.WriteLine("Car pet name: {0}", c.PetName);

// Insert a new item.

Console.WriteLine("->Inserting new Car."); carArList.Insert(2, new Car("TheNewCar", 0, 12)); Console.WriteLine("Items in carArList: {0}", carArList.Count);

// Get object array from ArrayList and print again. object[] arrayOfCars = carArList.ToArray(); for(int i = 0; i < arrayOfCars.Length; i++)

{

Console.WriteLine("Car pet name: {0}", ((Car)arrayOfCars[i]).PetName);

}

}

314 CHAPTER 10 COLLECTIONS AND GENERICS

Here you are making use of the AddRange() method to populate your ArrayList with an array of Car types (which is basically a shorthand notation for calling Add() n number of times). Once you print out the number of items in the collection (as well as enumerate over each item to obtain the pet name), you invoke Insert(). As you can see, Insert() allows you to plug a new item into the ArrayList at a specified index.

Finally, notice the call to the ToArray() method, which returns an array of System.Object types based on the contents of the original ArrayList. From this array, we loop over the items once again using the array’s indexer syntax. If you call this method from within Main(), you will find the ArrayList has indeed grown by one item to account for the new Car object.

Working with the Queue Type

Queues are containers that ensure items are accessed using 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 are modeling a scenario in which items are handled on a firstcome, first-served basis, System.Collections.Queue fits the bill. In addition to the functionality provided by the supported interfaces, Queue defines the key members shown in Table 10-3.

Table 10-3. Members of the Queue Type

Member of System.Collection.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

 

 

To illustrate these methods, we will leverage our automobile theme once again and build a Queue object that simulates a line of cars waiting to enter a car wash. First, assume the following static helper method:

static void WashCar(Car c)

{

Console.WriteLine("Cleaning {0}", c.PetName);

}

Now assume this additional helper method, which calls WashCar() internally:

static void QueueTest()

{

Console.WriteLine("\n=> Queue Test:\n");

//Make a Q with three items.

Queue carWashQ = new Queue(); carWashQ.Enqueue(new Car("FirstCar", 10)); carWashQ.Enqueue(new Car("SecondCar", 20)); carWashQ.Enqueue(new Car("ThirdCar", 30));

//Peek at first car in Q.

Console.WriteLine("First in Q is {0}", ((Car)carWashQ.Peek()).PetName);

//Remove each item from Q.

WashCar((Car)carWashQ.Dequeue());

CHAPTER 10 COLLECTIONS AND GENERICS

315

WashCar((Car)carWashQ.Dequeue());

WashCar((Car)carWashQ.Dequeue());

// Try to de-Q again? try

{WashCar((Car)carWashQ.Dequeue()); } catch(Exception e)

{Console.WriteLine("Error!! {0}", e.Message);}

}

Here, you insert three items into the Queue type via its Enqueue() method. The call to Peek() allows you to view (but not remove) the first item currently in the Queue, which in this case is the object named FirstCar. Finally, the call to Dequeue() removes the item from the line and sends it into the WashCar() helper function for processing. Do note that if you attempt to remove items from an empty queue, a runtime exception is thrown.

Working with the Stack Type

The System.Collections.Stack type represents a collection that maintains items using a last-in, first-out manner. As you would expect, Stack defines a member named Push() and Pop() (to place items onto or remove items from the stack). The following stack example makes use of the standard

System.String:

static void StackTest()

{

Console.WriteLine("\n=> Stack Test:\n"); Stack stringStack = new Stack(); stringStack.Push("One"); stringStack.Push("Two"); stringStack.Push("Three");

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

Console.WriteLine("Top item is: {0}", stringStack.Peek()); Console.WriteLine("Popped off {0}", stringStack.Pop()); Console.WriteLine("Top item is: {0}", stringStack.Peek()); Console.WriteLine("Popped off {0}", stringStack.Pop()); Console.WriteLine("Top item is: {0}", stringStack.Peek()); Console.WriteLine("Popped off {0}", stringStack.Pop());

try

{

Console.WriteLine("Top item is: {0}", stringStack.Peek()); Console.WriteLine("Popped off {0}", stringStack.Pop());

}

catch(Exception e)

{ Console.WriteLine("Error!! {0}", e.Message);}

}

Here, you build a stack that contains three string types (named according to their order of insertion). As you peek into the stack, you will always see the item at the very top, and therefore the first call to Peek() reveals the third string. After a series of Pop() and Peek() calls, the stack is eventually empty, at which time additional Peek()/Pop() calls raise a system exception.

Source Code The CollectionTypes project can be found under the Chapter 10 subdirectory.

316 CHAPTER 10 COLLECTIONS AND GENERICS

System.Collections.Specialized Namespace

In addition to the types defined within the System.Collections namespace, you should also be aware that the .NET base class libraries provide the System.Collections.Specialized namespace defined in the System.dll assembly, which defines another set of types that are more (pardon the redundancy) specialized. For example, the StringDictionary and ListDictionary types each provide a stylized implementation of the IDictionary interface. Table 10-4 documents the key class types.

Table 10-4. Types of the System.Collections.Specialized Namespace

Member of System.Collections.Specialized

Meaning in Life

BitVector32

A simple structure that stores Boolean values and

 

small integers in 32 bits of memory.

CollectionsUtil

Creates collections that ignore the case in strings.

HybridDictionary

Implements IDictionary by using a

 

ListDictionary while the collection is small, and

 

then switching to a Hashtable when the collection

 

gets large.

ListDictionary

Implements IDictionary using a singly linked list.

 

Recommended for collections that typically

 

contain ten items or fewer.

NameValueCollection

Represents a sorted collection of associated

 

String keys and String values that can be

 

accessed either with the key or with the index.

StringCollection

Represents a collection of strings.

StringDictionary

Implements a hashtable with the key strongly

 

typed to be a string rather than an object.

StringEnumerator

Supports a simple iteration over a

 

StringCollection.

 

 

Now that you have had a chance to examine some of the basic collection types within the

System.Collections (and System.Collections.Specialized) namespace, you might be surprised when I tell you that these types are basically regarded as legacy types that should not be used for new project developments for .NET 2.0 or higher. The reason is not because these types are somehow dangerous, but due to the fact that they suffer from performance issues and a lack of type safety.

New projects should ignore these legacy container types in favor of related types in the System.Collections.Generic namespace. However, before we examine how to make use of generic types, it is very helpful to understand exactly what problems generics intend to solve in the first place. To begin, we must examine the role of boxing and unboxing.

The Boxing, Unboxing, and System.Object

Relationship

As you recall from Chapter 4, the .NET platform supports two broad groups of data types, termed value types and reference types. Given that .NET defines two major categories of types, you may occasionally need to represent a variable of one category as a variable of the other category. To do

CHAPTER 10 COLLECTIONS AND GENERICS

317

so, C# provides a very simple mechanism, termed boxing, to convert a value type to a reference type. Assume that you have created a variable of type short:

// Make a short value type. short s = 25;

If during the course of your application you wish to represent this value type as a reference type, you would box the value as follows:

// Box the value into an object reference. object objShort = s;

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

.NET developers have no need to make use of a set of wrapper classes used to temporarily treat stack data as heap-allocated objects.

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. The unboxing operation 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 objShort is indeed a short:

// Unbox the reference back into a corresponding short. short anotherShort = (short)objShort;

Again, it is mandatory that you unbox into an appropriate data type. Thus, the following unboxing logic generates an InvalidCastException exception:

// Illegal unboxing.

static void Main(string[] args)

{

short s = 25; object objShort = s;

try

{

// The type contained in the box is NOT an int, but a short! int i = (int)objShort;

}

catch(InvalidCastException e)

{

Console.WriteLine("OOPS!\n{0} ", e.ToString());

}

}

At first glance, boxing/unboxing may seem like a rather uneventful language feature that is more academic than practical. In reality, the (un)boxing process is very helpful in that it allows us to assume everything can be treated as a System.Object, while the CLR takes care of the memoryrelated details on our behalf.

To see a practical use of this technique, assume you have created a System.Collections. ArrayList to hold numeric (stack-allocated) data. If you were to examine the members of ArrayList, you would find they are typically prototyped to receive and return System.Object types:

public class System.Collections.ArrayList : object, System.Collections.IList, System.Collections.ICollection,

318 CHAPTER 10 COLLECTIONS AND GENERICS

System.Collections.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; }

}

However, rather than forcing programmers to manually wrap the stack-based integer in a related object wrapper, the runtime will automatically do so via a boxing operation:

static void Main(string[] args)

{

//Value types are automatically boxed when

//passed to a member requesting an object.

ArrayList myInts = new ArrayList(); myInts.Add(10); Console.ReadLine();

}

If you wish to retrieve this value from the ArrayList object using the type indexer, you must unbox the heap-allocated object into a stack-allocated integer using a casting operation:

static void Main(string[] args)

{

...

//Value is now unboxed. int i = (int)myInts[0];

//Now it is reboxed, as WriteLine() requires object types!

Console.WriteLine("Value of your int: {0}", i); Console.ReadLine();

}

When the C# compiler transforms a boxing operation into terms of CIL code, you find the box opcode is used internally. Likewise, the unboxing operation is transformed into a CIL unbox operation. Here is the relevant CIL code for the previous Main() method (which can be viewed using ildasm.exe):

.method private hidebysig static void Main(string[] args) cil managed

{

...

box [mscorlib]System.Int32

callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) pop

ldstr "Value of your int: {0}" ldloc.0

ldc.i4.0

callvirt instance object [mscorlib] System.Collections.ArrayList::get_Item(int32)

unbox [mscorlib]System.Int32 ldind.i4

box [mscorlib]System.Int32

call void [mscorlib]System.Console::WriteLine(string, object)

...

}

CHAPTER 10 COLLECTIONS AND GENERICS

319

Note that the stack-allocated System.Int32 is boxed prior to the call to ArrayList.Add() in order to pass in the required System.Object. Also note that the System.Object is unboxed back into a System.Int32 once retrieved from the ArrayList using the type indexer (which maps to the hidden get_Item() method), only to be boxed again when it is passed to the Console.WriteLine() method, as this method is operating on System.Object types.

The Problem with (Un)Boxing Operations

Although boxing and unboxing are very convenient from a programmer’s point of view, 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 the current Main() 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 are manipulated by your program on a somewhat regular basis.

Now consider the lack of type safety regarding unboxing operations. As previously explained, to unbox a value using the syntax of C#, you make use of the casting operator. However, the success or failure of a cast is not known until runtime. Therefore, if you attempt to unbox a value into the wrong data type, you receive an InvalidCastException:

static void Main(string[] args)

{

ArrayList myInts = new ArrayList(); myInts.Add(10);

//Runtime exception! short i = (int)myInts[0];

//Now it is reboxed as WriteLine() requires object types!

Console.WriteLine("Value of your int: {0}", i); Console.ReadLine();

}

In an ideal world, the C# compiler would be able to resolve these illegal unboxing operations at compile time, rather than at runtime. On a related note, in a really ideal world, we could store sets of value types in a container that did not require boxing in the first place. Generics are the solution to each of these issues.

The Issue of Type Safety and Strongly Typed

Collections

The final major collection-centric issue we have in a generic-free programming world is the fact that a majority of the types of System.Collections can typically hold anything whatsoever, as their members are prototyped to operate on System.Objects:

320CHAPTER 10 COLLECTIONS AND GENERICS

static void Main(string[] args)

{

//The ArrayList can hold anything at all.

ArrayList allMyObject = new ArrayList(); allMyObjects.Add(true); allMyObjects.Add(new Car());

allMyObjects.Add(66);

allMyObjects.Add(3.14);

}

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

Building a Custom Collection

Prior to the introduction of generics in .NET 2.0, programmers attempted to address type safety by manually building custom strongly typed collections. To illustrate why this can be problematic, create a new Console Application project named CustomNonGenericCollection. Once you have done so, be sure you import the System.Collections namespace. Now, assume you wish to create a custom collection that can only contain objects of type Person:

public class Person

{

// Made public for simplicity. public int Age;

public string FirstName, LastName;

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 person collection, you could define a System.Collections.ArrayList member variable within a class named PeopleCollection and configure all members to operate on strongly typed Person objects, rather than on System.Object types:

public class PeopleCollection : IEnumerable

{

private ArrayList arPeople = new ArrayList(); public PeopleCollection(){}

// Cast for caller.

public Person GetPerson(int pos) { return (Person)arPeople[pos]; }

CHAPTER 10 COLLECTIONS AND GENERICS

321

// Only insert Person types. 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 PeopleCollection type implements the IEnumerable interface, to allow foreach- like iteration over each contained item. Also notice that our GetPerson() and AddPerson() method has been prototyped to only operate 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 type:

static void Main(string[] args)

{

Console.WriteLine("***** Custom Person Collection *****\n");

PeopleCollection myPeople = new PeopleCollection(); 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);

Console.ReadLine();

}

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 type you wish to contain. Thus, if you need a custom collection that will be able to operate only on classes deriving from the Car base class, you need to build a very similar type:

public class CarCollection : IEnumerable

{

private ArrayList arCars = new ArrayList(); public CarCollection(){}

//Cast for caller. public Car GetCar(int pos)

{ return (Car) arCars[pos]; }

//Only insert Car types. public void AddCar(Car c) { arCars.Add(c); }