Chapter 13. Interfaces

There are times when you may not want to create a new type, but you do want to describe a set of behaviors that any number of types might implement. For example, you might want to describe what it means to be storable (capable of being written to disk) or printable.

Such a description is called an interface. An interface is a contract. When you design an interface, you’re saying “if you want to provide this capability, you must implement these methods, provide these properties and indexers, and support these events.” The implementer of the interface agrees to the contract and implements the required elements.

Tip

See Chapter 8 for information about methods and properties, Chapter 17 for information about events, and Chapter 14 for coverage of indexers.

When specifying interfaces, it is easy to get confused about who is responsible for what. There are three concepts to keep clear:

The interface

This is the contract. By convention, interface names begin with a capital I, so your interface might have a name such as IPrintable. The IPrintable interface might require, among other things, a Print( ) method. This states that any class that wants to implement IPrintable must implement a Print( ) method, but it does not specify how that method works. That is up to the designer of the implementing class.

The implementing class

This is the class that agrees to the contract described by the interface. For example, Document might be a class that implements IPrintable and thus implements the Print( ) method in whatever way the designer of the Document class thinks is appropriate.

The client class

The client calls methods on the implementing class. For example, you might have an Editor class that has a collection of IPrintable objects (every object in the class is an instance of a type that implements IPrintable). The client can expect to be able to call Print( ) on each, and while each may implement the method differently, each will do so appropriately and without complaint.

Interfaces are a critical addition to any framework, and they are used extensively throughout .NET. For example, the collection classes (stacks, queues, dictionaries) are defined, in large measure, by the interfaces they implement. (The collection classes are reviewed in detail in Chapter 14.)

In this chapter, you will learn how to create, implement, and use interfaces. You’ll learn how one class can implement multiple interfaces, and you will also learn how to make new interfaces by combining or deriving from existing interfaces. Finally, you will learn how to test whether a class has implemented an interface.

Implementing an Interface

The syntax for defining an interface is very similar to the syntax for defining a class:

    [attributes] [access-modifier] interface interface-name [:base-list] {interface-body}

The optional attributes are well beyond the scope of this book (however, see the sidebar, "Attributes“).

Access modifiers (public, private, etc.) work just as they do with classes. (See Chapter 7 for more about access modifiers.) The interface keyword is followed by an identifier (the interface name). It is common (but not required) to begin the name of your interface with a capital I (IStorable , ICloneable, IGetNoKickFromChampagne, etc.). The optional base list is discussed later in this chapter.

Attributes are most often used in one of two ways: either for interacting with legacy COM objects or for creating controls that will be fully recognized by the Visual Studio development environment. You can create your own custom attributes , but this is unusual and not covered in this book (for more on this, see Programming C#, Fourth Edition [O’Reilly, 2005]). Now suppose you are the author of a Document class, which specifies that Document objects can be stored in a database. You decide to have Document implement the IStorable interface. It isn’t required that you do so, but by implementing the IStorable interface, you signal to potential clients that the Document class can be used just like any other IStorable object. This will, for example, allow your clients to add your Document objects to an array of Notes

    IStorable[] myStorableArray = new IStorable[3];

and to otherwise interact with your Document in this very general and well-understood way.

To implement the IStorable interface, 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

You can read this as “define a public class named Document that implements the IStorable interface.” The compiler distinguishes whether the colon indicates inheritance or implementation of an interface by checking to see if IStorable is defined, and whether it is an interface or base class.

Your definition of the Document class that implements the IStorable interface might look like this:

    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. Example 13-1 illustrates defining and implementing the IStorable interface.

The output looks like this:

    Creating document with: Test Document
    Implementing the Read Method for IStorable
    Document Status: -1

Once you’ve defined the IStorable interface, you can define classes that implement your interface. Keep in mind that you cannot 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. Thus, your Document class must provide both a Read( ) and a Write( ) method and the Status property.

    public class Document : IStorable
    {

This statement defines Document as a class that defines IStorable. I also like to separate the implementation of an interface in a region—this is a Visual Studio 2005 convenience that allows you to collapse and expand the code within the region to make reading the code easier:

    #region IStorable
      //...
    #endregion

Within the region, you place the code that implements the two required methods and the required property. Exactly how your Document class fulfills the requirements of the interface, however, is entirely up to you.

Although IStorable dictates that Document must have a Status property, it does not know or care whether Document stores the actual status as a member variable or looks it up in a database. Example 13-1 implements the Status property by returning (or setting) the value of a private member variable, status. Another class that implements IStorable could provide the Status property in an entirely different manner (such as by looking it up in a database).

Implementing More than One Interface

Classes can derive from only one class (and if it doesn’t explicitly derive from a class, then it implicitly derives from Object).

When you design your class, you can choose not to implement any interfaces, you can implement a single interface, or you can implement two or more interfaces. For example, in addition to IStorable, you might have a second interface, ICompressible , for files that can be compressed to save disk space. If your Document class can be stored and compressed, you might choose to have Document implement both the IStorable and ICompressible interfaces.

Example 13-2 shows the complete listing of the new ICompressible interface and demonstrates how you modify the Document class to implement the two interfaces.

The output looks like this:

    Creating document with: Test Document
    Implementing the Read Method for IStorableImplementing Compress
    Document Status: -1

As Example 13-2 shows, you declare the fact that your Document class will implement two interfaces by adding the second interface to the declaration (in the base list), separating the two interfaces with commas:

    public class Document :IStorable, ICompressible

Once you’ve done this, the Document class must also implement the methods specified by the ICompressible interface. ICompressible has only two methods, Compress( ) and Uncompress( ), which are specified as:

    interface ICompressible
     {
       void Compress(  );
       void Decompress(  );
     }

In this simplified example, Document implements these two methods as follows, printing notification messages to the console:

    public void Compress(  )
    {
     Console.WriteLine("Implementing the Compress Method");
    }

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

You can access the members of an interface through an object of any class that implements the interface. For example, because Document implements IStorable, you can access the IStorable methods and property through any Document instance:

    Document doc = new Document("Test Document");
    doc.Status = -1;
    doc.Read(  );

At times, though, you won’t know that you have a Document object; you’ll only know that you have objects that implement IStorable, for example, if you have an array of IStorable objects. You can create a reference of type IStorable, and assign that to each member in the array, accessing the IStorable methods and property (but not the Document-specific methods, because all the compiler knows is that you have an IStorable, not a 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 assign that object to a reference to any of the interfaces it implements:

    Document myDoc = new Document(//...);
    IStorable myStorable = myDoc;

You can read this line as “assign the IStorable-implementing object myDoc to the IStorable reference myStorable.”

You are now free to use the IStorable reference to access the IStorable methods and properties of the document:

    myStorable.Status = 0;
    myStorable.Read(  );

Notice that the IStorable reference myStorable has access to the IStorable property Status, but not to the Document’s private member variable status, even though the IStorable reference was instantiated as a reference to the Document. The IStorable reference only knows about the IStorable interface, not about the Document’s internal members.

Thus far, you have assigned the Document object (myDoc) to an IStorable reference.

There may be times, however, in which you do not know at compile time whether or not an object supports a particular interface. For instance, given a List of IStorable objects, you might not know whether any given object in the collection also implements ICompressible (some do, some do not). Let’s set aside the question of whether this is a good design, and move on to how we solve the problem.

You could cast each member blindly to ICompressible, and then catch the exception that will be thrown for those that are not ICompressible, but this is ugly, and there are two better ways to do so: the is and the as operators .

The is operator lets you query whether an object implements an interface (or derives from a base class). The form of the is operator is:

    if ( myObject is ICompressible )

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 without throwing an exception.[9]

The as operator tries to cast the object to the type, and if an exception would be thrown, it instead returns null:

    ICompressible myCompressible = myObjectas ICompressible
    if ( myCompressible != null )

Example 13-3 illustrates the use of both the is and the as operators by creating two classes. The Note class implements IStorable. The Document class derives from Note (and thus inherits the implementation of IStorable) and adds a property (ID) along with an implementation of ICompressible.

In this example, you’ll create an array of Note objects and then, if you want to access either ICompressible or the ID, you’ll need to test the Note to see if it is of the correct type. Both the is and the as operators are demonstrated. The entire program is documented fully immediately after the source code.

Example 13-3. The is and as operators
using System;

namespace InterfaceDemo
{
   interface IStorable
   {
      void Read(  );
      void Write( object obj );
      int Status { get; set; }
   }

   interface ICompressible
   {
      void Compress(  );
      void Decompress(  );
   }

   public class Note : IStorable
   {
      private int status = 0; // IStorable
      private string myString;

      public Note( string theString )
      {
         myString = theString;
      }

      public override string ToString(  )
      {
         return myString;
      }

      #region 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; }
      }

      #endregion // IStorable

   }

   public class Document : Note, ICompressible
   {
      private int documentID;
      public int ID
      {
         get { return this.documentID; }
      }

      public Document( string docString, int documentID )
         :
      base( docString )
      {
         this.documentID = documentID;
      }

      #region ICompressible

      public void Compress(  )
      {
         Console.WriteLine( "Compressing..." );
      }
      public void Decompress(  )
      {
         Console.WriteLine( "Decompressing..." );
      }
      #endregion  // ICompressible

   }  // end Document class

   class Tester
   {
      public void Run(  )
      {
         string testString = "String ";
         Note[] myNoteArray = new Note[3];

         for ( int i = 0; i < 3; i++ )
         {
            string docText = testString + i.ToString(  );
            if ( i % 2 == 0 )
            {
               Document myDocument = new Document( docText, ( i + 5 ) * 10 );
               myNoteArray[i] = myDocument;
            }
            else
            {
               Note myNote = new Note( docText );
               myNoteArray[i] = myNote;
            }
         }

         foreach ( Note theNote in myNoteArray )
         {
            Console.WriteLine( "\nTesting {0} with IS", theNote );

            theNote.Read(  );     // all notes can do this
            if ( theNote is ICompressible )
            {
               ICompressible myCompressible = theNote as ICompressible;
               myCompressible.Compress(  );
            }
            else
            {
               Console.WriteLine( "This storable object is not compressible." );
            }

            if ( theNote is Document )
            {
               Document myDoc = theNote as Document;

               // clean cast
               myDoc = theNote as Document;
               Console.WriteLine( "my documentID is {0}", myDoc.ID );

               // old fashioned cast!
               Console.WriteLine( "My documentID is {0}",
                 ( ( Document ) theNote ).ID );
            }
         }

         foreach ( Note theNote in myNoteArray )
         {
            Console.WriteLine( "\nTesting {0} with AS", theNote );
            ICompressible myCompressible = theNote as ICompressible;
            if ( myCompressible != null )
            {
               myCompressible.Compress(  );
            }
            else
            {
               Console.WriteLine( "This storable object is not compressible." );
            }    // end else

            Document theDoc = theNote as Document;
            if ( theDoc != null )
            {
               Console.WriteLine( "My documentID is {0}",
                  ( ( Document ) theNote ).ID );
            }
            else
            {
               Console.WriteLine( "Not a document." );
            }
         }
      }

      static void Main(  )
      {
         Tester t = new Tester(  );
         t.Run(  );
      }
   }          // end class Tester
}             // end Namespace InterfaceDemo

The output looks like this:

    Testing String 0 with IS
    Implementing the Read Method for IStorable
    Compressing...
    my documentID is 50
    My documentID is 50

    Testing String 1 with IS
    Implementing the Read Method for IStorable
    This storable object is not compressible.

    Testing String 2 with IS
    Implementing the Read Method for IStorable
    Compressing...
    my documentID is 70
    My documentID is 70

    Testing String 0 with AS
    Compressing...
    My documentID is 50

    Testing String 1 with AS
    This storable object is not compressible.
    Not a document.

    Testing String 2 with AS
    Compressing...
    My documentID is 70

The best way to understand this program is to take it apart piece by piece.

Within the namespace, we declare two interfaces, IStorable and ICompressible, and then three classes: Note, which implements IStorable; and Document, which derives from Note (and thus inherits the implementation of IStorable) and which also implements ICompressible). Finally, we add the class Tester to test the program.

Within the Run( ) method of the Tester class, we create an array of Note objects, and we add to that array two Document and one Note instances (using the expedient that each time through the for loop, we check whether the counter variable i is even, and if so, we create a Document; otherwise, we create a Note).

We then iterate through the array, extract each Note in turn, and use the is operator to test first if the Note can safely be assigned to an ICompressible reference and then to check if the Note can safely be cast to a Document. In the case shown, these tests amount to the same thing, but you can imagine that we could have a collection with many types derived from Note, some of which implement ICompressible and some of which do not.

We have a choice as to how we cast to a document. The old-fashioned way is to use the C-style cast:

    myDoc = (Document) theNote;

The preferred way is to use the as operator:

    myDoc = theNote as Document;

The advantage of the latter is that it will return null (rather than throwing an exception) if the cast fails, and it may be a good idea to get in the habit of using this new form of casting.

In any case, you can use the interim variable:

    myDoc = theNote as Document;
     Console.WriteLine( "my documentID is {0}", myDoc.ID );

Or you can cast and access the property all in one ugly but effective line:

    Console.WriteLine( "My documentID is {0}",
       ( ( Document ) theNote ).ID );

The extra parentheses are required to ensure that the cast is done before the attempt at accessing the property.

The second foreach loop uses the as operator to accomplish the same work, and the results are identical. (If you bother to look at the actual IL code, you’ll see that the second foreach loop actually generates less code, and thus is slightly more efficient.)

It is possible to extend an existing interface to add new methods or members. 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. One such method might be called LogSavedBytes( ). The following code creates a new interface named ILoggedCompressible that is identical to ICompressible except that it adds the method LogSavedBytes:

    interface ILoggedCompressible : ICompressible
    {
     void LogSavedBytes(  );
    }

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 also ICompressible. Objects of that type can be cast either to ILoggedCompressible or to ICompressible.

Example 13-4 extends ICompressible to create ILoggedCompressible, and then casts the Document first to be of type IStorable, then to be of type ILoggedCompressible. Finally, the example casts the Document object to ICompressible. This last cast is safe because any object that implements ILoggedCompressible must also have implemented ICompressible (the former is a superset of the latter). This is the same logic that says you can cast any object of a derived type to an object of a base type (that is, if Student derives from Human, then all Students are Human, even though not all Humans are Students).

The output looks like this:

    Creating document with: Test Document

    Calling both ICompressible and ILoggedCompressible methods...
    Implementing Compress
    Implementing LogSavedBytes

Example 13-4 starts by creating the ILoggedCompressible interface, which extends the ICompressible interface:

    // extend ICompressible to log the bytes saved
    interface ILoggedCompressible : ICompressible
    {
       void LogSavedBytes(  );
    }

Notice that the syntax for extending an interface is the same as that for deriving from a class. This extended interface defines only one new method (LogSavedBytes( )), but any class implementing this interface must also implement the base interface (ICompressible) and all its members. (In this sense, it is reasonable to say that an ILoggedCompressible object is-a ICompressible object.)

You can also create new interfaces by combining existing interfaces and optionally adding new methods or properties. For example, you might decide to combine the definitions of IStorable and ICompressible into a new interface called IStorableCompressible. This interface would combine the methods of each of the other two interfaces, but would also add a new method, LogOriginalSize( ), to store the original size of the pre-compressed item:

    interface IStorableCompressible : IStorable, ILoggedCompressible
    {
     void LogOriginalSize(  );
    }

Having created this interface, you can now modify Document to implement IStorableCompressible:

    public class Document : IStorableCompressible

You now can cast the Document object to any of the four interfaces you’ve created so far:

    IStorable isDoc = doc as IStorable;
    ILoggedCompressible ilDoc = doc as ILoggedCompressible;
    ICompressible icDoc = doc as ICompressible;
    IStorableCompressible iscDoc = doc as IStorableCompressible;

When you cast to the new combined interface, you can invoke any of the methods of any of the interfaces it extends or combines. The following code invokes four methods on iscDoc (the IStorableCompressible object). Only one of these methods is defined in IStorableCompressible, but all four are methods defined by interfaces that IStorable-Compressible extends or combines.

    if (iscDoc != null)
    {
     iscDoc.Read(); // Read(  ) from IStorable
     iscDoc.Compress(); // Compress(  ) from ICompressible
     iscDoc.LogSavedBytes(); // LogSavedBytes(  ) from
     // ILoggedCompressible
     iscDoc.LogOriginalSize(); // LogOriginalSize(  ) from
     // IStorableCompressible
    }

An implementing class is free to mark any or all of the methods from the interface as virtual. Derived classes can then override or provide new implementations, just as they might with any other virtual instance method.

For example, a Document class might implement the IStorable interface and mark its Read( ) and Write( ) methods as virtual. In an earlier example, we created a base class Note, and a derived class Document. While the Note class implements Read( ) and Write( ) to save to a file, the Document class might implement Read( ) and Write( ) to read from and write to a database.

Example 13-5 strips down the complexity of the previous examples and illustrates overriding an interface implementation. Note implements the IStorable-required Read( ) method as a virtual method, and Document overrides that implementation.

The complete listing is shown in Example 13-5.

Example 13-5. Overriding an interface implementation
using System;


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

   public class Note : IStorable
   {
      public Note( string s )
      {
         Console.WriteLine(
         "Creating Note with: {0}", s );
      }

      // NB: virtual
      public virtual void Read(  )
      {
         Console.WriteLine(
         "Note Read Method for IStorable" );
      }

      // NB: Not virtual!
      public void Write(  )
      {
         Console.WriteLine(
         "Note Write Method for IStorable" );
      }
   }

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

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

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

   class Tester
   {
      public void Run(  )
      {
         Note theNote = new Document( "Test Document" );

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

         Console.WriteLine( "\n" );

         IStorable isStorable = theNote as IStorable;
         if ( isStorable != null )
         {
            isStorable.Read(  );
            isStorable.Write(  );
         }
         Console.WriteLine( "\n" );

         // This time create a reference to the derived type
         Document theDoc = new Document( "Second Test" );

         theDoc.Read(  );
         theDoc.Write(  );
         Console.WriteLine( "\n" );

         IStorable isStorable2 = theDoc as IStorable;
         if ( isStorable != null )
         {
            isStorable2.Read(  );
            isStorable2.Write(  );
         }
      }

      static void Main(  )
      {
         Tester t = new Tester(  );
         t.Run(  );
      }
   }
}

The output looks like this:

    Creating Note with: Test Document
    Creating Document with: Test Document
    Overriding the Read method for Document!
    Note Write Method for IStorable


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


    Creating Note with: Second Test
    Creating Document with: Second Test
    Overriding the Read method for Document!
    Implementing the Write method for Document!


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

In Example 13-5, the IStorable interface is simplified for clarity’s sake:

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

The Note class implements the IStorable interface:

    public class Note : IStorable

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

    public virtual void Read(  )
    public void Write(  )

The new class, Document, derives from Note:

    public class Document : Note

It is not necessary for Document to override Read( ), but it is free to do so and has done so here:

    public override void Read(  )

To illustrate the implications of marking an implementing method as virtual, the Run( ) method calls the Read( ) and Write( ) methods in four ways:

  • Through the Note class reference to a Document object

  • Through an interface created from the Note class reference to the Document object

  • Through a Document object

  • Through an interface created from the Document object

Virtual implementations of interface methods are polymorphic, just like the virtual methods of classes.

When you call the non-polymorphic Write( ) method on the IStorable interface cast from the derived Document, you actually get the Note’s Write method, because Write( ) is implemented in the base class and is non-virtual.

To see polymorphism at work with interfaces, you’ll create a reference to the Note class and initialize it with a new instance of the derived Document class:

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

Invoke the Read and Write methods:

    theDocument.Read(  );
    theDocument.Write(  );

The output reveals that the (virtual) Read( ) method is called polymorphically—that is, the Document class overrides the Note class’s Read( ), while the non-virtual Write( ) method of the Note class is invoked because it was not made virtual.

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

The overridden method of Read( ) is called because you’ve created a new Document object:

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

The non-virtual Write method of Note is called because you’ve assigned theDocument to a reference to a Note:

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

To illustrate calling the methods through an interface that is created from the Note class reference to the Document object, create an interface reference named isDocument. Use the as operator to cast the Note (theDocument) to the IStorable reference:

    IStorable isDocument = theDocument as IStorable;

Then invoke the Read( ) and Write( ) methods for theDocument through that interface:

    if (isDocument != null)
    {
     isDocument.Read(  );
     isDocument.Write(  );
    }

The output is the same: once again, the virtual Read( ) method is polymorphic, and the non-virtual Write( ) method is not:

    Overriding the Read method for Document
    Note Write Method for IStorable

Next, create a second Document object, this time assigning its address to a reference to a Document, rather than a reference to a Note. This will be used to illustrate the final cases (a call through a Document object and a call through an interface created from the Document object):

    Document Document2 = new Document("Second Test");

Call the methods on the Document object:

    Document2.Read(  );
    Document2.Write(  );

Again, the virtual Read( ) method is polymorphic, and the non-virtual Write( ) method is not, but this time you get the Write( ) method for Document because you are calling the method on a Document object:

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

Finally, cast the Document object to an IStorable reference and call Read( ) and Write( ):

    IStorable isDocument2 = Document2 as IStorable;
    if (isDocument != null)
    {
     isDocument2.Read(  );
     isDocument2.Write(  );
    }

The Read( ) method is called polymorphically, but the Write( ) method for Note is called because Note implements IStorable, and Write( ) is not polymorphic:

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

In the implementation shown so far, the class that implements the interface (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 Document is implementing IStorable, for example; the compiler understand this implicitly.

What happens, however, if the class implements two interfaces, each of which has a method with the same signature? This might happen if the class implements interfaces defined by two different organizations or even two different programmers. The next example creates two interfaces: IStorable and ITalk. ITalk 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:

    voidITalk.Read(  )

Marking the Read( ) method as a member of the ITalk interface resolves the conflict between the identical Read( ) methods. There are some additional aspects you should keep in mind.

First, the explicit implementation method cannot have an access modifier:

    void ITalk.Read(  )

This method is implicitly public. In fact, a method declared through explicit implementation cannot be declared with the abstract, virtual, override, or new keywords.

Most importantly, you cannot 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 the interface:

    ITalk itDoc = theDoc as ITalk;
    if (itDoc != null)
    {
     itDoc.Read(  );
    }

Explicit implementation is demonstrated in Example 13-6. Note that there is no need to use explicit implementation with the other method of ITalk:

    public void Talk(  )

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

The output looks like this:

    Creating document with: Test Document
    Document Read Method for IStorable
    Implementing ITalk.Read
    Document Read Method for IStorable
    Implementing ITalk.Talk


[9] Historical footnote: “It depends on what the meaning of the word ‘is’ is. If the—if he—if ‘is’ means is and never has been, that is not—that is one thing.”—But not in C#.