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

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

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

302 CHAPTER 9 WORKING WITH INTERFACES

foreach(Car c in myAutos) Console.WriteLine("{0} {1}", c.ID, c.PetName);

//Now, sort them using IComparable!

Array.Sort(myAutos);

Console.WriteLine();

//Display sorted array.

Console.WriteLine("Here is the ordered set of cars:"); foreach(Car c in myAutos)

Console.WriteLine("{0} {1}", c.ID, c.PetName); Console.ReadLine();

}

Figure 9-13 illustrates a test run.

Figure 9-13. Comparing automobiles based on car ID

Specifying Multiple Sort Orders (IComparer)

In this version of the Car type, you made use of the car’s ID to function as the baseline of the sort order. Another design might have used the pet name of the car as the basis of the sorting algorithm (to list cars alphabetically). Now, what if you wanted to build a Car that could be sorted by ID as well as by pet name? If this is the behavior you are interested in, you need to make friends with another standard interface named IComparer, defined within the System.Collections namespace as follows:

// A generic way to compare two objects. interface IComparer

{

int Compare(object o1, object o2);

}

Unlike the IComparable interface, IComparer is typically not implemented on the type you are trying to sort (i.e., the Car). Rather, you implement this interface on any number of helper classes, one for each sort order (pet name, car ID, etc.). Currently, the Car type already knows how to compare itself against other cars based on the internal car ID. Therefore, allowing the object user to sort an array of Car types by pet name will require an additional helper class that implements IComparer. Here’s the code:

CHAPTER 9 WORKING WITH INTERFACES

303

// This helper class is used to sort an array of Cars by pet name. using System.Collections;

public class PetNameComparer : IComparer

{

// Test the pet name of each object.

int IComparer.Compare(object o1, object o2)

{

Car t1 = (Car)o1;

Car t2 = (Car)o2;

return String.Compare(t1.PetName, t2.PetName);

}

}

The object user code is able to make use of this helper class. System.Array has a number of overloaded Sort() methods, one that just happens to take an object implementing IComparer. Figure 9-14 shows the output of sorting by a car’s pet name.

static void Main(string[] args)

{

...

//Now sort by pet name.

Array.Sort(myAutos, new PetNameComparer());

//Dump sorted array.

Console.WriteLine("Ordering by pet name:"); foreach(Car c in myAutos)

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

...

}

Figure 9-14. Sorting automobiles by pet name

304 CHAPTER 9 WORKING WITH INTERFACES

Custom Properties, Custom Sort Types

It is worth pointing out that you can make use of a custom static property in order to help the object user along when sorting your Car types by a specific data point. Assume the Car class has added a static read-only property named SortByPetName that returns an instance of an object implementing the IComparer interface (PetNameComparer, in this case):

//We now support a custom property to return

//the correct IComparer interface.

public class Car : IComparable

{

...

// Property to return the SortByPetName comparer. public static IComparer SortByPetName

{ get { return (IComparer)new PetNameComparer(); } }

}

The object user code can now sort by pet name using a strongly associated property, rather than just “having to know” to use the stand-alone PetNameComparer class type:

// Sorting by pet name made a bit cleaner.

Array.Sort(myAutos, Car.SortByPetName);

Source Code The ComparableCar project is located under the Chapter 9 subdirectory.

Hopefully at this point, you not only understand how to define and implement interface types, but can understand their usefulness. To be sure, interfaces will be found within every major .NET namespace. To wrap up this chapter, let’s check out the interfaces that can be used to enable callback mechanisms.

Understanding Callback Interfaces

Beyond using interfaces to establish polymorphism across diverse class hierarchies, namespaces, and assemblies, interfaces may also be used as a callback mechanism. This technique enables objects to engage in a two-way conversation using a common set of members.

Note The .NET platform provides a formal fabric used to build events (which is quite different from the technique that will be shown here). As you will see in Chapter 11, delegates, events, and lambdas are the standard way to enable objects to chit-chat back and forth.

To illustrate the use of callback interfaces, let’s update the now familiar Car type in such a way that it is able to inform the caller when it is about to explode (the current speed is 10 miles below the maximum speed) and has exploded (the current speed is at or above the maximum speed).

Begin by creating a new Console Application named CallbackInterface. The ability to send and receive these events will be facilitated with a new custom interface named IEngineNotification:

// The callback interface.

public interface IEngineNotification

{

CHAPTER 9 WORKING WITH INTERFACES

305

void AboutToBlow(string msg); void Exploded(string msg);

}

Callback interfaces are often not implemented by the object directly interested in receiving the events, but rather by a helper object called a sink object. The sender of the events (the Car type in this case) will make calls on the sink under the appropriate circumstances. Assume the sink class is called CarEventSink. When this object is notified of the various incoming events, it will simply print out the incoming messages to the console. Furthermore, our sink will also maintain a string member variable that represents its friendly name (you’ll see how this can be useful as you move through the example):

// Car event sink.

public class CarEventSink : IEngineNotification

{

private string name; public CarEventSink(){}

public CarEventSink(string sinkName) { name = sinkName; }

public void AboutToBlow(string msg)

{

Console.WriteLine("{0} reporting: {1}", name, msg);

}

public void Exploded(string msg)

{

Console.WriteLine("{0} reporting: {1}", name, msg);

}

}

Now that you have a sink object that implements the callback interface, your next task is to pass a reference to this sink into the Car type. The Car holds onto the reference and makes calls back on the sink when appropriate. In order to allow the Car to obtain a reference to the sink, we will need to add a public helper member to the Car type that we will call Advise(). Likewise, if the caller wishes to detach from the event source, it may call another helper method on the Car type named Unadvise(). Finally, in order to allow the caller to register multiple event sinks (for the purposes of multicasting), the Car now maintains an ArrayList to represent each outstanding connection.

Note The ArrayList class is contained within the System.Collections namespace of the mscorlib.dll assembly. Be sure to import this namespace within the file containing your Car definition. Collections (and generics for that matter) will be examined in detail in Chapter 10.

//This Car and caller can now communicate

//using the IEngineNotification interface. public class Car

{

//The set of connected sinks.

ArrayList clientSinks = new ArrayList();

// Attach or disconnect from the source of events. public void Advise(IEngineNotification sink)

{

clientSinks.Add(sink);

}

306 CHAPTER 9 WORKING WITH INTERFACES

public void Unadvise(IEngineNotification sink)

{

clientSinks.Remove(sink);

}

...

}

To actually send the events, let’s update the Car.Accelerate() method to iterate over the list of connections maintained by the ArrayList and fire the correct notification when appropriate. First of all, add a new Boolean member variable named carIsDead to represent the engine’s state:

class Car

{

// Is the car alive or dead? bool carIsDead;

...

}

Next, update your current Accelerate() method to make use of this new member variable as follows:

public void Accelerate(int delta)

{

// If the car is dead, send Exploded event. if (carIsDead)

{

foreach (IEngineNotification sink in clientSinks) sink.Exploded("Sorry, this car is dead...");

}

else

{

currSpeed += delta;

// Almost dead?

if (10 == (maxSpeed – currSpeed))

{

foreach (IEngineNotification sink in clientSinks) sink.AboutToBlow("Careful buddy! Gonna blow!");

}

// Still OK!

if (currSpeed >= maxSpeed) carIsDead = true;

else

Console.WriteLine("->CurrSpeed = " + currSpeed);

}

}

With our infrastructure in place, we can now implement our Main() method to receive the events sent from the Car type as follows:

// Make a car and listen to the events. static void Main(string[] args)

{

Console.WriteLine("***** Interfaces as event enablers *****\n");

Car c1 = new Car("SlugBug", 100, 10);

// Make sink object.

CarEventSink sink = new CarEventSink();

CHAPTER 9 WORKING WITH INTERFACES

307

//Pass the Car a reference to the sink. c1.Advise(sink);

//Speed up (this will trigger the events). for(int i = 0; i < 10; i++)

c1.Accelerate(20);

//Detach from event source. c1.Unadvise(sink); Console.ReadLine();

}

Figure 9-15 shows the end result of this interface-based event protocol.

Figure 9-15. Receiving event notifications using callback interfaces

Do note that the Unadvise() method can be very helpful in that it allows the caller to selectively detach from an event source at will. Here, you call Unadvise() before exiting Main(), although this is not technically necessary. However, assume that the application now wishes to register two sinks, dynamically remove a particular sink during the flow of execution, and continue processing the program at large:

static void Main(string[] args)

{

Console.WriteLine("***** Interfaces as event enablers *****\n");

Car c1 = new Car("SlugBug", 100, 10);

//Make 2 sink objects.

Console.WriteLine("***** Creating sinks *****");

CarEventSink sink = new CarEventSink("First sink"); CarEventSink myOtherSink = new CarEventSink("Other sink");

//Hand sinks to Car.

Console.WriteLine("\n***** Sending 2 sinks into Car *****");

c1.Advise(sink);

c1.Advise(myOtherSink);

// Speed up (this will generate the events).

Console.WriteLine("\n***** Speeding up *****");

for(int i = 0; i < 10; i++) c1.Accelerate(20);

308CHAPTER 9 WORKING WITH INTERFACES

//Detach first sink from events.

Console.WriteLine("\n***** Removing first sink *****");

c1.Unadvise(sink);

//Speed up again (only myOtherSink will be called).

Console.WriteLine("\n***** Speeding up again *****");

for(int i = 0; i < 10; i++) c1.Accelerate(20);

//Detach other sink from events.

Console.WriteLine("\n***** Removing second sink *****");

c1.Unadvise(myOtherSink);

Console.ReadLine();

}

Callback interfaces can be helpful in that they can be used under any language or platform (.NET, J2EE, or otherwise) that supports interface-based programming. However, as mentioned, Chapter 11 will examine a number of event-centric techniques that are specific to the .NET platform.

Source Code The CallbackInterface project is located under the Chapter 9 subdirectory.

Summary

An interface can be defined as a named collection of abstract members. Because an interface does not provide any implementation details, it is common to regard an interface as a behavior that may be supported by a given type. When two or more classes implement the same interface, you are able to treat each type the same way (aka interface-based polymorphism) even if the types are defined within unique class hierarchies.

C# provides the interface keyword to allow you to define a new interface. As you have seen, a type can support as many interfaces as necessary using a comma-delimited list. Furthermore, it is permissible to build interfaces that derive from multiple base interfaces.

In addition to building your custom interfaces, the .NET libraries define a number of standard (i.e., framework-supplied) interfaces. As you have seen, you are free to build custom types that implement these predefined interfaces to gain a number of desirable traits such as cloning, sorting, and enumerating. Finally, you spent some time investigating how interface types can be used to establish bidirectional communications between two objects in memory.

C H A P T E R 1 0

Collections and Generics

The most primitive container within the .NET platform is the System.Array type. As you have seen over the course of the previous chapters, 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 types) of a fixed upper limit. While this will often fit the bill, there are many other times where you require

more flexible data structures, such as a dynamically growing and shrinking container, or a container that can hold only items that meet a specific criteria (e.g., only items deriving from a given base class, items implementing a particular interface, or whatnot). To begin understanding the task of building flexible and type-safe containers, this chapter will first examine the System.Collections namespace that has been part of the .NET base class libraries since the initial release.

However, since the release of .NET 2.0, the C# programming language was enhanced to support a new feature of the CTS termed generics. Many of the generics you will use on a daily basis are found within the System.Collections.Generic namespace. As shown over this chapter, generic containers are in many ways far superior to their nongeneric counterparts in that they provide greater type safety and performance benefits. Once you’ve seen generic support within the base class libraries, in the remainder of this chapter you’ll examine how you can build your own generic members, classes, structures, and interfaces.

Note It is also possible to create generic delegate types, which will be addressed in the next chapter.

The Interfaces of the System.Collections

Namespace

The most primitive container construct would have to be our good friend System.Array. As you have already seen in Chapter 4, this class provides a number of services (e.g., reversing, sorting, clearing, and enumerating). However, the simple Array class has a number of limitations; most notably, it does not automatically resize itself as you add or clear items. When you need to contain types in a more flexible container, one option is to leverage the types defined within the

System.Collections namespace.

The System.Collections namespace defines a number of interfaces (some of which you have already implemented during Chapter 9). A majority of the classes within System.Collections implement these interfaces to provide access to their contents. Table 10-1 gives a breakdown of the core collection-centric interfaces.

309

310 CHAPTER 10 COLLECTIONS AND GENERICS

Table 10-1. Interfaces of System.Collections

System.Collections Interface

Meaning in Life

ICollection

Defines general characteristics (e.g., size, enumeration, thread

 

safety) for all nongeneric collection types.

IComparer

Allows two objects to be compared.

IDictionary

Allows a nongeneric collection object to represent its contents

 

using name/value pairs.

IDictionaryEnumerator

Enumerates the contents of a type supporting IDictionary.

IEnumerable

Returns the IEnumerator interface for a given object.

IEnumerator

Enables foreach style iteration of subtypes.

IHashCodeProvider

Returns the hash code for the implementing type using a

 

customized hash algorithm.

IList

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

 

objects. Also, this interface defines members to determine

 

whether the implementing collection type is read-only and/or

 

a fixed-size container.

 

 

Many of these interfaces are related by an interface hierarchy, while others are stand-alone entities. Figure 10-1 illustrates the relationship between each type (recall from Chapter 9 that it is permissible for a single interface to derive from multiple interfaces).

Figure 10-1. The System.Collections interface hierarchy

The Role of ICollection

The ICollection interface is the most primitive interface of the System.Collections namespace in that it defines a behavior supported by a collection type. In a nutshell, this interface provides a small set of members that allow you to determine (a) the number of items in the container, (b) the thread safety of the container, as well as (c) the ability to copy the contents into a System.Array type. Formally, ICollection is defined as follows (note that ICollection extends IEnumerable):

public interface ICollection : IEnumerable

{

int Count { get; }

bool IsSynchronized { get; } object SyncRoot { get; }

void CopyTo(Array array, int index);

}

CHAPTER 10 COLLECTIONS AND GENERICS

311

The Role of IDictionary

A dictionary is simply a collection that maintains a set of name/value pairs. For example, you could build a custom type that implements IDictionary such that you can store Car types (the values) that may be retrieved by ID or pet name (e.g., names). Given this functionality, you can see that the

IDictionary interface defines a Keys and Values property as well as Add(), Remove(), and Contains() methods. The individual items may be obtained by the type indexer, which is a construct that allows you to interact with subitems using an arraylike syntax. Here is the formal definition:

public interface IDictionary : ICollection, IEnumerable

{

bool IsFixedSize { get; } bool IsReadOnly { get; }

// Type indexer; see Chapter 12 for full details. object this[object key] { get; set; }

ICollection Keys { get; } ICollection Values { get; }

void Add(object key, object value); void Clear();

bool Contains(object key); IDictionaryEnumerator GetEnumerator(); void Remove(object key);

}

The Role of IDictionaryEnumerator

If you were paying attention in the previous section, you may have noted that IDictionary. GetEnumerator() returns an instance of the IDictionaryEnumerator type. IDictionaryEnumerator is simply a strongly typed enumerator, given that it extends IEnumerator by adding the following functionality:

public interface IDictionaryEnumerator : IEnumerator

{

DictionaryEntry Entry { get; } object Key { get; }

object Value { get; }

}

Notice how IDictionaryEnumerator allows you to enumerate over items in the dictionary via the generalized Entry property, which returns a System.Collections.DictionaryEntry class type. In addition, you are also able to traverse the name/value pairs using the Key/Value properties.

The Role of IList

The final core interface of System.Collections is IList, which provides the ability to insert, remove, and index items into (or out of) a container:

public interface IList : ICollection, IEnumerable

{

bool IsFixedSize { get; } bool IsReadOnly { get; }

object this[ int index ] { get; set; }