14.3 Casting to an InterfaceYou can access the members (i.e., methods and properties) of an interface through the object of any class that implements the interface. Thus, you can access the methods and properties of IStorable, through the Document object: Document doc = new Document("Test Document"); doc.status = -1; doc.Read(); In Chapter 16, you'll learn that at times you won't know that you have a Document object; rather you'll know only that you have objects that implement IStorable. You can create a variable of type IStorable and cast your Document to that type. You can then access the IStorable methods through the IStorable variable. When you cast you say to the compiler, "trust me, I know this object is really of this type." In this case you are saying "trust me, I know this document really implements IStorable, so you can treat it as an IStorable." Casting is safe to do because the Document object implements IStorable and thus is safely treated as an IStorable object. You cast by placing the type you are casting to in parentheses. The following line declares a variable of type IStorable and assigns to that variable the Document object, cast to type IStorable: IStorable isDoc = (IStorable) doc; You can read this line as "cast doc to IStorable and assign the resulting IStorable object to the variable isDoc, which is declared to be of type IStorable." Note that the variable isDoc is now a reference to the same document, but that reference is of type IStorable and so has the methods and properties of IStorable. You are now free to use this IStorable variable to access the IStorable methods and properties of the document. isDoc.status = 0; isDoc.Read(); In these two lines of code you set the status property of the document to zero, and you call the Read() method of the document. You can do so because Status and Read() are members of the IStorable interface implemented by the document. You cannot instantiate an interface directly; that is, you cannot write: IStorable isDoc = new IStorable(); You can, however, create an instance of the implementing class and then create an instance of the interface by casting the implementing object to the interface type: IStorable isDoc = (IStorable) doc; isDoc is a reference to an IStorable object to which you've assigned the document cast to IStorable. Note that you can combine these steps by writing: IStorable isDoc = (IStorable) new Document("Test Document");
Thus far, you have cast the Document object (doc) to IStorable and assigned the result to the reference to an IStorable: isDoc. You knew this was safe to do because you defined the Document class to implement IStorable. However, there may be instances in which you do not know in advance (at compile time) that an object supports a particular interface. For instance, given a collection of objects, you might not know whether each object in the collection implements IStorable, ICompressible, or both. You can find out what interfaces are implemented by a particular object by casting blindly and then catching the exceptions that arise when you've tried to cast the object to an interface it hasn't implemented. The code to cast Document to ICompressible might be: Document doc = new Document("Test Document"); ICompressible icDoc = (ICompressible) doc; icDoc.Compress(); If Document implements only the IStorable interface but not the ICompressible interface: public class Document : IStorable the cast to ICompressible would still compile because ICompressible is a valid interface. However, because of the illegal cast, an exception will be thrown when the program is run: An exception of type System.InvalidCastException was thrown.
You could then catch the exception and take corrective action, but this approach is ugly and evil and you should not do things this way. This is like testing whether a gun is loaded by firing it; it's dangerous and it annoys the neighbors. Rather than firing blindly, you would like to be able to ask the object if it implements an interface, in order to then invoke the appropriate methods. C# provides two operators to help you ask the object if it implements an interface: the is operator and the as operator. The distinction between them is subtle but important. 14.3.1 The is OperatorThe is operator lets you query whether an object implements an interface. The form of the is operator is: expression is type The is operator evaluates true if the expression (which must be a reference type such as an instance of a class) can be safely cast to type (e.g., an Interface) without throwing an exception. Example 14-3 illustrates the use of the is operator to test whether a Document object implements the IStorable and ICompressible interfaces. Example 14-3. The is operatorusing System; namespace InterfaceDemo { 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 both interfaces public class Document : IStorable { // 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 { return status; } set { status = value; } } // hold the data for IStorable's Status property private int status = 0; } class Tester { public void Run() { Document doc = new Document("Test Document"); // only cast if it is safe if (doc is IStorable) { IStorable isDoc = (IStorable) doc; isDoc.Read(); } else { Console.WriteLine("Could not cast to IStorable"); } // this test will fail if (doc is ICompressible) { ICompressible icDoc = (ICompressible) doc; icDoc.Compress(); } else { Console.WriteLine("Could not cast to ICompressible"); } } [STAThread] static void Main() { Tester t = new Tester(); t.Run(); } } } Output: Creating document with: Test Document Implementing the Read Method for IStorable Could not cast to ICompressible In Example 14-3, the Document class implements only IStorable: public class Document : IStorable In the Run() method of the Tester class, you create an instance of Document: Document doc = new Document("Test Document"); and test whether that instance is an IStorable (that is, does it implement the IStorable interface?): if (doc is IStorable) If so, you cast Document to an IStorable, and you are now free to use the interface to call the methods of that interface. if (doc is IStorable) { IStorable isDoc = (IStorable) doc; isDoc.Read(); } Then repeat the test with ICompressible, and if the test fails, print an error message: if (doc is ICompressible) { ICompressible icDoc = (ICompressible) doc; icDoc.Compress(); } else { Console.WriteLine("Could not cast to ICompressible"); } The output shows that the first test (is IStorable) succeeds (as expected) and the second test (is ICompressible) fails (also as expected). Implementing the Read Method for IStorable Could not cast to ICompressible
14.3.2 The as OperatorThe is operator works, but it is not terribly efficient. There is a test done to evaluate the is operator, and another test done when you make the cast. That isn't a big deal if you are just casting a single object, but if you have a large collection of objects, it would be better to use a more efficient mechanism. The as operator combines the is evaluation 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 if 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, and you avoid the overhead of checking the cast twice. For these reasons, it is optimal to cast interfaces using as rather than is. The form of the as operator is: type instance = expression as type The expression is typically an object of a class that might implement the interface, and the type is typically an Interface. What is returned is either a reference to the type or null. For example, if the Document class (of which doc is an instance) does in fact implement ICompressible, then icDoc will be an ICompressible reference to the doc object. If the Document class does not implement ICompressible, icDoc will be null. ICompressible icDoc = doc as ICompressible The code in Example 14-4 replaces the Run() method in Example 14-3 and uses the as operator rather than the is operator. The rest of the example is unchanged and so is not reproduced here. Example 14-4. The as operatorpublic void Run() { Document doc = new Document("Test Document"); // cast using as, then test for null IStorable isDoc = doc as IStorable; if (isDoc != null) { isDoc.Read(); } else { Console.WriteLine("Could not cast to IStorable"); } // cast using as, then test for null ICompressible icDoc = doc as ICompressible; if (icDoc != null) { icDoc.Compress(); } else { Console.WriteLine("Could not cast to ICompressible"); } } |