Chapter 8. Interfaces

An interface is a contract that guarantees to a client how a class or struct will behave. When a class (or struct) implements an interface, it tells any potential client “I guarantee I’ll support the methods, properties, events, and indexers of the named interface.” (See Chapter 4 for information about methods and properties, Chapter 12 for information about events, and Chapter 9 for coverage of indexers.)

An interface offers an alternative to an abstract class for creating contracts among classes and their clients. 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/or events that will be implemented by the class that implements the interface.

Interfaces are often compared to abstract classes. An abstract class serves as the base class for a family of derived classes, while interfaces are meant to be mixed in with other inheritance trees.

Tip

For the rest of this chapter, wherever you see the word class, assume the text applies equally to structs, unless noted otherwise.

When a class implements an interface, it must implement all the parts of that interface (methods, properties, etc.); in effect, the class says “I agree to fulfill the contract defined by this interface.”

Tip

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

You will remember from Chapter 5 that inheriting from an abstract class implements the is-a relationship. Implementing an interface, on the other hand, defines a different relationship that we’ve not seen until now, called (not surprisingly) the implements relationship. These two relationships are subtly different. A car is-a vehicle, but it might implement the CanBeBoughtWithABigLoan capability (as can a house, for example).

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 interfaces, 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; they’re covered in Chapter 18.

Access modifiers, including public, private, protected, internal, and protected internal, were discussed 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, Section 8.1.1).

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
{
   void Read();
   void Write(object);
}

The purpose of an interface is to define the capabilities that 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 if 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 meaningful 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. This is illustrated in Example 8-1, in which the Document class implements the IStorable interface.

Example 8-1. Using a simple interface

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace SimpleInterface
{
// declare the interface

   interfaceIStorable
   {
      // 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
   {

      // store the value for the property
      private int status = 0;

      public Document( string s )
      {
         Console.WriteLine( "Creating document with: {0}", s );
      }

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

      // implement the property
      public int Status
      {
         get
         {
            return status;
         }

         set
         {
            status = value;
         }
      }
   }

// 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 defines a simple interface, IStorable, with two methods (Read() and Write()) and a property (Status) of type integer. Notice that the property declaration 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 (for example, 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 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 completely. 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. 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 (which is declared in Example 8-2):

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();
}

Tip

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 either 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 create 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

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ExtendAndCombineInterface
{
   interfaceIStorable
   {
      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( );
   }

// 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
         {
            return status;
         }

         set
         {
            status = value;
         }
      }

      // implement ICompressible
      public void Compress( )
      {
         Console.WriteLine( "Implementing Compress" );
      }

      public void Decompress( )
      {
         Console.WriteLine( "Implementing Decompress" );
      }

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

         // cast the document to the various interfaces
         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" );

         ILoggedCompressible ilcDoc = doc as ILoggedCompressible;
         if ( ilcDoc != null )
         {
            ilcDoc.LogSavedBytes( );
            ilcDoc.Compress( );
            // ilcDoc.Read( );
         }
         else
            Console.WriteLine( "LoggedCompressible not supported" );

         IStorableCompressible isc = doc as IStorableCompressible;
         if ( isc != null )
         {
            isc.LogOriginalSize( );  // IStorableCompressible
            isc.LogSavedBytes( );    // ILoggedCompressible
            isc.Compress( );         // ICompressible
            isc.Read( );             // IStorable

         }
         else
         {
            Console.WriteLine( "StorableCompressible not supported" );
         }

         IEncryptable ie = doc as IEncryptable;
         if ( ie != null )
         {
            ie.Encrypt( );
         }
         else
            Console.WriteLine( "Encryptable not supported" );
      }
   }
}

Output:
Creating document with: Test Document
Implementing the Read Method for IStorable
Implementing Compress
Implementing LogSavedBytes
Implementing Compress
Implementing LogOriginalSize
Implementing LogSavedBytes
Implementing Compress
Implementing the Read Method for IStorable
Implementing Encrypt

Example 8-2 starts by implementing the IStorable interface and the ICompressible interface. The latter is extended to ILoggedCompressible and then the two are combined into IStorableCompressible. Finally, the example adds a new interface, IEncryptable.

The Tester program creates a new Document object and then uses it as an instance of the various interfaces. You are free to cast:

ICompressible icDoc = doc as ICompressible;

But this is unnecessary. The compiler knows that doc implements ICompressible and so can make the implicit cast for you:

ICompressible icDoc = doc;

On the other hand, if you are uncertain whether your class does implement a specific interface, you can cast using the as operator (described in detail later in this chapter), and then test whether the cast object is null (indicating that the cast was not legal) instead of assuming the cast and risk raising an exception.

ICompressible icDoc = doc as ICompressible;
if ( icDoc != null )
{
   icDoc.Compress( );
}
else
   Console.WriteLine( "Compressible not supported" );

Casting to extended interfaces

When the object is cast to ILoggedCompressible, you can use the interface to call methods on ICompressible because ILoggedCompressible extends (and thus subsumes) the methods from the base interface:

ILoggedCompressible ilcDoc = doc as ILoggedCompressible;
if (ilcDoc != null)
{
    ilcDoc.LogSavedBytes( );
    ilcDoc.Compress( );
    // ilcDoc.Read( );
}

You can’t call Read( ), however, because that is a method of IStorable, an unrelated interface. And if you uncomment out the call to Read( ), you will receive a compiler error.

If you cast to IStorableCompressible (which combines the extended interface with the Storable interface), you can then call methods of IStorableCompressible, ICompressible, and IStorable:

IStorableCompressible isc = doc as IStorableCompressible
if (isc != null)
{
    isc.LogOriginalSize( );  // IStorableCompressible
    isc.LogSavedBytes( );    // ILoggedCompressible
    isc.Compress( );         // ICompressible
    isc.Read( );             // IStorable
}

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

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

The is Operator

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

Tip

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

Example 8-3. Using the is operator

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace IsOperator
{
   interfaceIStorable
   {
      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.

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.

Tip

The keyword null represents a null reference—one that doesn’t 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()
{
      // A collection of DocumentsDocument[] 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( )

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

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

Tip

Some designers at Microsoft discourage the use of interfaces and prefer abstract base classes because the latter do better with versioning.

Note

For example, suppose you design an interface and programmers in your shop start using it. You now want to add a new member to that interface. You have two bad choices: you can either change the interface and break existing code, or you can treat the interface as immutable and create, for example, IStore2 or IStorageExtended. If you do that often enough, however, you will soon have dozens of closely related interfaces and a mess on your hands.

With an abstract base class, you can just add a new virtual method with a default implementation. Hey! Presto! Existing code continues to work, but no new class is introduced into the namespace.

The best practice seems to be that if you are creating a class library that will be reused by many people (especially outside your company), you might want to favor abstract base classes. If you are creating classes for a single project, however, interfaces may make for easier-to-understand and more flexible code.

Overriding Interface Implementations

An implementing class is free to mark any or all of the methods that implement the interface as virtual. Derived classes can override these implementations to achieve polymorphism. For example, a Document class might implement the IStorable interface and mark the Read() and Write() methods as virtual. The Document might Read() and Write( ) its contents to a File type. The developer might later derive new types from Document, such as a Note or EmailMessage type, and he might decide that Note will read and write to a database rather than to a file.

Example 8-4 strips down the complexity of Example 8-3 and illustrates overriding an interface implementation. The Read() method is marked as virtual and implemented by Document. Read() is then overridden in a Note type that derives from Document.

Example 8-4. Overriding an interface implementation

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace overridingInterface
{
   interfaceIStorable
   {
      void Read( );
      void Write( );
   }

// Simplify Document to implement only IStorable
   public class Document : 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 Note( string s ):
      base(s)
      {
         Console.WriteLine(
            "Creating note with: {0}", s );
      }

      // override the Read method

      public override void Read( )
      {
         Console.WriteLine(
            "Overriding the Read method for Note!" );
      }

      // implement my own Write method
      public new void Write( )
      {
         Console.WriteLine(
            "Implementing the Write method for Note!" );
      }
   }
   public class Tester
   {

      static void Main( )
      {
         // create a document reference to a Note object
         Document theNote = new Note( "Test Note" );
         IStorable isNote = theNote as IStorable;
         if ( isNote != null )
         {
            isNote.Read( );
            isNote.Write( );
         }

         Console.WriteLine( "
" );

         // direct call to the methods
         theNote.Read( );
         theNote.Write( );

         Console.WriteLine( "
" );

         // create a note object
         Note note2 = new Note( "Second Test" );
         IStorable isNote2 = note2 as IStorable;
         if ( isNote2 != null )
         {
            isNote2.Read( );
            isNote2.Write( );
         }

         Console.WriteLine( "
" );

         // directly call the methods
         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 Read method for Note!
Implementing the Write method for Note!

In this example, Document implements a simplified IStorable interface (simplified to make the example clearer):

interface IStorable
{
    void Read();
    void Write( );
}

The designer of Document has opted to make the Read() method virtual, but not to make the Write( ) method virtual:

public virtual void Read()

In a real-world application, if you were to mark one as virtual, you would almost certainly mark both as virtual, but I’ve differentiated them to demonstrate that the developer is free to pick and choose which methods are made virtual.

The Note class derives from Document:

public class Note : Document

It’s not necessary for Note to override Read(), but it is free to do so and has in fact done so here:

public override void Read()

In Tester, the Read and Write methods are called in four ways:

  • Through the base class reference to a derived object

  • Through an interface created from the base class reference to the derived object

  • Through a derived object

  • Through an interface created from the derived object

To accomplish the first two calls, a Document (base class) reference is created, and the address of a new Note (derived) object created on the heap is assigned to the Document reference:

Document theNote = new Note("Test Note");

An interface reference is created and the as operator is used to cast the Document to the IStorable reference:

IStorable isNote = theNote as IStorable;

You then invoke the Read() and Write( ) methods through that interface. The output reveals that the Read() method is responded to polymorphically and the Write( ) method is not, just as we would expect:

Overriding the Read method for Note!
Document Write Method for IStorable

The Read( ) and Write( ) methods are then called directly on the object itself:

theNote.Read();
theNote.Write();

and once again you see the polymorphic implementation has worked:

Overriding the Read method for Note!
Document Write Method for IStorable

In both cases, the Read( ) method of Note is called and the Write() method of Document is called.

To prove to yourself that this is a result of the overriding method, next create a second Note object, this time assigning its address to a reference to a Note. This will be used to illustrate the final cases (i.e., a call through a derived object and a call through an interface created from the derived object):

Note note2 = new Note("Second Test");

Once again, when you cast to a reference, the overridden Read( ) method is called. When, however, methods are called directly on the Note object:

note2.Read();
note2.Write();

the output reflects that you’ve called a Note and not an overridden Document:

Overriding the Read method for Note!
Implementing the Write method for Note!

Explicit Interface Implementation

In the implementation shown so far, the implementing class (in this case, Document) creates a member method with the same signature and return type as the method detailed in the interface. It is not necessary to explicitly state that this is an implementation of an interface; this is understood by the compiler implicitly.

What happens, however, if the class implements two interfaces, each of which has a method with the same signature? Example 8-5 creates two interfaces: IStorable and ITalk. The latter implements a Read( ) method that reads a book aloud. Unfortunately, this conflicts with the Read() method in IStorable.

Because both IStorable and ITalk have a Read() method, the implementing Document class must use explicit implementation for at least one of the methods. With explicit implementation, the implementing class (Document) explicitly identifies the interface for the method:

void ITalk.Read()

This resolves the conflict, but it creates a series of interesting side effects.

First, there is no need to use explicit implementation with the other method of Talk():

public void Talk( )

Because there is no conflict, this can be declared as usual.

More important, the explicit implementation method can’t have an access modifier:

void ITalk.Read( )

This method is implicitly public.

In fact, a method declared through explicit implementation can’t be declared with the abstract, virtual, override, or new modifiers.

Most important, you can’t access the explicitly implemented method through the object itself. When you write:

theDoc.Read( );

the compiler assumes you mean the implicitly implemented interface for IStorable. The only way to access an explicitly implemented interface is through a cast to an interface:

ITalk itDoc = theDoc;
itDoc.Read();

Explicit implementation is demonstrated in Example 8-5.

Example 8-5. Explicit implementation

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ExplicitImplementation
{
   interfaceIStorable
   {
      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" );
      }

      public void Write( )
      {
         Console.WriteLine( "Implementing IStorable.Write" );

      }

      void ITalk.Read( )
      {
         Console.WriteLine( "Implementing ITalk.Read" );
      }

      public void Talk( )
      {
         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 ITalk.Talk

Selectively Exposing Interface Methods

A class designer can take advantage of the fact that when an interface is implemented through explicit implementation, the interface is not visible to clients of the implementing class except through casting.

Suppose the semantics of your Document object dictate that it implement the IStorable interface, but you don’t want the Read() and Write( ) methods to be part of the public interface of your Document. You can use explicit implementation to ensure that they aren’t available except through casting. This allows you to preserve the public API of your Document class while still having it implement IStorable. If your client wants an object that implements the IStorable 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 be obtained only through a cast. Even if IStorable didn’t have a Read() method, you might choose to make Read( ) explicitly implemented so that you don’t expose Read() as a method of Document.

Note that because explicit interface implementation prevents the use of the virtual keyword, a derived class would be forced to reimplement the method. Thus, if Note derived from Document, it would be forced to reimplement ITalk.Read() because the Document implementation of ITalk.Read() couldn’t be virtual.

Member Hiding

It is possible for an interface member to become hidden. For example, suppose you have an interface IBase that has a property P:

interface IBase
{
   int P { get; set; }
}

Suppose you derive from that interface a new interface, IDerived, that hides the property P with a new method P():

interface IDerived : IBase 
{
   new int P();
}

Setting aside whether this is a good idea, you have now hidden the property P in the base interface. An implementation of this derived interface will require at least one explicit interface member. You can use explicit implementation for either the base property or the derived method, or you can use explicit implementation for both. Thus, any of the following three versions would be legal:

class myClass : IDerived
{
   // explicit implementation for the base property
   int IBase.P { get {...} }
   
   // implicit implementation of the derived method
   public int P( ) {...}
}

class myClass : IDerived
{
   // implicit implementation for the base property
   public int P { get {...} }
   
   // explicit implementation of the derived method
   int IDerived.P( ) {...}
}

class myClass : IDerived
{
   // explicit implementation for the base property
   int IBase.P { get {...} }
   
   // explicit implementation of the derived method
   int IDerived.P( ) {...}
}

Accessing Sealed Classes and Value Types

Generally, it is preferable to access the methods of an interface through an interface cast. The exception is with value types (e.g., structs) or with sealed classes. In that case, it is preferable to invoke the interface method through the object.

When you implement an interface in a struct, you are implementing it in a value type. When you cast to an interface reference, there is an implicit boxing of the object. Unfortunately, when you use that interface to modify the object, it is the boxed object, not the original value object, that is modified. Further, if you change the value of the struct from inside the method, the boxed type will remain unchanged (this is considered quite funny when it is in someone else’s code). Example 8-6 creates a struct that implements IStorable and illustrates the impact of implicit boxing when you cast the struct to an interface reference.

Example 8-6. References on value types

using System;

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ReferencesOnValueTypes
{
// declare a simple interface
   interfaceIStorable
   {
      void Read( );
      int Status { get;set;}

   }

// Implement through a struct
   public struct myStruct : IStorable
   {

      public void Read( )
      {
         Console.WriteLine(
            "Implementing IStorable.Read" );
      }

      public int Status
      {
         get
         {
            return status;
         }
         set
         {
            status = value;
         }
      }

      private int status;
   }

   public class Tester
   {

      static void Main( )
      {
         // create a myStruct object
         myStruct theStruct = new myStruct( );
         theStruct.Status = -1;  // initialize
         Console.WriteLine(
            "theStruct.Status: {0}", theStruct.Status );

         // Change the value
         theStruct.Status = 2;
         Console.WriteLine( "Changed object." );
         Console.WriteLine(
            "theStruct.Status: {0}", theStruct.Status );

         // cast to an IStorable
         // implicit box to a reference type
         IStorable isTemp = ( IStorable ) theStruct;

         // set the value through the interface reference
         isTemp.Status = 4;
         Console.WriteLine( "Changed interface." );
         Console.WriteLine( "theStruct.Status: {0}, isTemp: {1}",
            theStruct.Status, isTemp.Status );

         // Change the value again
         theStruct.Status = 6;
         Console.WriteLine( "Changed object." );
         Console.WriteLine( "theStruct.Status: {0}, isTemp: {1}",
            theStruct.Status, isTemp.Status );
      }
   }
}

Output:
theStruct.Status: -1
Changed object.
theStruct.Status: 2
Changed interface.
theStruct.Status: 2, isTemp: 4
Changed object.
theStruct.Status: 6, isTemp: 4

In Example 8-6, the IStorable interface has a method (Read) and a property (Status).

This interface is implemented by the struct named myStruct:

public struct myStruct : IStorable

The interesting code is in Tester. Start by creating an instance of the structure and initializing its property to -1. The status value is then printed:

myStruct theStruct = new myStruct();
theStruct.status = -1;  // initialize
Console.WriteLine(
   "theStruct.Status: {0}", theStruct.status);

The output from this shows that the status was set properly:

theStruct.Status: -1

Next access the property to change the status, again through the value object itself:

// Change the value
theStruct.status = 2;
Console.WriteLine("Changed object.");
Console.WriteLine(
   "theStruct.Status: {0}", theStruct.status);

The output shows the change:

Changed object.
theStruct.Status: 2

No surprises so far. At this point, create a reference to the IStorable interface. This causes an implicit boxing of the value object theStruct. Then use that interface to change the status value to 4:

// cast to an IStorable
// implicit box to a reference type
IStorable isTemp = (IStorable) theStruct;

// set the value through the interface reference
isTemp.status = 4;
Console.WriteLine("Changed interface.");
Console.WriteLine("theStruct.Status: {0}, isTemp: {1}", 
    theStruct.status, isTemp.status);

Here the output can be a bit surprising:

Changed interface.
theStruct.Status: 2, isTemp: 4

Aha! The object to which the interface reference points has been changed to a status value of 4, but the struct value object is unchanged. Even more interesting, when you access the method through the object itself:

// Change the value again
theStruct.status = 6;
Console.WriteLine("Changed object.");
Console.WriteLine("theStruct.Status: {0}, isTemp: {1}", 
    theStruct.status, isTemp.status);

the output reveals that the value object has been changed, but the boxed reference value for the interface reference has not:

Changed object.
theStruct.Status: 6, isTemp: 4

A quick look at the MSIL code (Example 8-7) reveals what’s going on under the hood.

Example 8-7. MSIL code resulting from Example 8-6

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       194 (0xc2)
  .maxstack  3
  .locals init ([0] valuetype ReferencesOnValueTypes.myStruct theStruct,
           [1] class ReferencesOnValueTypes.IStorable isTemp)
  IL_0000:  ldloca.s   theStruct
  IL_0002:  initobj    ReferencesOnValueTypes.myStruct
  IL_0008:  ldloca.s   theStruct
  IL_000a:  ldc.i4.m1
  IL_000b:  call       instance void ReferencesOnValueTypes.myStruct::
                       set_Status(int32)
  IL_0010:  ldstr      "theStruct.Status: {0}"
  IL_0015:  ldloca.s   theStruct
  IL_0017:  call       instance int32 ReferencesOnValueTypes.myStruct::
                       get_Status( )
  IL_001c:  box        [mscorlib]System.Int32
  IL_0021:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_0026:  nop
  IL_0027:  ldloca.s   theStruct
  IL_0029:  ldc.i4.2
  IL_002a:  call       instance void ReferencesOnValueTypes.myStruct::
                       set_Status(int32)
  IL_002f:  ldstr      "Changed object."
  IL_0034:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0039:  nop
  IL_003a:  ldstr      "theStruct.Status: {0}"
  IL_003f:  ldloca.s   theStruct
  IL_0041:  call       instance int32 ReferencesOnValueTypes.myStruct::
                       get_Status( )
  IL_0046:  box        [mscorlib]System.Int32
  IL_004b:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_0050:  nop
  IL_0051:  ldloc.0
  IL_0052:  box        ReferencesOnValueTypes.myStruct
  IL_0057:  stloc.1
  IL_0058:  ldloc.1
  IL_0059:  ldc.i4.4
  IL_005a:  callvirt   instance void ReferencesOnValueTypes.IStorable::
                       set_Status(int32)
  IL_005f:  ldstr      "Changed interface."
  IL_0064:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0069:  nop
  IL_006a:  ldstr      "theStruct.Status: {0}, isTemp: {1}"
  IL_006f:  ldloca.s   theStruct
  IL_0071:  call       instance int32 ReferencesOnValueTypes.myStruct::
                       get_Status( )
  IL_0076:  box        [mscorlib]System.Int32
  IL_007b:  ldloc.1
  IL_007c:  callvirt   instance int32 ReferencesOnValueTypes.IStorable::
                       get_Status( )
  IL_0081:  box        [mscorlib]System.Int32
  IL_0086:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_008b:  nop
  IL_008c:  ldloca.s   theStruct
  IL_008e:  ldc.i4.6
  IL_008f:  call       instance void ReferencesOnValueTypes.myStruct::
                       set_Status(int32)
  IL_0094:  ldstr      "Changed object."
  IL_0099:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_009e:  nop
  IL_009f:  ldstr      "theStruct.Status: {0}, isTemp: {1}"
  IL_00a4:  ldloca.s   theStruct
  IL_00a6:  call       instance int32 ReferencesOnValueTypes.myStruct::
                       get_Status( )
  IL_00ab:  box        [mscorlib]System.Int32
  IL_00b0:  ldloc.1
  IL_00b1:  callvirt   instance int32 ReferencesOnValueTypes.IStorable::
                       get_Status( )
  IL_00b6:  box        [mscorlib]System.Int32
  IL_00bb:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_00c0:  nop
  IL_00c1:  ret
} // end of method Tester::Main

On line IL_000b, set_Status( ) was called on the value object. We see the second call on line IL_0017. Notice that the calls to WriteLine() cause boxing of the integer value status so that the GetString( ) method can be called.

The key line is IL_001c (highlighted) where the struct itself is boxed. It is that boxing that creates a reference type for the interface reference. Notice on line IL_005a that this time IStorable::set_Status is called rather than myStruct::set_Status.

The design guideline is if you are implementing an interface with a value type, be sure to access the interface members through the object rather than through an interface reference.



[1] Or more accurately, a properly cast reference to the object that implements the interface.

[2] Both the is and the as operator (described next) can be used to evaluate types through inheritance, in addition to evaluating implementation of interfaces. Thus, you can use is to check whether a dog is a mammal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.205.99