8.2. Accessing Interface MethodsYou can access the members of the IStorable interface as if they were members of the Document class: Document doc = new Document("Test Document"); doc.status = -1; doc.Read(); You can also create an instance of the interface[1] by casting the document to the interface type, and then use that interface to access the methods:
IStorable isDoc = doc; isDoc.status = 0; isDoc.Read( ); In this case, in Main( ) you know that Document is in fact an IStorable, so you can take advantage of that knowledge and not explicitly cast or test the cast. As stated earlier, you can't instantiate an interface directly. That is, you can't say: IStorable isDoc = new IStorable(); You can, however, create an instance of the implementing class, as in the following: Document doc = new Document("Test Document"); You can then create an instance of the interface by casting the implementing object to the interface type, which in this case is IStorable: IStorable isDoc = doc; You can combine these steps by writing: IStorable isDoc = new Document("Test Document"); Access through an interface allows you to treat the interface polymorphically. In other words, you can have two or more classes implement the interface, and then by accessing these classes only through the interface, you can ignore their real runtime type and treat them interchangeably. See Chapter 5 for more information about polymorphism. 8.2.1. Casting to an InterfaceIn many cases, you don't know in advance that an object supports a particular interface. Given a collection of objects, you might not know whether a particular object supports IStorable or ICompressible or both. You can just cast to the interfaces: Document doc = myCollection[0]; IStorable isDoc = (IStorable) doc; isDoc.Read( ); ICompressible icDoc = (ICompressible) doc; icDoc.Compress( ); If it turns out that Document implements only the IStorable interface: public class Document : IStorable the cast to ICompressible still compiles because ICompressible is a valid interface. However, because of the illegal cast, when the program is run, an exception is thrown: An exception of type System.InvalidCastException was thrown. Exceptions are covered in detail in Chapter 11. 8.2.2. The is OperatorYou would like to be able to ask the object if it supports the interface, to then invoke the appropriate methods. In C# there are two ways to accomplish this. The first method is to use the is operator. The form of the is operator is: expression is type The is operator evaluates true if the expression (which must be a reference type) can be safely cast to type without throwing an exception.[2] Example 8-3 illustrates the use of the is operator to test whether a Document implements the IStorable and ICompressible interfaces.
Example 8-3. Using the is operator#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace IsOperator { interface IStorable { void Read( ); void Write( object obj ); int Status { get; set; } } // here's the new interface interface ICompressible { void Compress( ); void Decompress( ); } // Document implements IStorable public class Document : IStorable { private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // IStorable.Read public void Read( ) { Console.WriteLine( "Reading..."); } // IStorable.Write public void Write( object o ) { Console.WriteLine( "Writing..."); } // IStorable.Status public int Status { get { return status; } set { status = value; } } } // derives from Document and implements ICompressible public class CompressibleDocument : Document, ICompressible { public CompressibleDocument(String s) : base(s) { } public void Compress( ) { Console.WriteLine("Compressing..."); } public void Decompress( ) { Console.WriteLine("Decompressing..."); } } public class Tester { static void Main( ) { // A collection of Documents Document[] docArray = new Document[2]; // First entry is a Document docArray[0] = new Document( "Test Document" ); // Second entry is a CompressibleDocument (ok because // CompressibleDocument is a Document) docArray[1] = new CompressibleDocument("Test compressibleDocument"); // don't know what we'll pull out of this hat foreach (Document doc in docArray) { // report your name Console.WriteLine("Got: {0}", doc); // Both pass this test if (doc is IStorable) { IStorable isDoc = (IStorable)doc; isDoc.Read( ); } // fails for Document // passes for CompressibleDocument if (doc is ICompressible) { ICompressible icDoc = (ICompressible)doc; icDoc.Compress( ); } } } } } Output: Creating document with: Test Document Creating document with: Test compressibleDocument Got: IsOperator.Document Reading... Got: IsOperator.CompressibleDocument Reading... Compressing... Example 8-3 differs from Example 8-2 in that Document no longer implements the ICompressible interface, but a class derived from Document named CompressibleDocument does. Main() checks whether each cast is legal (sometimes referred to as safe) by evaluating the following if clause: if (doc is IStorable) This is clean and nearly self-documenting. The if statement tells you that the cast will happen only if the object is of the right interface type. The Document class passes this test, but fails the next: if (doc is ICompressible) but the CompressibleDocument passes both tests. We put both types of documents into an array (you can imagine such an array being handed to a method which can't know its contents). Before you try to call the ICompressible methods, you must be sure that the type of Document you have does implement ICompressible. The is operator makes that test for you. Unfortunately, this use of the is operator turns out to be inefficient. To understand why, you need to dip into the MSIL code that this generates. Here is a small excerpt (note that the line numbers are in hexadecimal notation): IL_0023: isinst ICompressible IL_0028: brfalse.s IL_0039 IL_002a: ldloc.0 IL_002b: castclass ICompressible IL_0030: stloc.2 IL_0031: ldloc.2 IL_0032: callvirt instance void ICompressible::Compress( ) What is most important here is the test for ICompressible on line 23. The keyword isinst is the MSIL code for the is operator. It tests to see if the object (doc) is in fact of the right type. Having passed this test we continue on to line 2b, in which castclass is called. Unfortunately, castclass also tests the type of the object. In effect, the test is done twice. A more efficient solution is to use the as operator. 8.2.3. The as OperatorThe as operator combines the is and cast operations by testing first to see whether a cast is valid (i.e., whether an is test would return true) and then completing the cast when it is. If the cast is not valid (i.e., if an is test would return false), the as operator returns null.
Using the as operator eliminates the need to handle cast exceptions. At the same time you avoid the overhead of checking the cast twice. For these reasons, it is optimal to cast interfaces using as. The form of the as operator is: expression as type The following code adapts the test code from Example 8-3, using the as operator and testing for null: static void Main() { // A collection of Documents Document[] docArray = new Document[2]; // First entry is a Document docArray[0] = new Document( "Test Document" ); // Second entry is a CompressibleDocument (ok because // CompressibleDocument is a Document) docArray[1] = new CompressibleDocument("Test compressibleDocument"); // don't know what we'll pull out of this hat foreach (Document doc in docArray) { // report your name Console.WriteLine("Got: {0}", doc); // Both pass this test IStorable isDoc = doc as IStorable; if (isDoc != null) { isDoc.Read( ); } // fails for Document // passes for CompressibleDocument ICompressible icDoc = doc as ICompressible; if (icDoc != null) { icDoc.Compress( ); } } } A quick look at the comparable MSIL code shows that the following version is in fact more efficient: IL_0023: isinst ICompressible IL_0028: stloc.2 IL_0029: ldloc.2 IL_002a: brfalse.s IL_0034 IL_002c: ldloc.2 IL_002d: callvirt instance void ICompressible::Compress( ) 8.2.4. The is Operator Versus the as OperatorIf your design pattern is to test the object to see if it is of the type you need, and if so to immediately cast it, the as operator is more efficient. At times, however, you might want to test the type of an operator but not cast it immediately. Perhaps you want to test it but not cast it at all; you simply want to add it to a list if it fulfills the right interface. In that case, the is operator will be a better choice. 8.2.5. Interface Versus Abstract ClassInterfaces are very similar to abstract classes. In fact, you could change the declaration of IStorable to be an abstract class: abstract class Storable { abstract public void Read(); abstract public void Write( ); } Document could now inherit from Storable, and there would not be much difference from using the interface. Suppose, however, that you purchase a List class from a third-party vendor whose capabilities you wish to combine with those specified by Storable. In C++, you could create a StorableList class and inherit from both List and Storable. But in C#, you're stuck; you can't inherit from both the Storable abstract class and also the List class because C# doesn't allow multiple inheritance with classes. However, C# does allow you to implement any number of interfaces and derive from one base class. Thus, by making Storable an interface, you can inherit from the List class and also from IStorable, as StorableList does in the following example: public class StorableList : List, IStorable { // List methods here ... public void Read( ) {...} public void Write(object obj) {...} // ... }
|