Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 59 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
59
Dung lượng
1,29 MB
Nội dung
CHAPTER 5 ■ INTERFACES AND CONTRACTS 148 } } In this example, note a couple of things. First, FancyComboBox lists IUIControl in its inheritance list. That’s how you indicate that FancyComboBox is planning to reimplement the IUIControl interface. Had IUIControl inherited from another interface, FancyComboBox would have had to reimplement the methods from those inherited interfaces as well. I also had to use the new keyword for FancyComboBox.Paint, because it hides CombBox.Paint. This wouldn’t have been a problem had ComboBox implemented the IUIControl.Paint method explicitly, because it wouldn’t have been part of the ComboBox public contract. When the compiler matches class methods to interface methods, it also considers public methods of base classes. In reality, FancyComboBox could have indicated that it reimplements IUIControl but without redeclaring any methods, as the compiler would have just wired up the interface to the base class methods. Of course, doing so would be pointless, because the reason you reimplement an interface in a derived class is to modify behavior. ■ Note The ability to reimplement an interface is a powerful one. It highlights the vast differences between the way C# and the CLR handle interfaces and the C++ treatment of interfaces as abstract class definitions. Gone are the intricacies of C++ vtables, as well as the question of when you should use C++ virtual inheritance. As I’ve said before, and don’t mind saying again, C#/CLR interfaces are nothing more than contracts that say, “You, Mr. Concrete Class, agree to implement all of these methods in said contract, a.k.a. interface.” When you implement methods in an interface contract implicitly, they must be publicly accessible. As long as they meet those requirements, they can also have other attributes, including the virtual keyword. In fact, implementing the IUIControl interface in ComboBox using virtual methods as opposed to nonvirtual methods would make the previous problem a lot easier to solve, as demonstrated in the following: using System; public interface IUIControl { void Paint(); void Show(); } public interface IEditBox : IUIControl { void SelectText(); } public interface IDropList : IUIControl { void ShowList(); } public class ComboBox : IEditBox, IDropList { CHAPTER 5 ■ INTERFACES AND CONTRACTS 149 public virtual void Paint() { Console.WriteLine( "ComboBox.Paint()" ); } public void Show() { } public void SelectText() { } public void ShowList() { } } public class FancyComboBox : ComboBox { public override void Paint() { Console.WriteLine( "FancyComboBox.Paint()" ); } } public class EntryPoint { static void Main() { FancyComboBox cb = new FancyComboBox(); cb.Paint(); ((IUIControl)cb).Paint(); ((IEditBox)cb).Paint(); } } In this case, FancyComboBox doesn’t have to reimplement IUIControl. It merely has to override the virtual ComboBox.Paint method. It’s much cleaner for ComboBox to declare Paint virtual in the first place. Any time you have to use the new keyword to keep the compiler from warning you about hiding a method, consider whether the method of the base class should be virtual. ■ Caution Hiding methods causes confusion and makes code hard to follow and debug. Again, just because the language allows you to do something does not mean that you should. Of course, the implementer of ComboBox would have had to think ahead and realize that someone might derive from ComboBox, and anticipated these issues. In my opinion, it’s best to seal the class and avoid any surprises by people who attempt to derive from your class when you never meant for it to be derived from. Imagine who they will scream at when they encounter a problem. Have you ever used Microsoft Foundation Classes (MFC) in the past and come to a point where you’re pulling your hair out because you’re trying to derive from an MFC class and wishing a particular method were virtual? In that case, it’s easy to blame the designers of MFC for being so flagrantly thoughtless and not making the method virtual when, in reality, it’s more accurate to consider the fact that they probably never meant for you to derive from the class in the first place. Chapter 13 describes how containment rather than inheritance is the key in situations like these. CHAPTER 5 ■ INTERFACES AND CONTRACTS 150 Beware of Side Effects of Value Types Implementing Interfaces All the examples so far have shown how classes may implement interface methods. In fact, value types can implement interfaces as well. However, there’s one major side effect to doing so. If you cast a value type to an interface type, you’ll incur a boxing penalty. Even worse, if you modify the value via the interface reference, you’re modifying the boxed copy and not the original. Given the intricacies of boxing that I cover in Chapters 4 and 13, you may consider that to be a bad thing. As an example, consider System.Int32. I’m sure you’ll agree that it is one of the most basic types in the CLR. However, you may or may not have noticed that it also implements several interfaces: IComparable, IFormattable, and IConvertible. Consider System.Int32’s implementation of IConvertible, for example. All of the methods are implemented explicitly. IConvertible has quite a few methods declared within it. However, none of those are in the public contract of System.Int32. If you want to call one of those methods, you must first cast your Int32 value type into an IConvertible interface reference. Only then may you call one of the IConvertible methods. And of course, because interface-typed variables are references, the Int32 value must be boxed. PREFER THE CONVERT CLASS OVER ICONVERTIBLE Even though I use the IConvertible interface implemented by a value type as an example to prove a point, the documentation urges you not to call the methods of IConvertible on Int32; rather, it recommends using the Convert class instead. The Convert class provides a collection of methods with many overloads of common types for converting a value to just about anything else, including custom types (by using Convert.ChangeType), and it makes your code easier to change later. For example, if you have the following int i = 0; double d = Int32.ToDouble(i); and you want to change the type of i to long, you have to also change the Int32 type to Int64. On the other hand, if you write int i = 0; double d = Convert.ToDouble(i); then all you have to do is change the type of i. Interface Member Matching Rules Each language that supports interface definitions has rules about how it matches up method implementations with interface methods. The interface member matching rules for C# are pretty straightforward and boil down to some simple rules. However, to find out which method actually gets called at runtime, you need to consider the rules of the CLR as well. These rules are only relevant at compile time. Suppose you have a hierarchy of classes and interfaces. To find the implementation for SomeMethod on ISomeInterface, start at the bottom of the hierarchy and search for the first type that implements the interface in question. In this case, that interface is ISomeInterface. This is the level at which the search for a matching method begins. Once you find the type, recursively move up through CHAPTER 5 ■ INTERFACES AND CONTRACTS 151 the type hierarchy and search for a method with the matching signature, while first giving preference to explicit interface member implementations. If you don’t find any, look for public instance methods that match the same signature. The C# compiler uses this algorithm when matching up method implementations with interface implementations. The method that it picks must be a public instance method or an explicitly implemented instance method, and it may or may not be tagged in C# as virtual. However, when the IL code is generated, all interface method calls are made through the IL callvirt instruction. So, even though the method is not necessarily marked as virtual in the C# sense, the CLR treats interface calls as virtual. Be sure that you don’t confuse these two concepts. If the method is marked as virtual in C# and has methods that override it in the types below it, the C# compiler will generate vastly different code at the point of call. Be careful, as this can be quite confusing, as shown by the following contrived example: using System; public interface IMyInterface { void Go(); } public class A : IMyInterface { public void Go() { Console.WriteLine( "A.Go()" ); } } public class B : A { } public class C : B, IMyInterface { public new void Go() { Console.WriteLine( "C.Go()" ); } } public class EntryPoint { static void Main() { B b1 = new B(); C c1 = new C(); B b2 = c1; b1.Go(); c1.Go(); b2.Go(); ((I)b2).Go(); } } The output from this example is as follows: CHAPTER 5 ■ INTERFACES AND CONTRACTS 152 A.Go() C.Go() A.Go() C.Go() The first call, on b1, is obvious, as is the second call on c1. However, the third call, on b2, is not obvious at all. Because the A.Go method is not marked as virtual, the compiler generates code that calls A.Go. The fourth and final call is almost equally confusing, but not if you consider the fact that the CLR handles virtual calls on class type references and calls on interface references significantly differently. The generated IL for the fourth call makes a call to IMyInterface.Go, which, in this case, boils down to a call to C.Go, because b2 is actually a C, and C reimplements IMyInterface. You have to be careful when searching for the actual method that gets called, because you must consider whether the type of your reference is a class type or an interface type. The C# compiler generates IL virtual method calls in order to call through to interfaces methods, and the CLR uses interface tables internally to achieve this. ■ Note C++ programmers must realize that interface tables are different from C++ vtables. Each CLR type only has one method table, whereas a C++ instance of a type may have multiple vtables. The contents of these interface tables are defined by the compiler using its method-matching rules. For more detailed information regarding these interface tables, see Essential .NET, Volume I: The Common Language Runtime by Don Box and Chris Sells (Boston, MA: Addison-Wesley Professional, 2002), as well as the CLI standard document itself. The C# method-matching rules explain the situation I discussed previously in the section “Interface Inheritance and Member Hiding.” Hiding a method in one hierarchical path of a diamond-shaped hierarchy hides the method in all inheritance paths. The rules state that when you walk up the hierarchy, you short-circuit the search once you find a method at a particular level. These simple rules also explain how interface reimplementation can greatly affect the method-matching process, thus short-circuiting the compiler’s search during its progression up the hierarchy. Let’s consider an example of this in action: using System; public interface ISomeInterface { void SomeMethod(); } public interface IAnotherInterface : ISomeInterface { void AnotherMethod(); } CHAPTER 5 ■ INTERFACES AND CONTRACTS 153 public class SomeClass : IAnotherInterface { public void SomeMethod() { Console.WriteLine( "SomeClass.SomeMethod()" ); } public virtual void AnotherMethod() { Console.WriteLine( "SomeClass.AnotherMethod()" ); } } public class SomeDerivedClass : SomeClass { public new void SomeMethod() { Console.WriteLine( "SomeDerivedClass.SomeMethod()" ); } public override void AnotherMethod() { Console.WriteLine( "SomeDerivedClass.AnotherMethod()" ); } } public class EntryPoint { static void Main() { SomeDerivedClass obj = new SomeDerivedClass(); ISomeInterface isi = obj; IAnotherInterface iai = obj; isi.SomeMethod(); iai.SomeMethod(); iai.AnotherMethod(); } } Let’s apply the search rules to each method call in Main in the previous example. In all cases, I’ve implicitly converted an instance of SomeDerivedClass to references of the two interfaces, ISomeInterface and IAnotherInterface. I place the first call to SomeMethod through ISomeInterface. First, walk up the class hierarchy, starting at the concrete type of the reference, looking for the first class that implements this interface or an interface derived from it. Doing so leaves us at the SomeClass implementation, because, even though it does not implement ISomeInterface directly, it implements IAnotherInterface, which derives from ISomeInterface. Thus, we end up calling SomeClass.SomeMethod. You may be surprised that SomeDerivedClass.SomeMethod was not called. But if you follow the rules, you’ll notice that you skipped right over SomeDerivedClass, looking for the bottom-most class in the hierarchy that implements the interface. In order for SomeDerivedClass.SomeMethod to be called instead, SomeDerivedClass would need to reimplement ISomeInterface. The second call to SomeMethod through the IAnotherInterface reference follows exactly the same path when finding the matching method. Things get interesting in the third call in Main, where you call AnotherMethod through a reference to IAnotherInterface. As before, the search begins at the bottom-most class in the hierarchy that implements this interface, inside SomeClass. Because SomeClass has a matching method signature, your search is complete. However, the twist is that the matching method signature is declared virtual. So when the call is made, the virtual method mechanism places execution within CHAPTER 5 ■ INTERFACES AND CONTRACTS 154 SomeDerivedClass.AnotherMethod. It’s important to note that AnotherMethod doesn’t change the rules for interface method matching, even though it is implemented virtually. It’s not until after the interface method has been matched that the virtual nature of the method has an impact on exactly which implementation gets called at runtime. ■ Note Interface method matching is applied statically at compile time. Virtual method dispatching happens dynamically at runtime. You should note the difference between the two when trying to determine which method implementation gets invoked. The output from the previous example code is as follows: SomeClass.SomeMethod() SomeClass.SomeMethod() SomeDerivedClass.AnotherMethod() Explicit Interface Implementation with Value Types Many times, you’ll encounter general-use interfaces that take parameters in the form of a reference to System.Object. These interfaces are typically general usage, nongeneric interfaces. For example, consider the IComparable interface, which looks like the following: public interface IComparable { int CompareTo( object obj ); } ■ Note NET 2.0 added support for IComparable<T>, which you should always consider using along with IComparable in order to offer greater type safety. It makes sense that the CompareTo method accepts such a general type, because it would be nice to be able to pass it just about anything to see how the object passed in compares to the one that implements CompareTo. When dealing strictly with reference types, there’s really no loss of efficiency here, because conversion to and from System.Object on reference types is free for all practical purposes. But things get a little sticky when you consider value types. Let’s look at some code to see the gory details: CHAPTER 5 ■ INTERFACES AND CONTRACTS 155 using System; public struct SomeValue : IComparable { public SomeValue( int n ) { this.n = n; } public int CompareTo( object obj ) { if( obj is SomeValue ) { SomeValue other = (SomeValue) obj; return n - other.n; } else { throw new ArgumentException( "Wrong Type!" ); } } 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 the innocuous call to WriteLine in Main, you see val1 being compared to val2. But look closely at how many boxing operations are required. First, because CompareTo takes an object reference, val2 must be boxed at the point of the method call. Had you implemented the CompareTo method explicitly, you would have needed to cast the val1 value into an IComparable interface, which would incur a boxing penalty. But once you’re inside the CompareTo method, the boxing nightmare is still not overdue to the amount of unboxing necessary. Ouch. Thankfully, you can employ an optimization when SomeValue is compared to certain types. Take, for example, the case where an instance of SomeValue is compared to another SomeValue instance. You can provide a type-safe version of the CompareTo method to get the job done, as shown in the following code: using System; public struct SomeValue : IComparable { public SomeValue( int n ) { this.n = n; } int IComparable.CompareTo( object obj ) { if( obj is SomeValue ) { SomeValue other = (SomeValue) obj; CHAPTER 5 ■ INTERFACES AND CONTRACTS 156 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, because 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 typeless 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 typeless 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 attention to detail and commitment 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 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 a single vendor’s WiFi card were the only one that worked at your favorite Pacific Northwest- based coffee shops? 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 modified. Most of the time, devices that implement the new version also CHAPTER 5 ■ INTERFACES AND CONTRACTS 157 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 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 exactly the same idiom in use in the COM world. Typically, if someone such as 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 any 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 runtime. However, this practice can become confusing to the developers using your interface, because 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 a 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 all 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 two-dimensional Point structure. It starts to look as though you can model this system by the following code: using System; [...]... 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, because introducing two IMyOperations interfaces that differ only by version number of the containing... to create an object for use withNET 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... following code: 9.2233720368 547 8E+18 9.2233720368 547 8E+18 False True Before, you cry foul, I would like to point out that floating point numbers are one of those very tricky, but often trivialized areas of programming Moreover, when dealing with floating point numbers, comparison often only considers the significant digits of the floating point numbers leaving the rest within the acceptable margin... instance and produce another Complex instance is for you to decipher In general, operator overloading syntax follows the previous pattern, with the + replaced with the operator du jour, and of course, some operators accept only one parameter ■ Note When comparing C# operators with C++ operators, note that C# operator declarations are more similar to the friend function technique of declaring C++ operators... 3.0 ); Complex cpx2 = new Complex( 1.0, 2.0 ); Complex cpx3 = cpx1 + cpx2; Complex cpx4 = 20.0 + cpx1; Complex cpx5 = cpx1 + 25.0; Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine( Console.WriteLine( "cpx1 "cpx2 "cpx3 "cpx4 "cpx5 == == == == == {0}", {0}", {0}", {0}", {0}", cpx1 cpx2 cpx3 cpx4 cpx5 ); ); ); ); ); } } Notice that, as recommended, the overloaded operator methods... is done with a simple assignment, whereas explicit conversion requires the familiar casting syntax with the target type of the conversion provided in parentheses immediately preceding the instance being assigned from There is an important restriction on implicit operators The C# standard requires that implicit operators do not throw exceptions and that they’re always guaranteed to succeed with no loss... type with larger storage to a type with smaller storage may result in a truncation error if the original value is too large to be represented by the smaller type For example, if you explicitly cast a long into a short, you may trigger an overflow situation By default, your compiled code will 1 I describe this guideline in more detail in Chapter 5 in the section, “Explicit Interface Implementation with. .. operators to make sure that you don’t open up users to any surprises or confusion with your implicit conversions It’s difficult to introduce confusion with explicit operators when the users of your type must use the casting syntax to get it to work After all, how can users be surprised when they must provide the type to convert to within parentheses? On the other hand, inadvertent use or misguided use of... change, or the code within it must do a cast to IOperations2, which could fail at runtime Because 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... thus not all NET languages support overloaded operators Therefore, it’s important to always provide explicitly named methods that provide the same functionality Sometimes, those methods are already defined in system interfaces, such as IComparable or IComparable Never isolate functionality strictly within overloaded operators unless you’re 100% sure that your code will be consumed by NET languages . c1 = new C(); B b2 = c1; b1.Go(); c1.Go(); b2.Go(); ((I)b2).Go(); } } The output from this example is as follows: CHAPTER 5 ■ INTERFACES AND CONTRACTS 1 52 A.Go() C.Go(). ); SomeValue val2 = new SomeValue( 2 ); Console.WriteLine( val1.CompareTo(val2) ); } } In the innocuous call to WriteLine in Main, you see val1 being compared to val2. But look closely. those manufacturers who want to achieve maximum market saturation. In the 8 02. 11 example, 8 02. 11a, 8 02. 11b, and 8 02. 11g represent the various revisions of the standard. The point of this example