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

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

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

282 CHAPTER 9 WORKING WITH INTERFACES

foreach(IPointy i in myPointyObjects) Console.WriteLine("Object has {0} points.", i.Points);

Console.ReadLine();

}

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

Implementing Interfaces Using Visual Studio 2008

Although interface-based programming is a very powerful programming technique, implementing interfaces may entail a healthy amount of typing. Given that interfaces are a named set of abstract members, you will be required to type in the definition and implementation for each interface method on each type that supports the behavior.

As you would hope, Visual Studio 2008 does support various tools that make the task of implementing interfaces less burdensome. By way of a simple test, insert a final class into your current project named PointyTestClass. When you implement IPointy (or any interface for that matter) on a type, you might have noticed that when you complete typing the interface’s name (or when you position the mouse cursor on the interface name in the code window), the first letter is underlined (formally termed a “smart tag”). When you click the smart tag, you will be presented a drop-down list that allows you to implement the interface (see Figure 9-7).

Figure 9-7. Implementing interfaces using Visual Studio 2008

Notice you are presented with two options, the second of which (explicit interface implementation) will be examined in the next section. For the time being, once you select the first option, you will see that Visual Studio 2008 has built generated stub code (within a named code region) for you to update (note that the default implementation throws a System.Exception, which can obviously be deleted).

namespace CustomInterface

{

class PointyTestClass : IPointy

{

#region IPointy Members public byte Points

{

CHAPTER 9 WORKING WITH INTERFACES

283

get { throw new Exception("The method or operation is not implemented."); }

}

#endregion

}

}

Note Visual Studio 2008 also supports an extract interface refactoring, available from the Refactoring menu. This allows you to pull out a new interface definition from an existing class definition. See my MSDN article “Refactoring C# Code Using Visual Studio 2005” (the same holds true for Visual Studio 2008) for further details.

Resolving Name Clashes via Explicit Interface Implementation

As shown earlier in this chapter, a single class or structure can implement any number of interfaces. Given this, there is always a possibility that you may implement interfaces that contain identically named members, and therefore have a name clash to contend with. To illustrate various manners in which you can resolve this issue, create a new Console Application named InterfaceNameClash. Now design three custom interfaces that represent various locations to which an implementing type could render its output:

//Draw image to a Form. public interface IDrawToForm

{

void Draw();

}

//Draw to buffer in memory. public interface IDrawToMemory

{

void Draw();

}

//Render to the printer. public interface IDrawToPrinter

{

void Draw();

}

Notice that each interface defines a method named Draw(). If you now wish to support each of these interfaces on a single class type named Octagon, the compiler would allow the following definition:

class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter

{

public void Draw()

{

// Shared drawing logic.

Console.WriteLine("Drawing the Octagon...");

}

}

284 CHAPTER 9 WORKING WITH INTERFACES

Although the code compiles cleanly, you may agree we do have a possible problem. Simply put, providing a single implementation of the Draw() method does not allow us to take unique courses of action based on which interface is obtained from an Octagon object. For example, the following code will invoke the same Draw() method, regardless of which interface we obtain:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Interface Name Clashes *****\n");

//All of these invocations call the

//same Draw() method!

Octagon oct = new Octagon(); oct.Draw();

IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw();

IDrawToPrinter itfPriner = (IDrawToPrinter)oct; itfPriner.Draw();

IDrawToMemory itfMemory = (IDrawToMemory)oct; itfMemory.Draw();

Console.ReadLine();

}

Clearly, the sort of code required to render the image to a window is quite different from the code needed to render the image to a networked printer or a region of memory. When you implement a collection of interfaces that have identical members, you can resolve this sort of name clash using explicit interface implementation syntax. Consider the following update to the Octagon type:

class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter

{

//Explicitly bind Draw() implementations

//to a given interface.

void IDrawToForm.Draw()

{

Console.WriteLine("Drawing to form...");

}

void IDrawToMemory.Draw()

{

Console.WriteLine("Drawing to memory...");

}

void IDrawToPrinter.Draw()

{

Console.WriteLine("Drawing to a printer...");

}

}

As you can see, when explicitly implementing an interface member, the general pattern breaks down to

returnValue InterfaceName.MethodName(args)

Note that when using this syntax, you do not supply an access modifier; explicitly implemented members are automatically private. For example, the following is illegal syntax:

// Error! No access modifer! public void IDrawToForm.Draw()

{

Console.WriteLine("Drawing to form...");

}

CHAPTER 9 WORKING WITH INTERFACES

285

Because explicitly implemented members are always implicitly private, these members are no longer available from the object level. In fact, if you were to apply the dot operator to an Octagon type, you will find that IntelliSense will not show you any of the Draw() members (see Figure 9-8).

Figure 9-8. Explicitly implemented interface members are not exposed from the object level.

As expected, you must make use of explicit casting to access the required functionality. For example:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Interface Name Clashes *****\n");

Octagon oct = new Octagon();

//We now must use casting to access the Draw()

//members.

IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw();

//Shorthand notation if you don't need

//the interface variable for later use.

((IDrawToPrinter)oct).Draw();

//Could also use the "as" keyword. if(oct is IDrawToMemory)

((IDrawToMemory)oct).Draw();

Console.ReadLine();

}

While this syntax is quite helpful when you need to resolve name clashes, you are able to use explicit interface implementation simply to hide more “advanced” members from the object level. In this way, when the object user applies the dot operator, he or she will only see a subset of the type’s overall functionality. However, those who require the more advanced behaviors can extract out the desired interface via an explicit cast.

286 CHAPTER 9 WORKING WITH INTERFACES

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

Designing Interface Hierarchies

Interfaces can be arranged into an interface hierarchy. Like a class hierarchy, when an interface extends an existing interface, it inherits the abstract members defined by the parent type(s). Of course, unlike class-based inheritance, derived interfaces never inherit true implementation.

Rather, a derived interface simply extends its own definition with additional abstract members. Interface hierarchies can be useful when you wish to extend the functionality of an existing

interface without breaking existing code bases. To illustrate, create a new Console Application named InterfaceHierarchy. Now, let’s redesign the previous set of rendering-centric interfaces (from the InterfaceNameClash example) such that IDrawable is the root of the family tree:

public interface IDrawable

{

void Draw();

}

Given that IDrawable defines a basic drawing behavior, we could now create a derived interface that extends this type with the ability to render its output to the printer. Assume this method is called Print():

public interface IPrintable : IDrawable

{

void Print();

}

And just for good measure, we could define a final interface named IRenderToMemory, which extends IPrintable with a new member named Render():

public interface IRenderToMemory : IPrintable

{

void Render();

}

Given this design, if a type were to implement IRenderToMemory, we would now be required to implement each and every member defined up the chain of inheritance (specifically, the Render(), Print(), and Draw() methods). On the other hand, if a type were to only implement IPrintable, we would only need to contend with Print() and Draw(). For example:

public class SuperShape : IRenderToMemory

{

public void Draw()

{

Console.WriteLine("Drawing...");

}

public void Print()

{

Console.WriteLine("Printing...");

}

public void Render()

{

CHAPTER 9 WORKING WITH INTERFACES

287

Console.WriteLine("Rendering...");

}

}

Now, when we make use of the SuperShape, we are able to invoke each method at the object level (as they are all public) as well as extract out a reference to each supported interface explicitly via casting:

static void Main(string[] args)

{

Console.WriteLine("***** The SuperShape *****");

// Call from object level.

SuperShape myShape = new SuperShape(); myShape.Draw();

//Get IPrintable explicitly.

//(and IDrawable implicitly!)

IPrintable iPrint;

iPrint = (IPrintable)myShape; iPrint.Draw(); iPrint.Print(); Console.ReadLine();

}

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

Multiple Inheritance with Interface Types

Unlike class types, it is possible for a single interface to extend multiple base interfaces. This allows us to design some very powerful and flexible abstractions. Create a new Console Application project named MIInterfaceHierarchy. Here is a brand-new collection of interfaces that model various rendering and shape-centric abstractions. Notice that the IShape interface is extending both IDrawable and IPrintable:

// Multiple inheritance for interface types is a-okay. public interface IDrawable

{

void Draw();

}

public interface IPrintable

{

void Print();

void Draw(); // <-- Note possible name clash here!

}

// Multiple interface inheritance. OK!

public interface IShape : IDrawable, IPrintable

{

int GetNumberOfSides();

}

Figure 9-9 illustrates the current interface hierarchy.

288 CHAPTER 9 WORKING WITH INTERFACES

Figure 9-9. Unlike classes, interfaces can extend multiple interface types.

Now, the million dollar question is, if we have a class supporting IShape, how many methods will it be required to implement? The answer: it depends. If we wish to provide a simple implementation of the Draw() method, we only need to provide three members, as shown in the following

Rectangle type:

class Rectangle : IShape

{

public int GetNumberOfSides() { return 4; }

public void Draw()

{ Console.WriteLine("Drawing..."); }

public void Print()

{ Console.WriteLine("Prining..."); }

}

If you would rather have specific implementations for each Draw() method (which in this case would make the most sense), you can resolve the name clash using explicit interface implementation, as shown in the following Square type:

class Square : IShape

{

// Using explicit implementation to handle member name clash. void IPrintable.Draw()

{// Draw to printer ...

}

void IDrawable.Draw()

{// Draw to screen ...

}

public void Print()

{// Print ...

}

public int GetNumberOfSides() { return 4; }

}

CHAPTER 9 WORKING WITH INTERFACES

289

So at this point, you hopefully feel more comfortable with the process of defining and implementing custom interfaces using the syntax of C#. To be honest, interface-based programming can take awhile to get comfortable with, so if you are in fact still scratching your head just a bit, this is a perfectly normal reaction.

Do be aware, however, that interfaces are a fundamental aspect of the .NET Framework. Regardless of the type of application you are developing (web-based, desktop GUIs, data access libraries, etc.), working with interfaces will be part of the process. To summarize the story thus far, remember that interfaces can be extremely useful when

You have a single hierarchy where only a subset of the derived types support a common behavior.

You need to model a common behavior that is found across multiple hierarchies with no common parent class beyond System.Object.

Now that you have drilled into the specifics of building and implementing custom interfaces, the remainder of the chapter examines a number of predefined interfaces contained within the

.NET base class libraries.

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

Building Enumerable Types (IEnumerable and

IEnumerator)

To begin examining the process of implementing existing .NET interfaces, let’s first look at the role of IEnumerable and IEnumerator. Recall that C# supports a keyword named foreach, which allows you to iterate over the contents of any array type:

// Iterate over an array of items. int[] myArrayOfInts = {10, 20, 30, 40}; foreach(int i in myArrayOfInts)

{

Console.WriteLine(i);

}

While it may seem that only array types can make use of this construct, the truth of the matter is any type supporting a method named GetEnumerator() can be evaluated by the foreach construct. To illustrate, begin by creating a new Console Application project named CustomEnumerator. Next, add the Car.cs and Radio.cs files defined in the SimpleException example of Chapter 7 (via the Project Add Existing Item menu option) and update the current class definition with two new properties (named PetName and Speed) that wrap the existing currSpeed and petName member variables:

public class Car

{

private int currSpeed; private string petName;

public int Speed

{

get { return currSpeed; }

290 CHAPTER 9 WORKING WITH INTERFACES

set { currSpeed = value; }

}

public string PetName

{

get { return petName; } set { petName = value; }

}

...

}

Note You may wish to rename the namespace containing the Car and Radio types to CustomEnumerator, simply to avoid having to import the CustomException namespace within this new project.

Now, insert a new class named Garage that stores a set of Car types within a System.Array:

// Garage contains a set of Car objects. public class Garage

{

private Car[] carArray = new Car[4];

// Fill with some Car objects upon startup. public Garage()

{

carArray[0] = new Car("Rusty", 30); carArray[1] = new Car("Clunker", 55); carArray[2] = new Car("Zippy", 30); carArray[3] = new Car("Fred", 30);

}

}

Ideally, it would be convenient to iterate over the Garage object’s subitems using the C# foreach construct, just like an array of data values:

// This seems reasonable...

public class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****\n");

Garage carLot = new Garage();

// Hand over each car in the collection? foreach (Car c in carLot)

{

Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);

}

Console.ReadLine();

}

}

Sadly, the compiler informs you that the Garage class does not implement a method named GetEnumerator(). This method is formalized by the IEnumerable interface, which is found lurking within the System.Collections namespace. Types that support this behavior advertise that they are able to expose contained subitems to the caller (in this example, the foreach keyword itself):

CHAPTER 9 WORKING WITH INTERFACES

291

//This interface informs the caller

//that the object's subitems can be enumerated. public interface IEnumerable

{

IEnumerator GetEnumerator();

}

As you can see, the GetEnumerator() method returns a reference to yet another interface named System.Collections.IEnumerator. This interface provides the infrastructure to allow the caller to traverse the internal objects contained by the IEnumerable-compatible container:

//This interface allows the caller to

//obtain a container's subitems. public interface IEnumerator

{

bool MoveNext (); object Current { get;} void Reset ();

}

//Advance the internal position of the cursor.

//Get the current item (read-only property).

//Reset the cursor before the first member.

If you wish to update the Garage type to support these interfaces, you could take the long road and implement each method manually. While you are certainly free to provide customized versions of GetEnumerator(), MoveNext(), Current, and Reset(), there is a simpler way. As the System.Array type (as well as many other types) already implements IEnumerable and IEnumerator, you can simply delegate the request to the System.Array as follows:

using System.Collections;

...

public class Garage : IEnumerable

{

// System.Array already implements IEnumerator! private Car[] carArray = new Car[4];

public Garage()

{

carArray[0] = new Car("FeeFee", 200, 0); carArray[1] = new Car("Clunker", 90, 0); carArray[2] = new Car("Zippy", 30, 0); carArray[3] = new Car("Fred", 30, 0);

}

public IEnumerator GetEnumerator()

{

// Return the array object's IEnumerator. return carArray.GetEnumerator();

}

}

Once you have updated your Garage type, you can now safely use the type within the C# foreach construct. Furthermore, given that the GetEnumerator() method has been defined publicly, the object user could also interact with the IEnumerator type:

// Manually work with IEnumerator.

IEnumerator i = carLot.GetEnumerator(); i.MoveNext();

Car myCar = (Car)i.Current;

Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.Speed);