Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 50 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
50
Dung lượng
166,15 KB
Nội dung
132 Chapter 8 CHAPTER 8 Interfaces 8 An interface is a contract that guarantees to a client how a class or struct will behave (I’ll just use the term class for the rest of this chapter, though everything I say will apply to structs as well). When a class implements an interface, it tells any potential client “I guarantee I’ll support all the methods, properties, events, and indexers of the named interface.” (See Chapter 4 for information about methods and properties, Chapter 12 for infor- mation about events, and Chapter 9 for coverage of indexers.) See also the sidebar “Abstract Class Versus Interface Versus Mix-Ins.” These contracts are made manifest using the interface keyword, which declares a reference type that encapsulates the contract. When you define an interface, you may define methods, properties, indexers, and events that will (and must!) be implemented by any class that implements the interface. Java programmers take note: C# doesn’t support the use of constant fields (member constants) in interfaces. The closest analog is the use of enumerated constants (enums). In this chapter, you will learn how to create, implement, and use interfaces. You’ll learn how to implement multiple interfaces, and how to combine and extend inter- faces, as well as how to test whether a class has implemented an interface. Defining and Implementing an Interface The syntax for defining an interface is as follows: [attributes] [access-modifier] interface interface-name[:base-list] {interface-body} Don’t worry about attributes for now; I cover them in Chapter 20. Defining and Implementing an Interface | 133 I discussed access modifiers, including public, private, protected, internal, and protected internal, in Chapter 4. The interface keyword is followed by the name of the interface. It is common (but not required) to begin the name of your interface with a capital I (thus, IStorable, ICloneable, IClaudius, etc.). The base-list lists the interfaces that this interface extends (as described in the next section, “Implementing More Than One Interface”). The interface-body describes the methods, properties, and so forth that must be implemented by the implementing class. Suppose you wish to create an interface that describes the methods and properties a class needs, to be stored to and retrieved from a database or other storage such as a file. You decide to call this interface IStorable. In this interface, you might specify two methods: Read( ) and Write( ), which appear in the interface-body: interface IStorable { Abstract Class Versus Interface Versus Mix-Ins An interface offers an alternative to an abstract class for creating contracts among classes and their clients; the difference is that abstract classes serve as the top of an inheritance hierarchy, whereas interfaces may add their contract to numerous inherit- ance trees. Thus, for example, you might have an interface named IPrintable (by convention, interface names begin with a capital I, such as IPrintable, IStorable, IClaudius). IPrintable defines all the methods, events, and so on that a class must implement to be printable, and any number of classes (notes, documents, calendar items, email, spreadsheet documents) might implement that interface without having to share a common root element. Further, because a subset of these IPrintable types might also be IStorable, using interfaces rather than abstract classes keeps your inheritance tree much cleaner. This allows inheritance to define the is-a relationship (a note is a document) rather than the implements relationship (both notes and email implement IPrintable). Historical Note of Interest to East Coast Geeks: In Somerville, Massachusetts, there was, at one time, an ice cream parlor where you could have candies and other goodies “mixed in” with your chosen ice cream flavor. This seemed like a good metaphor to some of the object-oriented pioneers from nearby MIT who were working on the for- tuitously named SCOOPS programming language. They appropriated the term mix-in for classes that mixed in additional capabilities. These mix-in—or capability—classes serve much the same role as interfaces do in C#. 134 | Chapter 8: Interfaces void Read( ); void Write(object); } The purpose of an interface is to define the capabilities you want to have available in a class. For example, you might create a class, Document. It turns out that Document types can be stored in a database, so you decide to have Document implement the IStorable interface. To do so, use the same syntax as though the new Document class were inheriting from IStorable—a colon (:), followed by the interface name: public class Document : IStorable { public void Read( ) { } public void Write(object obj) { } // } It is now your responsibility, as the author of the Document class, to provide a mean- ingful implementation of the IStorable methods. Having designated Document as implementing IStorable, you must implement all the IStorable methods, or you will generate an error when you compile. I illustrate this in Example 8-1, in which the Document class implements the IStorable interface. Example 8-1. Using a simple interface using System; namespace SimpleInterface { interface IStorable { // no access modifiers, methods are public // no implementation void Read( ); void Write(object obj); int Status { get; set; } } // create a class which implements the IStorable interface public class Document : IStorable { public Document(string s) { Console.WriteLine("Creating document with: {0}", s); } Defining and Implementing an Interface | 135 Example 8-1 defines a simple interface, IStorable, with two methods (Read( ) and Write( )), and a property (Status) of type integer. Notice that the property declara- tion doesn’t provide an implementation for get and set, but simply designates that there is a get and a set: int Status { get; set; } Notice also that the IStorable method declarations don’t include access modifiers (e.g., public, protected, internal, private). In fact, providing an access modifier generates a compile error. Interface methods are implicitly public because an interface is a contract // implement the Read method public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable"); } // implement the Write method public void Write(object o) { Console.WriteLine( "Implementing the Write Method for IStorable"); } public int Status { get; set; } } // Take our interface out for a spin public class Tester { static void Main( ) { // access the methods in the Document object Document doc = new Document("Test Document"); doc.Status = -1; doc.Read( ); Console.WriteLine("Document Status: {0}", doc.Status); } } } Output: Creating document with: Test Document Implementing the Read Method for IStorable Document Status: -1 Example 8-1. Using a simple interface (continued) 136 | Chapter 8: Interfaces meant to be used by other classes. You can’t create an instance of an interface; instead, you instantiate a class that implements the interface. The class implementing the interface must fulfill the contract exactly and com- pletely. Document must provide both a Read( ) and a Write( ) method and the Status property. How it fulfills these requirements, however, is entirely up to the Document class. Although IStorable dictates that Document must have a Status property, it doesn’t know or care whether Document stores the actual status as a member variable or looks it up in a database. The details are up to the implementing class. Implementing More Than One Interface Classes can implement more than one interface. For example, if your Document class can be stored and it also can be compressed, you might choose to implement both the IStorable and ICompressible interfaces, shown here: interface ICompressible { void Compress( ); void Decompress( ); } To do so, change the declaration (in the base list) to indicate that both interfaces are implemented, separating the two interfaces with commas: public class Document : IStorable, ICompressible Having done this, the Document class must also implement the methods specified by the ICompressible interface: public void Compress( ) { Console.WriteLine("Implementing the Compress Method"); } public void Decompress( ) { Console.WriteLine("Implementing the Decompress Method"); } Extending Interfaces It is possible to extend an existing interface to add new methods or members, or to modify how existing members work. For example, you might extend ICompressible with a new interface, ILoggedCompressible, which extends the original interface with methods to keep track of the bytes saved: interface ILoggedCompressible : ICompressible { void LogSavedBytes( ); } Defining and Implementing an Interface | 137 Effectively, by extending ICompressible in this way, you are saying that anything that implements ILoggedCompressible must also implement ICompressible. Classes are now free to implement either ICompressible or ILoggedCompressible, depending on whether they need the additional functionality. If a class does implement ILoggedCompressible, it must implement all the methods of both ILoggedCompressible and ICompressible. Objects of that type can be cast to ILoggedCompressible or to ICompressible. Combining Interfaces Similarly, you can create new interfaces by combining existing interfaces and, optionally, adding new methods or properties. For example, you might decide to cre- ate IStorableCompressible. This interface would combine the methods of each of the other two interfaces, but would also add a new method to store the original size of the precompressed item: interface IStorableCompressible : IStorable, ILoggedCompressible { void LogOriginalSize( ); } Example 8-2 illustrates extending and combining interfaces. Example 8-2. Extending and combining interfaces using System; namespace ExtendAndCombineInterface { interface IStorable { void Read( ); void Write(object obj); int Status { get; set; } } // here's the new interface interface ICompressible { void Compress( ); void Decompress( ); } // Extend the interface interface ILoggedCompressible : ICompressible { void LogSavedBytes( ); } 138 | Chapter 8: Interfaces // Combine Interfaces interface IStorableCompressible : IStorable, ILoggedCompressible { void LogOriginalSize( ); } // yet another interface interface IEncryptable { void Encrypt( ); void Decrypt( ); } public class Document : IStorableCompressible, IEncryptable { // hold the data for IStorable's Status property private int status = 0; // the document constructor public Document(string s) { Console.WriteLine("Creating document with: {0}", s); } // implement IStorable public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable"); } public void Write(object o) { Console.WriteLine( "Implementing the Write Method for IStorable"); } public int Status { get; set; } // implement ICompressible public void Compress( ) { Console.WriteLine("Implementing Compress"); } public void Decompress( ) { Console.WriteLine("Implementing Decompress"); } Example 8-2. Extending and combining interfaces (continued) Defining and Implementing an Interface | 139 // implement ILoggedCompressible public void LogSavedBytes( ) { Console.WriteLine("Implementing LogSavedBytes"); } // implement IStorableCompressible public void LogOriginalSize( ) { Console.WriteLine("Implementing LogOriginalSize"); } // implement IEncryptable public void Encrypt( ) { Console.WriteLine("Implementing Encrypt"); } public void Decrypt( ) { Console.WriteLine("Implementing Decrypt"); } } public class Tester { static void Main( ) { // create a document object Document doc = new Document("Test Document"); doc.Read( ); doc.Compress( ); doc.LogSavedBytes( ); doc.Compress( ); doc.LogOriginalSize( ); doc.LogSavedBytes( ); doc.Compress( ); doc.Read( ); doc.Encrypt( ); } } } Output Creating document with: Test Document Implementing the Read Method for IStorable Implementing Compress Implementing LogSavedBytes Implementing Compress Example 8-2. Extending and combining interfaces (continued) 140 | Chapter 8: Interfaces Polymorphism with Interfaces The problem with the approach we’ve taken so far is that you could well have a collec- tion of Document objects, some implementing IStorable, some implementing ICompressible, some implementing ILoggedCompressible, some implementing IStorableCompressible, and some implementing IEncryptable. If you just call methods from each interface, sooner or later you’re going to throw an exception. Let’s build such an example slowly, because this problem is very real, very confus- ing, and very likely to cause a nasty bug in your program if it isn’t fully understood. Start by declaring the interfaces just as you did in the previous example (I won’t repeat them here). Next, rather than declaring a simple Document class, let’s declare an abstract Document class, and two derived Document classes: public abstract class Document { } public class BigDocument : Document, IStorableCompressible, IEncryptable { // } The implementation of BigDocument is identical to the implementation of Document in the previous example. There’s no change whatsoever, except that the constructor must be named BigDocument, and note that it now inherits from our abstract class. Finally, let’s add a smaller type of Document: class LittleDocument : Document, IEncryptable { public LittleDocument(string s) { Console.WriteLine("Creating document with: {0}", s); } void IEncryptable.Encrypt( ) { Console.WriteLine("Implementing Encrypt"); } void IEncryptable.Decrypt( ) { Console.WriteLine("Implementing Decrypt"); } } Implementing LogOriginalSize Implementing LogSavedBytes Implementing Compress Implementing the Read Method for IStorable Implementing Encrypt Example 8-2. Extending and combining interfaces (continued) Defining and Implementing an Interface | 141 Notice that LittleDocument also inherits from Document, but it implements only one interface: IEncryptable. Let’s change Main, now to create a collection of Documents: for (int i = 0; i < 5; i++) { if (i % 2 == 0) { folder[i] = new BigDocument("Big Document # " + i); } else { folder[i] = new LittleDocument("Little Document # " + i); } } We create five documents, with the even-numbered ones being “big” and the odd- numbered ones being “little.” If you now iterate through the “folder” (the array of Document objects) and try to call various methods of the interface, you have a problem: foreach (Document doc in folder) { doc.Read( ); doc.Compress( ); doc.LogSavedBytes( ); doc.Compress( ); doc.LogOriginalSize( ); doc.LogSavedBytes( ); doc.Compress( ); doc.Read( ); doc.Encrypt( ); } This won’t compile—nor should it. The compiler cannot know which kind of Document it has: a BigDocument (which can Read and Compress), or a LittleDocument (which can’t). To solve this problem, we need to see whether the Document in question implements the interface we want to use, as shown in Example 8-3. Example 8-3. Collections of Documents using System; namespace ExtendAndCombineInterface { interface IStorable { void Read( ); void Write(object obj); int Status { get; set; } } [...]... Console.WriteLine("Implementing ITalk.Talk"); } } public class Tester { static void Main( ) { // create a document object Document theDoc = new Document("Test Document"); IStorable isDoc = theDoc; isDoc.Read( ); ITalk itDoc = theDoc; itDoc.Read( ); theDoc.Read( ); theDoc.Talk( ); } } } Output: Creating document with: Test Document Implementing IStorable.Read Implementing ITalk.Read Implementing IStorable.Read Implementing... void Main( ) { Document[] folder = for (int i = 0; i < { if (i % 2 == 0) { folder[i] = } else { folder[i] = } } new Document[5]; 5; i++) new BigDocument("Big Document # " + i); new LittleDocument("Little Document # " + i); foreach (Document doc in folder) { // cast the document to the various interfaces IStorable isStorableDoc = doc as IStorable; if (isStorableDoc != null) { isStorableDoc.Read( ); }... Console.WriteLine("IStorable not supported"); ICompressible icDoc = doc as ICompressible; if (icDoc != null) { icDoc.Compress( ); } else Console.WriteLine("Compressible not supported"); ILoggedCompressible ilcDoc = doc as ILoggedCompressible; if (ilcDoc != null) { ilcDoc.LogSavedBytes( ); ilcDoc.Compress( ); // ilcDoc.Read( ); } 144 | Chapter 8: Interfaces Example 8-3 Collections of Documents (continued) else Console.WriteLine("LoggedCompressible... Console.WriteLine("Encryptable not supported"); } } } } // // // // end end end end for main class namespace Output: Creating document with: Big Document # 0 Creating document with: Little Document # 1 Creating document with: Big Document # 2 Creating document with: Little Document # 3 Creating document with: Big Document # 4 Implementing the Read Method for IStorable Implementing Compress Implementing LogSavedBytes Implementing... IStorable { // the document constructor public Document(string s) { Console.WriteLine( "Creating document with: {0}", s); } // Make read virtual public virtual void Read( ) { Console.WriteLine( "Document Read Method for IStorable"); } // NB: Not virtual! public void Write( ) { Console.WriteLine( "Document Write Method for IStorable"); } } // Derive from Document public class Note : Document { public... interface IEncryptable { void Encrypt( ); void Decrypt( ); } public abstract class Document { } public class BigDocument : Document, IStorableCompressible, IEncryptable { // hold the data for IStorable's Status property private int status = 0; // the document constructor public BigDocument(string s) { Console.WriteLine("Creating document with: {0}", s); } // implement IStorable public void Read( ) { Console.WriteLine(... note2.Read( ); note2.Write( ); } } } Output: Creating document with: Test Note Creating note with: Test Note Overriding the Read method for Note! Document Write Method for IStorable Overriding the Read method for Note! Document Write Method for IStorable Creating document with: Second Test Creating note with: Second Test Overriding the Read method for Note! Document Write Method for IStorable Overriding the... ExplicitImplementation { interface IStorable { void Read( ); void Write( ); } interface ITalk { void Talk( ); void Read( ); } // Modify Document to implement IStorable and ITalk public class Document : IStorable, ITalk { // the document constructor public Document(string s) { Console.WriteLine("Creating document with: {0}", s); } // Make read virtual public virtual void Read( ) { Console.WriteLine("Implementing IStorable.Read");... Decrypt( ) { Console.WriteLine("Implementing Decrypt"); } } class LittleDocument : Document, IEncryptable { public LittleDocument(string s) { Console.WriteLine("Creating document with: {0}", s); } void IEncryptable.Encrypt( ) { Console.WriteLine("Implementing Encrypt"); } Defining and Implementing an Interface | 143 Example 8-3 Collections of Documents (continued) void IEncryptable.Decrypt( ) { Console.WriteLine("Implementing... interface, it can make a cast, but when using your document as a Document, the API will not include Read( ) and Write( ) In fact, you can select which methods to make visible through explicit implementation so that you can expose some implementing methods as part of Document but not others In Example 8-5, the Document object exposes the Talk( ) method as a method of Document, but the ITalk.Read( ) method can . create a document object Document doc = new Document("Test Document"); doc. Read( ); doc. Compress( ); doc. LogSavedBytes( ); doc. Compress( ); doc. LogOriginalSize( ); doc. LogSavedBytes(. namespace Output: Creating document with: Big Document # 0 Creating document with: Little Document # 1 Creating document with: Big Document # 2 Creating document with: Little Document # 3 Creating document with: Big Document. the methods in the Document object Document doc = new Document("Test Document"); doc. Status = -1; doc. Read( ); Console.WriteLine("Document Status: {0}", doc. Status); }