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

C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005

.pdf
Скачиваний:
81
Добавлен:
12.02.2016
Размер:
2.09 Mб
Скачать

124 C H A P T E R 5 I N T E R FA C E S A N D C O N T R A C T S

{

public SomeValue( int n ) { this.n = n;

}

int IComparable.CompareTo( object obj ) { if( obj is SomeValue ) {

SomeValue other = (SomeValue) obj;

return n - other.n; } else {

throw new ArgumentException( "Wrong Type!" );

}

}

public int CompareTo( SomeValue other ) { return n - other.n;

}

private int n;

}

public class EntryPoint

{

static void Main() {

SomeValue val1 = new SomeValue( 1 ); SomeValue val2 = new SomeValue( 2 );

Console.WriteLine( val1.CompareTo(val2) );

}

}

In this example, there is absolutely no boxing in the call to CompareTo(). That’s because the compiler picks the one with the best match for the type. In this case, since you implement IComparable.CompareTo() explicitly, there is only one overload of CompareTo() in the public contract of SomeValue. But even if IComparable.CompareTo() had not been implemented explicitly, the compiler would have still chosen the type-safe version. The typical pattern involves hiding the type-less versions from casual use so that the user must do a boxing operation explicitly. This operation converts the value to an interface reference in order to get to the type-less version.

The bottom line is that you’ll definitely want to follow this idiom any time you implement an interface on a value type where you determine that you can define overloads with better type safety than the ones listed in the interface declaration. Avoiding unnecessary boxing is always a good thing, and your users will appreciate your detail to efficiency.

Versioning Considerations

The concept of versioning is essentially married to the concept of interfaces. When you create, define, and publish an interface, you’re defining a contract—or viewed in more rigid terms—a standard. Any time you have a standard form of communication, you must adhere to it so as not to break any clients of that contract. For example, consider the 802.11 standard upon which many WiFi devices are based. It’s important that access points from one vendor work with devices from as many vendors as possible. This works as long as all of the vendors agree and follow the standard. Can you imagine the chaos that would erupt if only a single vendor’s WiFi card were the only one that worked at the

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

125

local location of your favorite Pacific Northwest-based coffee company? It would be pandemonium. Therefore, we have standards.

Now, nothing states that the standard cannot be augmented. Certain manufacturers do just that. In some cases, if you use Manufacturer A’s access point with the same manufacturer’s wireless card, you can achieve speeds greater than those supported by the standard. However, note that those augmentations only augment, and don’t alter, the standard. Similarly, nothing states that a standard cannot be revised. Standards normally have version numbers attached to them, and when they are revised, the version number is incremented. Most of the time, devices that implement the new version also support the previous version. Although not required, it’s a good move for those manufacturers who want to achieve maximum market saturation. In the 802.11 example, 802.11a, 802.11b, and 802.11g represent the various revisions of the standard.

The point of this whole discussion and example is that you should apply these same rules to your interfaces once you publish them. You don’t normally create interfaces unless you’re doing so to allow entities to interact with each other using a common contract. So, once you’re done with creating that contract, do the right thing and slap a version number on it. You can create your version number in many ways. For new revisions of your interface, you could simply give it a new name—the key point being that you never change the original interface. You’ve probably already seen this exact same idiom in use in the COM world. Typically, if someone, probably Microsoft, decides they have a good reason to augment the behavior of an interface, you’ll find a new interface definition ending with either an Ex suffix or a numeric suffix. At either rate, it’s a completely different interface than the previous one, even though the contract of the new interface could inherit the original interface, and the implementations may be shared.

Note Current design guidelines in wide use suggest that if you need to create an augmented interface based upon another, you shouldn’t use the suffix Ex as COM does. Instead, you should follow the interface name with an ordinal. So, if the original interface is ISomeContract, then you should name the augmented interface ISomeContract2.

In reality, if your interface definitions live within a versioned assembly, you may define a newer version of the same interface, even with the same name, in an assembly with the same name but with a new version number. The assembly loader will resolve and load the proper assembly at run time. However, this practice can become confusing to the developers using your interface, since they now have to be more explicit about which assembly to reference at build time.

Contracts

Many times, you need to represent the notion of a contract when designing an application or system. A programming contract is no different than any other contract. You usually define a contract to facilitate communication between two types in your design. For example, suppose you have a virtual zoo, and in your zoo, you have animals. Now, an instance of your ZooKeeper needs a way to communicate to the collection of these ZooDweller objects that they should fly to a specific location. Ignoring the fact that they had better be fairly obedient, they had also better be able to fly. However, not all animals can fly, so clearly not all of the types in the zoo can support this flying contract.

Contracts Implemented with Classes

Let’s consider one way to manage the complexity of getting these creatures to fly from one location to the next. First, consider the assumptions that you can make here. Let’s say that this Zoo can have only one ZooKeeper. Second, let’s assume that you can model the locations within this Zoo by using a simple

126C H A P T E R 5 I N T E R FA C E S A N D C O N T R A C T S

two-dimensional Point structure. It starts to look as though you can model this system by the following code:

using System;

using System.Collections.ObjectModel;

namespace CityOfShanoo.MyZoo

{

public struct Point

{

public double X; public double Y;

}

public abstract class ZooDweller

{

public void EatSomeFood() { DoEatTheFood();

}

protected abstract void DoEatTheFood();

}

public sealed class ZooKeeper

{

public void SendFlyCommand( Point to ) { // Implementation removed for clarity.

}

}

public sealed class Zoo

{

private static Zoo theInstance = new Zoo(); public static Zoo GetInstance() {

return theInstance;

}

private Zoo() {

creatures = new Collection<ZooDweller>();3 zooKeeper = new ZooKeeper();

}

public ZooKeeper ZooKeeper { get {

return zooKeeper;

}

}

private ZooKeeper zooKeeper;

3.If the syntax of Collection<ZooDweller> looks foreign to you, don’t worry. It is a declaration of a collection based on a generic collection type. I will cover generics in detail in Chapter 10.

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

127

private Collection<ZooDweller> creatures;

}

}

Since there can only be one zoo in the CityOfShanoo, the Zoo is modeled as a singleton object, and the only way to obtain the instance of the one and only Zoo is to call Zoo.GetInstance(). Also, you can get a reference to the ZooKeeper via the Zoo.ZooKeeper property. It is common practice in the .NET Framework to name the property after the custom type that it represents.

Note The Singleton design pattern is one of the mostly widely used and well-known design patterns. Essentially, the pattern allows only one instance of its type to exist at one time. Many people still argue about the best way to implement it. Implementation difficulty varies depending on the language you’re using. But in general, some static private instance within the type declaration is lazily initialized at the point of first access. The previous implementation of the Zoo class does that, since the static initializer is not called until the type is first accessed through the GetInstance method.

This initial design defines the ZooDweller as an abstract class that implements a method EatSomeFood(). The ZooDweller uses the Non-Virtual Interface (NVI) pattern described in Chapter 13, where the virtual method that the concrete type overrides is declared protected rather than public.

It’s important to note that the ZooDweller type does, in fact, define a contract even though it is not an interface. The contract, as written, states that any type that derives from ZooDweller must implement EatSomeFood(). Any code that uses a ZooDweller instance can be guaranteed that this method is supported.

Note Notice that an interface is not required in order to define a contract.

So far, this design is missing a key operation, and that is the one commanding the creatures to fly to a destination within the zoo. Clearly, you cannot put a Fly method on the ZooDweller type, because not all animals in the zoo can fly. You must express this contract in a different way.

Interface Contracts

Since not all creatures in the zoo can fly, an interface provides an excellent mechanism for defining the flying contract. Consider the following modifications to the example from the previous section:

public interface IFly

{

void FlyTo( Point destination );

}

public class Bird : ZooDweller, IFly

{

public void FlyTo( Point destination ) { Console.WriteLine( "Flying to ({0}. {1}).",

destination );

}

protected override void DoEatTheFood() {

128 C H A P T E R 5 I N T E R FA C E S A N D C O N T R A C T S

Console.WriteLine( "Eating some food." );

}

}

Now, using the interface IFly, Bird is defined such that it derives from ZooDweller and implements IFly.

Note If you intend to have various bird types derive from Bird, and those various birds have different implementations of ToFly(),consider using the NVI pattern. You could introduce a protected virtual method named DoFlyTo that the base types override, while having Bird.FlyTo() call through to DoFlyTo(). Read the section titled “Use the Non-Virtual Interface (NVI) Pattern” in Chapter 13 for more information on why this is a good idea.

Choosing Between Interfaces and Classes

The previous section on contracts shows that you can implement a contract in multiple ways. In the C# and .NET environments, the two main methods are interfaces and classes, where the classes may even be abstract. In the zoo example from the previous section, it’s pretty clear as to when you should use an interface rather than an abstract class to define an interface. However, the choice is not always so clear, so let’s consider the ramifications of both methods.

Note Since COM became so popular, some developers have a false notion that the only way to define a contract is by defining an interface. It’s easy to jump to that conclusion when moving from the COM environment to the C# environment, simply because the basic building block of COM is the interface, and C# and .NET support interfaces natively. However, jumping to that conclusion would be perilous to your designs.

If you’re familiar with COM and you’ve created any serious COM projects in the past, you most certainly implemented the COM objects using C++. You probably even used the Active Template Library (ATL) to shield yourself from the intricacies of the mundane COM development tasks. But at the core of it all, how does C++ model COM interfaces? The answer is with abstract classes.

You can also model interfaces in C++ or C with simple function tables (structures containing function pointers), but the dominant method in C++ is to use abstract classes.

Since C# supports abstract classes, you can easily model a contract using abstract classes. But which method is more powerful? And which method is more appropriate? These are not easy questions to answer, although the guideline tends to be that you should prefer a class if possible. Let’s explore this.

When you implement a contract by defining an interface, you’re defining a versioned contract. That means that the interface, once released, must never change, as if it were cast into stone. Sure, you could change it later, but you would not be very popular when all of your clients’ code fails to compile with the modified interface. Consider the following example:

public interface IMyOperations

{

void Operation1(); void Operation2();

}

// Client class

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

129

public class ClientClass : IMyOperations

{

public void Operation1() { } public void Operation2() { }

}

Now, you’ve released this wonderful IMyOperations interface to the world, and thousands of clients have implemented it. Then, you start getting requests from your clients asking for Operation3() support in your library. It seems like it would be easy enough to simply add the Operation3() to the IMyOperations interface, but that would be a terrible mistake. If you add another operation to IMyOperations, then all of a sudden your clients’ code won’t compile until they implement the new operation. Also, code in another assembly that knows about the newer IMyOperations could attempt to cast a ClientClass instance into an IMyOperations reference and then call Operation3(), thus creating a runtime failure. Clearly, you shouldn’t modify an already published interface.

Caution Never modify an already publicly published interface declaration.

You could also address this problem by defining a completely new interface, say IMyOperations2. However, ClientClass would need to implement both interfaces in order to get the new behavior, as shown here:

public interface IMyOperations

{

void Operation1(); void Operation2();

}

public interface IMyOperations2

{

void Operation1(); void Operation2(); void Operation3();

}

// Client class

public class ClientClass : IMyOperations, IMyOperations2

{

public void Operation1() { } public void Operation2() { } public void Operation3() { }

}

public class AnotherClass

{

public void DoWork( IMyOperations ops ) {

}

}

130 C H A P T E R 5 I N T E R FA C E S A N D C O N T R A C T S

Modifying ClientClass to support the new operation from IMyOperations2 isn’t terribly difficult, but what about the code that already exists, such as what is shown in AnotherClass? The problem is that the DoWork method accepts a type of IMyOperations. In order to make it to where the new Operation3 method can be called, the prototype of DoWork() must change, or the code within it must do a cast to IOperations2, which could fail at run time. Since you want the compiler to be able to catch as many type bugs as possible, it would be better if you change the prototype of DoWork() to accept a type of IMyOperations2.

Note If you define your original IMyOperations interface within a fully versioned, strongly named assembly, then you can get away with creating a new interface with the same name in a new assembly, as long as the version of the new assembly is different. Although the .NET Framework supports this explicitly, it doesn’t mean you should do it without careful consideration, since introducing two IMyOperations interfaces that differ only by version number of the containing assembly could be confusing to your clients.

That was a lot of work just to make a new operation available to clients. Let’s examine the same situation, except using an abstract class:

public abstract class MyOperations

{

public virtual void Operation1() {

}

public virtual void Operation2() {

}

}

// Client class

public class ClientClass : MyOperations

{

public override void Operation1() { } public override void Operation2() { }

}

public class AnotherClass

{

public void DoWork( MyOperations ops ) {

}

}

MyOperations is a base class of ClientClass. One advantage is that MyOperations can contain default implementations if it wants to. Otherwise, the virtual methods in MyOperations could have been declared abstract. The previous example also declares MyOperations abstract, since it makes no sense for clients to be able to create instances of MyOperations. Now, let’s suppose you want to add a new Operation3 method to MyOperations, and you don’t want to break existing clients. You can do this as long as the added operation is not abstract, such that it forces changes on derived types, as shown here:

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

131

public abstract class MyOperations

{

public virtual void Operation1() {

}

public virtual void Operation2() {

}

public virtual void Operation3() { // New default implementation

}

}

// Client class

public class ClientClass : MyOperations

{

public override void Operation1() { } public override void Operation2() { }

}

public class AnotherClass

{

public void DoWork( MyOperations ops ) { ops.Operation3();

}

}

Notice that the addition of MyOperations.Operation3() doesn’t force any changes upon

ClientClass, and AnotherClass.DoWork() can make use of Operation3() without making any changes to the method declaration. This technique doesn’t come without its drawbacks, though. You’re restricted by the fact that the managed runtime only allows a class to have one base class. Since ClientClass has to derive from MyOperations to get the functionality, it uses up its only inheritance ticket. This may put complicated restrictions upon your client code. For example, what if one of your clients needs to create an object for use with .NET Remoting? In order to do so, the class must derive from MarshalByRefObject.

Sometimes, it’s tricky to find a happy medium when deciding between interfaces and classes. I use the following rules of thumb:

If modeling an is-a relationship, use a class: If it makes sense to name your contract with a noun, then you should probably model it with a class.

If modeling an IMPLEMENTS relationship, use an interface: If it makes sense to name your contract with an adjective, as if it is a quality, then you should probably model it as an interface.

Consider wrapping up your interface and abstract class declarations in a separate assembly: Implementations in other assemblies can then reference this separate assembly.

If possible, prefer classes over interfaces: This can be helpful for the sake of extensibility.

You can see examples of these techniques throughout the .NET Framework Base Class Library (BCL). Consider using them in your own code as well.

132 C H A P T E R 5 I N T E R FA C E S A N D C O N T R A C T S

Summary

This chapter introduced you to interfaces and how you can model a well-defined, versioned contract using an interface. Along with showing you the various ways that classes can implement interfaces, I also described the process that the C# compiler follows when matching up interface methods to implementations in the implementing class. I described interfaces from the perspective of reference types and value types—specifically, how expensive boxing operations can cause you pain when using interfaces on value types. Finally, I spent some time comparing and contrasting the use of interfaces and classes when modeling contracts between types in your design.

In the next chapter, I’ll explain the intricacies of operator overloading in the C# language and why you may want to avoid it when creating code used by other .NET languages.

C H A P T E R 6

■ ■ ■

Overloading Operators

C# adopted the capability of operator overloading from C++. Just as you can overload methods, you can overload operators such as +, -, *, and so on. In addition to overloading arithmetic operators, you can also create custom conversion operators to convert from one type to another. You can overload other operators to allow objects to be used in Boolean test expressions.

Just Because You Can Doesn’t Mean You Should

Overloading operators can make certain classes and structs more natural to use. However, overloading operators in a slipshod way can make code much more difficult to read and understand. You must be careful to consider the semantics of a type’s operators. Be careful not to introduce something that is hard to decipher. Always aim for the most readable code, not only for the next fortunate soul who claps eyes on your code, but also for yourself. Have you ever looked at code and wondered, “Who in their right mind wrote this stuff?!?” only to find out it was you? I know I have.

Another reason not to overload operators is that not all .NET languages support overloaded operators, because overloading operators is not part of the CLS. Languages that target the CLI aren’t required to support operator overloading. For example, Visual Basic 2005 is the first .NET version of the language that supports operator overloading. Therefore, it’s important that your overloaded operators be syntactic shortcuts to functionality provided by other methods that perform the same operation and can be called by CLS-compliant languages. In fact, I recommend that you design types as if overloaded operators don’t exist. Then, later on, you can add overloaded operators in such a way that they simply call the methods you defined that carry the same semantic meaning.

Types and Formats of Overloaded Operators

You define all overloaded operators as public static methods on the classes they’re meant to augment. Depending on the type of operator being overloaded, the method may accept either one or two parameters, and it always returns a value. For all operators except conversion operators, one of the parameter types must be of the same type as the enclosing type for the method. For example, it makes no sense to overload the + operator on class Complex if it adds two double values together, and, as you’ll see shortly, it’s impossible.

A typical + operator for a class Complex could look like the following:

public static Complex operator+( Complex lhs, Complex rhs )

Even though this method adds two instances of Complex together to produce a third instance of Complex, nothing says that one of the parameters cannot be that of type double, thus adding a double to a Complex instance. Now, how you add a double value to a Complex instance and produce

133

Соседние файлы в папке c#