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

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

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

272 CHAPTER 9 WORKING WITH INTERFACES

object theClone = c.Clone(); Console.WriteLine("Your clone is a: {0}",

theClone.GetType().Name);

}

}

When you run this application, you will find the full name of each class print out to the console, via the GetType() method you inherit from System.Object (Chapter 16 will provide full coverage of this method and .NET reflection services).

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

Another limitation of traditional abstract base classes is that each and every derived type must contend with the set of abstract members and provide an implementation. To see this problem, recall the shapes hierarchy we defined in Chapter 6. Assume we defined a new abstract method in the Shape base class named GetNumberOfPoints(), which allows derived types to return the number of points required to render the shape:

abstract class Shape

{

...

// Every derived class must now support this method! public abstract byte GetNumberOfPoints();

}

Clearly, the only type that has any points in the first place is Hexagon. However, with this update, every derived type (Circle, Hexagon, and ThreeDCircle) must now provide a concrete implementation of this function even if it makes no sense to do so.

Again, the interface type provides a solution. If we were to define an interface that represents the behavior of “having points,” we could simply plug it into the Hexagon type, leaving Circle and

ThreeDCircle untouched.

Defining Custom Interfaces

Now that you better understand the overall role of interface types, let’s see an example of defining custom interfaces. To begin, create a brand-new Console Application named CustomInterface. Using the Project Add Existing Item menu option, insert the files containing your shape type definitions (MyShapes.cs and Shape.cs in the book’s solution code) created back in Chapter 6 during the Shapes example. Once you have done so, rename the namespace that defines your shape-centric types to CustomInterface (simply to avoid having to import namespace definitions within your new project):

namespace CustomInterface

{

// Your previous shape types defined here...

}

Now, insert a new interface into your project named IPointy using the Project Add New Item menu option, as shown in Figure 9-1.

CHAPTER 9 WORKING WITH INTERFACES

273

Figure 9-1. Interfaces, like classes, can be defined in any *.cs file.

At a syntactic level, an interface is defined using the C# interface keyword. Unlike other .NET types, interfaces never specify a base class (not even System.Object) and their members never specify an access modifier (as all interface members are implicitly public and abstract). To get the ball rolling, here is a custom interface defined in C#:

// This interface defines the behavior of "having points." public interface IPointy

{

// Implicitly public and abstract. byte GetNumberOfPoints();

}

Notice that when you define interface members, you do not define an implementation scope for the member in question. Interfaces are pure protocol, and therefore never define an implementation (that is up to the supporting class or structure). Therefore, the following version of IPointy would result in various compiler errors:

// Ack! Errors abound! public interface IPointy

{

//Error! Interfaces cannot have fields! public int numbOfPoints;

//Error! Interfaces do not have constructors! public IPointy() { numbOfPoints = 0;};

//Error! Interfaces don't provide an implementation! byte GetNumberOfPoints() { return numbOfPoints; }

}

274 CHAPTER 9 WORKING WITH INTERFACES

In any case, this initial IPointy interface defines a single method. However, .NET interface types are also able to define any number of property prototypes. For example, you could create the IPointy interface to use a read-only property rather than a traditional accessor method:

// The pointy behavior as a read-only property. public interface IPointy

{

//A read-write property in an interface would look like

//retVal PropName { get; set; }

//while a write-only property in an interface would be

//retVal PropName { set; }

byte Points{ get; }

}

Note Interface types can also contain event (see Chapter 11) and indexer (see Chapter 12) definitions.

Do understand that interface types are quite useless on their own, as they are nothing more than a named collection of abstract members. For example, you cannot allocate interface types as you would a class or structure:

// Ack! Illegal to allocate interface types. static void Main(string[] args)

{

IPointy p = new IPointy(); // Compiler error!

}

Interfaces do not bring much to the table until they are implemented by a class or structure. Here, IPointy is an interface that expresses the behavior of “having points.” The idea is simple: some classes in the shapes hierarchy have points (such as the Hexagon), while others (such as the Circle) do not.

Implementing an Interface

When a class (or structure) chooses to extend its functionality by supporting interface types, it does so using a comma-delimited list in the type definition. Be aware that the direct base class must be the first item listed after the colon operator. When your class type derives directly from System. Object, you are free to simply list the interface(s) supported by the class, as the C# compiler will extend your types from System.Object if you do not say otherwise. On a related note, given that structures always derive from System.ValueType (see Chapter 4 for full details), simply list each interface directly after the structure definition. Ponder the following examples:

//This class derives from System.Object and

//implements a single interface.

public class Pencil : IPointy {...}

//This class also derives from System.Object

//and implements a single interface.

public class SwitchBlade : object, IPointy {...}

CHAPTER 9 WORKING WITH INTERFACES

275

//This class derives from a custom base class

//and implements a single interface.

public class Fork : Utensil, IPointy {...}

//This struct implicitly derives from System.ValueType and

//implements two interfaces.

public struct Arrow : IClonable, IPointy {...}

Understand that implementing an interface is an all-or-nothing proposition. The supporting type is not able to selectively choose which members it will implement. Given that the IPointy interface defines a single read-only property, this is not too much of a burden.

However, if you are implementing an interface that defines ten members (such as the IDbConnection interface seen earlier), the type is now responsible for fleshing out the details of all ten abstract entities.

For this example, insert a new class type named Triangle which “is-a” Shape and supports

IPointy:

// New Shape derived class named Triangle. public class Triangle : Shape, IPointy

{

public Triangle() { }

public Triangle(string name) : base(name) { } public override void Draw()

{ Console.WriteLine("Drawing {0} the Triangle", PetName); }

// IPointy Implementation. public byte Points

{

get { return 3; }

}

}

Now, update your existing Hexagon type to also support the IPointy interface type:

// Hexagon now implements IPointy. public class Hexagon : Shape, IPointy

{

public Hexagon(){ }

public Hexagon(string name) : base(name){ } public override void Draw()

{ Console.WriteLine("Drawing {0} the Hexagon", PetName); }

// IPointy Implementation. public byte Points

{

get { return 6; }

}

}

To sum up the story so far, the Visual Studio 2008 class diagram shown in Figure 9-2 illustrates IPointy-compatible classes using the popular “lollipop” notation. Notice again that Circle and ThreeDCircle do not implement IPointy, as this behavior makes no sense for these particular types.

276 CHAPTER 9 WORKING WITH INTERFACES

Figure 9-2. The shapes hierarchy (now with interfaces)

Note To display or hide interface names on the class designer, right-click on the interface icon and select Collapse or Expand.

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 (provided the interface members are not implemented explicitly; more details later in the section “Resolving Name Clashes via Explicit Interface Implementation”). For example, consider the following Main() method:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Interfaces *****\n");

// Call Points property 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 and therefore has a Points property. Other times, however, you may not be able to determine 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 an error. Next question: how can we dynamically determine the set of interfaces supported by a type?

One way to determine at runtime whether 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:

CHAPTER 9 WORKING WITH INTERFACES

277

static void Main(string[] args)

{

...

// Catch a possible InvalidCastException.

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

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 6. 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. Therefore, be sure to check against a null value before proceeding:

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..."); Console.ReadLine();

}

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 (also first seen in Chapter 6). 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 an array of Shape types containing some members that implement IPointy. Notice how we are able to determine which item in the array supports this interface using the is keyword, as shown in this retrofitted Main() method:

278CHAPTER 9 WORKING WITH INTERFACES

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Interfaces *****\n");

// Make an array of Shapes.

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

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); Console.WriteLine();

}

Console.ReadLine();

}

The output follows in Figure 9-3.

Figure 9-3. Dynamically determining implemented interfaces

Interfaces As Parameters

Given that interfaces are valid .NET types, you may construct methods that take interfaces as parameters as illustrated by the CloneMe() method earlier in this chapter. For the current example, assume you have defined another interface named IDraw3D:

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

{

void Draw3D();

}

CHAPTER 9 WORKING WITH INTERFACES

279

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

{

...

public void Draw3D()

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

}

Figure 9-4 presents the updated Visual Studio 2008 class diagram.

Figure 9-4. 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 method defined within your Program type:

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

{

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

}

We could now test whether an item in the Shape array supports this new interface, and if so, pass it into the DrawIn3D() method for processing:

280CHAPTER 9 WORKING WITH INTERFACES

static void Main()

{

Console.WriteLine("***** Fun with Interfaces *****\n");

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 type is never drawn in 3D, as it is not IDraw3D-compatible (see Figure 9-5).

Figure 9-5. 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 (if supported):

//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:

CHAPTER 9 WORKING WITH INTERFACES

281

static void Main(string[] args)

{

...

// Attempt to get IPointy from array of ints. int[] myInts = {10, 20, 30};

IPointy itfPt = ExtractPointyness(myInts); if(itfPt != null)

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

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

}

Arrays of Interface Types

Recall that the same interface can be implemented by numerous types, even if they are not within the same class hierarchy and do not have a common parent class beyond System.Object. This can yield some very powerful programming constructs. For example, assume that you have developed three new class types within your current project modeling kitchen utensils (via Knife and Fork classes) and another modeling gardening equipment (à la PitchFork). Consider Figure 9-6.

Figure 9-6. Recall that interfaces can be “plugged into” any type in any part of a class hierarchy.

If you did define the PitchFork, Fork, and Knife types, you could now define 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 item 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()};