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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

224 C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

Contrasting Interfaces to Abstract Base Classes

Given your work in Chapter 4, you may be wondering what the point of interface types are in the first place. After all, C# already allows you to build abstract class types containing abstract methods. Like an interface, when a class derives from an abstract base class, it is also under obligation to flesh out the details of the abstract methods (provided the derived class is not declared abstract as well). However, abstract base classes do far more than define a group of abstract methods. They are free to define public, private, and protected state data, as well as any number of concrete methods that can be accessed by the subclasses.

Interfaces, on the other hand, are pure protocol. Interfaces never define state data and never provide an implementation of the methods (if you try, you receive a compile-time error):

public interface IAmABadInterface

{

//Error, interfaces can't define data! int myInt = 0;

//Error, only abstract members allowed! void MyMethod()

{ Console.WriteLine("Eek!"); }

}

Interface types are also quite helpful given that C# (and .NET-aware languages in general) only support single inheritance; the interface-based protocol allows a given type to support numerous behaviors, while avoiding the issues that arise when deriving from extending multiple base classes.

Most importantly, interface-based programming provides yet another way to inject polymorphic behavior into a system. If multiple classes (or structures) implement the same interface in their unique ways, you have the power to treat each type in the same manner. As you will see a bit later in this chapter, interfaces are extremely polymorphic, given that types that are not related via classical inheritance can support identical behaviors.

Invoking Interface Members at the Object Level

Now that you have a set of types that support the IPointy interface, the next question is how you interact with the new functionality. The most straightforward way to interact with functionality supplied by a given interface is to invoke the methods directly from the object level. For example:

static void Main(string[] args)

{

// Call new Points member defined by IPointy.

Hexagon hex = new Hexagon(); Console.WriteLine("Points: {0}", hex.Points); Console.ReadLine();

}

This approach works fine in this particular case, given that you are well aware that the Hexagon type has implemented the interface in question. Other times, however, you will not be able to determine at compile time which interfaces are supported by a given type. For example, assume you have an array containing 50 Shape-compatible types, only some of which support IPointy. Obviously, if you attempt to invoke the Points property on a type that has not implemented IPointy, you receive a compile-time error. Next question: How can we dynamically determine the set of interfaces supported by a type?

The first way you can determine at runtime if a type supports a specific interface is to make use of an explicit cast. If the type does not support the requested interface, you receive an InvalidCastException. To handle this possibility gracefully, make use of structured exception handling, for example:

C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

225

static void Main(string[] args)

{

...

// Catch a possible InvalidCastException.

Circle c = new Circle("Lisa"); IPointy itfPt;

try

{

itfPt = (IPointy)c; Console.WriteLine(itfPt.Points);

}

catch (InvalidCastException e)

{ Console.WriteLine(e.Message); } Console.ReadLine();

}

While you could make use of try/catch logic and hope for the best, it would be ideal to determine which interfaces are supported before invoking the interface members in the first place. Let’s see two ways of doing so.

Obtaining Interface References: The as Keyword

The second way you can determine whether a given type supports an interface is to make use of the as keyword, which was first introduced in Chapter 4. If the object can be treated as the specified interface, you are returned a reference to the interface in question. If not, you receive a null reference:

static void Main(string[] args)

{

...

// Can we treat hex2 as IPointy?

Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy;

if(itfPt2 != null)

Console.WriteLine("Points: {0}", itfPt2.Points);

else

Console.WriteLine("OOPS! Not pointy...");

}

Notice that when you make use of the as keyword, you have no need to make use of try/catch logic, given that if the reference is not null, you know you are calling on a valid interface reference.

Obtaining Interface References: The is Keyword

You may also check for an implemented interface using the is keyword. If the object in question is not compatible with the specified interface, you are returned the value false. On the other hand, if the type is compatible with the interface in question, you can safely call the members without needing to make use of try/catch logic.

To illustrate, assume we have updated the array of Shape types by including some members that implement IPointy. Notice how we are able to determine which item in the array supports this interface using the is keyword:

static void Main(string[] args)

{

...

Shape[] s = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")} ;

226 C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

for(int i = 0; i < s.Length; i++)

{

//Recall the Shape base class defines an abstract Draw()

//member, so all shapes know how to draw themselves. s[i].Draw();

//Who's pointy?

if(s[i] is IPointy)

Console.WriteLine("-> Points: {0} ", ((IPointy)s[i]).Points);

else

Console.WriteLine("-> {0}\'s not pointy!", s[i].PetName);

}

}

The output follows in Figure 7-2.

Figure 7-2. Dynamically determining implemented interfaces

Interfaces As Parameters

Given that interfaces are valid .NET types, you may construct methods that take interfaces as parameters. To illustrate, assume you have defined another interface named IDraw3D:

// Models the ability to render a type in stunning 3D. public interface IDraw3D

{

void Draw3D();

}

Next, assume that two of your three shapes (Circle and Hexagon) have been configured to support this new behavior:

// Circle supports IDraw3D.

public class Circle : Shape, IDraw3D

{

...

public void Draw3D()

{Console.WriteLine("Drawing Circle in 3D!"); }

}

// Hexagon supports IPointy and IDraw3D. public class Hexagon : Shape, IPointy, IDraw3D

{

...

C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

227

public void Draw3D()

{Console.WriteLine("Drawing Hexagon in 3D!"); }

}

Figure 7-3 presents the updated Visual Studio 2005 class diagram.

Figure 7-3. The updated Shapes hierarchy

If you now define a method taking an IDraw3D interface as a parameter, you are able to effectively send in any object implementing IDraw3D (if you attempt to pass in a type not supporting the necessary interface, you receive a compile-time error). Consider the following:

// Make some shapes. If they can be rendered in 3D, do it! public class Program

{

// I'll draw anyone supporting IDraw3D. public static void DrawIn3D(IDraw3D itf3d)

{

Console.WriteLine("-> Drawing IDraw3D compatible type"); itf3d.Draw3D();

}

static void Main()

{

Shape[] s = { new Hexagon(), new Circle(),

new Triangle(), new Circle("JoJo")} ;

for(int i = 0; i < s.Length; i++)

{

...

// Can I draw you in 3D? if(s[i] is IDraw3D)

DrawIn3D((IDraw3D)s[i]);

}

}

}

Notice that the triangle is never drawn, as it is not IDraw3D-compatible (see Figure 7-4).

228 C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

Figure 7-4. Interfaces as parameters

Interfaces As Return Values

Interfaces can also be used as method return values. For example, you could write a method that takes any System.Object, checks for IPointy compatibility, and returns a reference to the extracted interface:

//This method tests for IPointy-compatibility and,

//if able, returns an interface reference.

static IPointy ExtractPointyness(object o)

{

if (o is IPointy) return (IPointy)o;

else

return null;

}

We could interact with this method as follows:

static void Main(string[] args)

{

// Attempt to get IPointy from Car object.

Car myCar = new Car();

IPointy itfPt = ExtractPointyness(myCar);

if(itfPt != null)

Console.WriteLine("Object has {0} points.", itfPt.Points);

else

Console.WriteLine("This object does not implement IPointy");

}

Arrays of Interface Types

Understand that the same interface can be implemented by numerous types, even if they are not within the same class hierarchy. This can yield some very powerful programming constructs. For example, assume that you have developed a brand new class hierarchy modeling kitchen utensils and another modeling gardening equipment.

C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

229

Although these hierarchies are completely unrelated from a classical inheritance point of view, you can treat them polymorphically using interface-based programming. To illustrate, assume you have an array of IPointy-compatible objects. Given that these members all support the same interface, you are able to iterate through the array and treat each object as an IPointy-compatible object, regardless of the overall diversity of the class hierarchies:

static void Main(string[] args)

{

//This array can only contain types that

//implement the IPointy interface.

IPointy[] myPointyObjects = {new Hexagon(), new Knife(), new Triangle(), new Fork(), new PitchFork()};

for (int i = 0; i < myPointyObjects.Length; i++)

Console.WriteLine("Object has {0} points.", myPointyObjects[i].Points);

}

Note Given the language-agonistic nature of .NET, understand that it is permissible to define an interface in one language (C#) and implement it in another (VB .NET). To understand how this is possible requires an understanding of .NET assemblies, which is the topic of Chapter 11.

Understanding Explicit Interface Implementation

In our current definition of IDraw3D, we were forced to name its sole method Draw3D() in order to avoid clashing with the abstract Draw() method defined in the Shape base class. While there is nothing horribly wrong with this interface definition, a more natural method name would simply be Draw():

// Refactor method name from "Draw3D" to "Draw". public interface IDraw3D

{

void Draw();

}

If we were to make such a change, this would require us to also update our implementation of

DrawIn3D().

public static void DrawIn3D(IDraw3D itf3d)

{

Console.WriteLine("-> Drawing IDraw3D compatible type"); itf3d.Draw();

}

Now, assume you have defined a new class named Line that derives from the abstract Shape class and implements IDraw3D (both of which now define an identically named abstract Draw() method):

// Problems? It depends...

public class Line : Shape, IDraw3D

{

public override void Draw()

{

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

}

}

230 C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

The Line class compiles without a hitch. But consider the following Main() logic:

static void Main(string[] args)

{

...

// Calls Draw().

Line myLine = new Line(); myLine.Draw();

// Calls same implementation of Draw()!

IDraw3D itfDraw3d= (IDraw3D) myLine; itfDraw3d.Draw();

}

Given what you already know about the Shape base class and IDraw3D interface, it looks as if you have called two variations of the Draw() method (one from the object level, the other from an interface reference). Nevertheless, the compiler is happy to call the same implementation from an interface or object reference, given that the Shape abstract base class and IDraw3D interface have an identically named member. This would be problematic if you would like to have the IDraw3D.Draw() method render a type in stunning 3D, while the overridden Shape.Draw() method draws in boring 2D.

Now consider a related problem. What if you wish to ensure that the methods defined by a given interface are only accessible from an interface reference rather than an object reference? Currently, the members defined by the IPointy interface can be accessed using either an object reference or an IPointy reference.

The answer to both questions comes by way of explicit interface implementation. Using this technique, you are able to ensure that the object user can only access methods defined by a given interface using the correct interface reference, as well as circumvent possible name clashes. To illustrate, here is the updated Line class (assume you have updated Hexagon and Circle in a similar manner):

//Using explicit method implementation we are able

//to provide distinct Draw() implementations. public class Line : Shape, IDraw3D

{

//You can only call this method from an IDraw3D interface reference. void IDraw3D.Draw()

{ Console.WriteLine("Drawing a 3D line..."); }

//You can only call this at the object level.

public override void Draw()

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

}

As you can see, when explicitly implementing an interface member, the general pattern breaks down to returnValue InterfaceName.MethodName(args). There are a few odds and ends to be aware of when using explicit interface implementation. First and foremost, you cannot define the explicitly implemented members with an access modifier. For example, the following is illegal syntax:

// Nope! Illegal.

public class Line : Shape, IDraw3D

{

public void IDraw3D.Draw() // <= Error!

{

Console.WriteLine("Drawing a 3D line...");

}

...

}

C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

231

This should make sense. The whole reason to use explicit interface method implementation is to ensure that a given interface method is bound at the interface level. If you were to add the public keyword, this would suggest that the method is a member of the public sector of the class, which defeats the point! Given this design, the caller is only able to invoke the Draw() method defined by the Shape base class from the object level:

// This invokes the overridden Shape.Draw() method.

Line myLine = new Line(); myLine.Draw();

To invoke the Draw() method defined by IDraw3D, we must now explicitly obtain the interface reference using any of the techniques shown previously. For example:

// This triggers the IDraw3D.Draw() method.

Line myLine = new Line(); IDraw3D i3d = (IDraw3D)myLine; i3d.Draw();

Resolving Name Clashes

Explicit interface implementation can also be very helpful whenever you are implementing a number of interfaces that happen to contain identical members. For example, assume you wish to create a class that implements all the following new interface types:

// Three interfaces each define identically named methods. public interface IDraw

{

void Draw();

}

public interface IDrawToPrinter

{

void Draw();

}

If you wish to build a class named SuperImage that supports basic rendering (IDraw), 3D rendering (IDraw3D), as well as printing services (IDrawToPrinter), the only way to provide unique implementations for each method is to use explicit interface implementation:

// Not deriving from Shape, but still injecting a name clash. public class SuperImage : IDraw, IDrawToPrinter, IDraw3D

{

void IDraw.Draw()

{/* Basic drawing logic. */ }

void IDrawToPrinter.Draw()

{/* Printer logic. */ }

void IDraw3D.Draw()

{/* 3D rendering logic. */ }

}

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

232 C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

Building Interface Hierarchies

To continue our investigation of creating custom interfaces, let’s examine the topic of interface hierarchies. Just as a class can serve as a base class to other classes (which can in turn function as base classes to yet another class), it is possible to build inheritance relationships among interfaces. As you might expect, the topmost interface defines a general behavior, while the most derived interface defines more specific behaviors. To illustrate, ponder the following interface hierarchy:

// The base interface. public interface IDrawable

{void Draw();}

public interface IPrintable : IDrawable

{void Print(); }

public interface IMetaFileRender : IPrintable

{void Render(); }

Figure 7-5 illustrates the chain of inheritance.

Figure 7-5. An interface hierarchy

Now, if a class wished to support each behavior expressed in this interface hierarchy, it would derive from the nth-most interface (IMetaFileRender in this case). Any methods defined by the base interface(s) are automatically carried into the definition. For example:

// This class supports IDrawable, IPrintable, and IMetaFileRender. public class SuperImage : IMetaFileRender

{

public void Draw()

{ Console.WriteLine("Basic drawing logic."); }

public void Print()

{ Console.WriteLine("Draw to printer."); }

public void Render()

{ Console.WriteLine("Render to metafile."); }

}

C H A P T E R 7 I N T E R FA C E S A N D C O L L E C T I O N S

233

Here is some sample usage of exercising each interface from a SuperImage instance:

// Exercise the interfaces. static void Main(string[] args)

{

SuperImage si = new SuperImage();

// Get IDrawable.

IDrawable itfDraw = (IDrawable)si; itfDraw.Draw();

//Now get ImetaFileRender, which exposes all methods up

//the chain of inheritance.

if (itfDraw is IMetaFileRender)

{

IMetaFileRender itfMF = (IMetaFileRender)itfDraw; itfMF.Render();

itfMF.Print();

}

Console.ReadLine();

}

Interfaces with Multiple Base Interfaces

As you build interface hierarchies, be aware that it is completely permissible to create an interface that derives from multiple base interfaces. Recall, however, that it is not permissible to build a class that derives from multiple base classes. To illustrate, assume you are building a new set of interfaces that model the automobile behaviors for a particular English agent:

public interface ICar

{void Drive(); }

public interface IUnderwaterCar

{void Dive(); }

// Here we have an interface with TWO base interfaces. public interface IJamesBondCar : ICar, IUnderwaterCar

{void TurboBoost(); }

Figure 7-6 illustrates the chain of inheritance.

Figure 7-6. Multiple inheritance of interface types is allowed by the CTS.

If you were to build a class that implements IJamesBondCar, you would now be responsible for implementing TurboBoost(), Dive(), and Drive():