8.2 Accessing Interface Methods

You 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 by casting the document to the interface type, and then use that interface to access the methods:

IStorable isDoc = (IStorable) 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.

As stated earlier, you cannot instantiate an interface directly. That is, you cannot 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 = (IStorable) doc;

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. Thus, it is 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 interchangeably. See Chapter 5 for more information about polymorphism.

8.2.1 Casting to an Interface

In 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 = new Document("Test Document");

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 would still compile because ICompressible is a valid interface. However, because of the illegal cast, when the program is run, an exception will be thrown:

An exception of type System.InvalidCastException was thrown.

Exceptions are covered in detail in Chapter 11.

8.2.2 The is Operator

You would like to be able to ask the object if it supports the interface, in order 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. Example 8-3 illustrates the use of the is operator to test whether a Document implements the IStorable and ICompressible interfaces.

Java programmers take note: The C# is operator is the equivalent of Java's instanceof.

Example 8-3. Using the is operator
using System;

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(
         "Implementing the Read Method for IStorable");        
   }

   // IStorable.Write
   public void Write(object o)
   {
      Console.WriteLine(
         "Implementing the Write Method for IStorable");  
   }

   // IStorable.Status
   public int Status
   {
      get
      {
         return status;
      }

      set
      {
         status = value;
      }
   }
}

public class Tester
{
 
   static void Main( )
   {
      Document doc = new Document("Test Document");

      
      // only cast if it is safe
      if (doc is IStorable)
      {
         IStorable isDoc = (IStorable) doc;
         isDoc.Read( );
      }

      // this test will fail
      if (doc is ICompressible)
      {
         ICompressible icDoc = (ICompressible) doc;
         icDoc.Compress( );

      }
   }
}

Example 8-3 differs from Example 8-2 in that Document no longer implements the ICompressible interface. Main( ) now determines whether the 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.

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 Operator

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

The keyword null represents a null referenceone that does not refer to any object.

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( )
{
    Document doc = new Document("Test Document");
    IStorable isDoc = doc as IStorable;
    if (isDoc != null)
        isDoc.Read( );
    else
        Console.WriteLine("IStorable not supported");
        
    ICompressible icDoc = doc as ICompressible;
    if (icDoc != null)
        icDoc.Compress( );
    else
        Console.WriteLine("Compressible not supported");
}

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 Operator

If your design pattern is to test the object to see if it is of the type you need, and if so you will 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 Class

Interfaces 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# does not 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) {...}
   // ...
}


    Part I: The C# Language