Team LiB   Previous Section   Next Section

14.3 Casting to an Interface

You 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");

In general, it is a better design decision to access the interface methods through an interface reference rather than through an object of a class that implements the interface. Thus, it was better to use isDoc.Read() than doc.Read() in the previous example.

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 simply as instances of the interface. You'll see the power of this technique in Chapter 16.

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.

Exceptions are used to report errors and are covered in detail in Chapter 18.

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 Operator

The 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 operator
using 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

The compiler issues a warning when you compile Example 14-3:

c:...\class1.cs(64,15): warning CS0183: The given expression is 
always of the provided ('InterfaceDemo.IStorable') type

The example is contrived, and the compiler recognizes that at compile time you can know for certain that the IStorable test will always succeed. In later chapters you'll see how to add objects to a collection, and when you test objects from the collection it will not be possible to know at compile time whether the test will succeed or not.

14.3.2 The as Operator

The 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.

The keyword null represents a null reference, which does not refer to any object.

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 operator
public 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");
    }
}

As Versus Is

If your design pattern is to test the object to see if it is of the type you need (i.e., it implements the Interface you want to cast to) and if so you immediately cast it, the as operator is more efficient. At times, however, you might want to test the type of an object 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 is a better choice.

    Team LiB   Previous Section   Next Section