다음을 통해 공유


Interfaces

This chapter is excerpted from Learning C# 3.0: Master the fundamentals of C# 3.0 by Jesse Liberty, Brian MacDonald, published by O'Reilly Media

Learning C# 3.0

Logo

Buy Now

Back in Chapter 11, Inheritance and Polymorphism, you saw how inheritance and abstract methods can dictate the methods that a class has to implement. However, it isn't always necessary to create a new parent class, even an abstract one, to guarantee the behaviors of your class. For example, you might want to dictate that your class must be storable (capable of being written to disk) or printable. "Storable" isn't a good candidate for a class, because it doesn't model an object; instead, it describes a set of behaviors that you want your class to have. Such a description is called an interface. The interface defines the methods that a class must implement, but it doesn't dictate how the class implements these required methods. This provides a lot of flexibility on the part of the class designer, yet it allows client classes to use those methods with confidence, because the interface methods are guaranteed to be implemented. There are a lot of interesting things you can do with interfaces, including implementing multiple interfaces, combining them, inheriting them, and casting to them. All of it can be tricky to understand at first, so we'll describe them all thoroughly in this chapter.

What Interfaces Are

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

You saw methods and properties in Chapter 8, Inside Methods. We'll discuss indexers in Chapter 14, Generics and Collections and events in Chapter 17, Delegates and Events. We promise you don't need to know about them for this chapter.

When you specify 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 internally. 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 an array of IPrintable objects (every object in the class is an instance of a type that implements IPrintable, even if they aren't all the same type). The client can expect to be able to call Print( ) on each object, and although each individual object may implement the method differently, each will do so appropriately and without complaint.

Interfaces Versus Abstract Base Classes

Programmers learning C# often ask about the difference between an interface and an abstract base class. The key difference is that an abstract base class serves as the base class for a family of derived classes, and an interface is meant to be mixed in with other inheritance chains. That is, a class can inherit from only a single parent class, but it can implement multiple interfaces.

In addition, when you derive from an abstract class, you must override all the abstract methods in the abstract base class, but you don't have to override any nonabstract methods. You can simply use the implementation that the base class provides. This is called partial implementation, and it's very common with abstract classes. Interfaces don't have any implementation, so you must implement every method defined in the interface. You can't partially implement an interface.

Inheriting from an abstract class implements the is-a relationship, introduced in Chapter 6, Object-Oriented Programming. Implementing an interface defines a different relationship, one you've not seen until now: 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).

One critical thing to remember about interfaces is that an interface is not a class, and you can't instantiate an instance of an interface. For example, this won't work:

IPrintable myPrintable = new IPrintable( );

Interfaces aren't classes, so they don't have constructors, and you can't have an instance of an interface. However-and this is where it gets a bit confusing-if you have a class that implements the interface, you can create a reference to an object of that class, of the type of the interface. Confused? Look at an example. Suppose you have a Document class that you know implements IPrintable. Although you can't create an IPrintable object, you can do this:

IPrintable myPrintable = new Document( );

myPrintable is called a reference, which in this case refers to some (unnamed) Document object. All your code needs to know about myPrintable is that it refers to some object that implements the IPrintable interface-it could be a Document, it could be a Memo, it could be a GreatAmericanNovel. Doesn't matter. This lets you treat interface references polymorphically, just like you can use inheritance to treat objects polymorphically (see Chapter 11, Inheritance and Polymorphism for a refresher, if you need it). You'll see how this works a little later in the chapter.

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

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}

Tip

The optional attributes are beyond the scope of this book. In short, every .NET application contains code, data, and metadata. Attributes are objects that are embedded in your program (invisible at runtime) and contain metadata-that is, data about your classes and your program. You don't need to worry about them for our purposes here.

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

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 IStorable references:

IStorable[] myStorableArray = new IStorable[3];

As we discussed earlier, the array doesn't specifically need to know that it holds a Document object, just that it holds objects that implement IStorable.

To implement the IStorable interface, use the same syntax as though 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 whether IStorable is defined, and whether it is an interface or base class.

If you derive from a base class and you also implement one or more interfaces, you use a single colon and separate the base class and the interfaces by commas. The base class must be listed first; the interfaces may be listed in any order.

public MyBigClass : TheBaseClass, IPrintable, IStorable, IClaudius, IAndThou

In this declaration, the new class MyBigClass derives from TheBaseClass and implements four interfaces.

Suppose that the definition of IStorable requires a void Read( ) method, and a void Write( ) method that takes an object. In that case, 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, "Implementing an interface simply requires implementing all of its properties and methods, in whatever way is best for your class" illustrates defining and implementing the IStorable interface. Have a look at it first, and we'll take it apart afterward.

Example 13.1. Implementing an interface simply requires implementing all of its properties and methods, in whatever way is best for your class

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

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

   public class Document : IStorable
   {

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

#region IStorable

      public void Read( )
      {
         Console.WriteLine( "Executing Document's Read
                             Method for IStorable" );
      }

      public void Write( object o )
      {
         Console.WriteLine( "Executing Document's Write
                             Method for IStorable" );
      }

      // property required by IStorable
      public int Status { get; set;}

#endregion

   }

   class Tester
   {
      public void Run( )
      {
         Document doc = new Document( "Test Document" );
         doc.Status = -1;
         doc.Read( );
         Console.WriteLine( "Document Status: {0}", doc.Status );
      }

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

The output looks like this:

Creating document with: Test Document
Executing Document's Read Method for IStorable
Document Status: -1

Defining the Interface

In Example 13.1, "Implementing an interface simply requires implementing all of its properties and methods, in whatever way is best for your class", the first few lines define an interface, IStorable, which has two methods (Read( ) and Write( )) and a property (Status) of type int:

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

Notice that the IStorable method declarations for Read( ) and Write( ) do not include access modifiers (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. In addition, you must declare these methods to be public, and not static, when you implement the interface.

In the interface declaration, the methods are otherwise defined just like methods in a class: you indicate the return type (void), followed by the identifier (Write), followed by the parameter list (objectobj), and, of course, you end all statements with a semicolon. The methods in the interface declaration have no body, however.

An interface can also require that the implementing class provide a property (see Chapter 8, Inside Methods for a discussion of properties). Notice that the declaration of the Status property does not provide an implementation for get( ) and set( ), but simply designates that there must be a get( ) and a set( ):

int Status { get; set; }

You can't define member variables in an interface, but defining properties like this has the same practical effect.

Implementing the Interface on the Client

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. We also like to separate the implementation of an interface in a region-this is a Visual Studio convenience that allows you to collapse and expand the code within the region to make the code easier to read:

#region IStorable
  //...
#endregion

Within the region, you place the code that implements the two required methods and the required property. In this case, we're not really reading or writing anything. To keep things simple in the example, we're just announcing to the console that we've invoked the appropriate method; you'll have to use your imagination a bit:

public void Read( )
{
   Console.WriteLine( "Executing Document's Read
                       Method for IStorable" );
}

public void Write( object o )
{
   Console.WriteLine( "Executing Document's Write
                       Method for IStorable" );
}

Notice that the Write( ) method takes an instance of class object as a parameter, even though the method never uses it. Perhaps your specific implementation would do something with an object, but it doesn't have to. Exactly how your Document class fulfills the requirements of the interface 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, "Implementing an interface simply requires implementing all of its properties and methods, in whatever way is best for your class" implements the Status property with an automatic property (introduced in Chapter 8, Inside Methods). 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, it implicitly derives from Object).

Tip

Some languages, such as C++, support inheritance from multiple base classes. C# allows inheritance from only a single class, but interfaces don't have that limitation.

When you design your class, you can choose not to implement any interfaces, you can implement a single interface, or you can implement more than one. For example, in addition to IStorable, you might have a second interface, ICompressible, for files that can be compressed to save disk space. This new interface might have methods of Compress( ) and Decompress( ), for example. If your Document class can be stored and compressed, you might choose to have Document implement both the IStorable and ICompressible interfaces.

Tip

Both IStorable and ICompressible are interfaces created for this book and are not part of the standard .NET Framework.

Example 13.2, "Implementing multiple interfaces isn't much more difficult than implementing a single one; you just have to implement the required methods for both interfaces" shows the complete listing of the new ICompressible interface and demonstrates how you modify the Document class to implement the two interfaces.

Example 13.2. Implementing multiple interfaces isn't much more difficult than implementing a single one; you just have to implement the required methods for both interfaces

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

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

    // here's the new interface
    interface ICompressible
    {
        void Compress( );
        void Decompress( );
    }

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

        #region IStorable

        public void Read( )
        {
            Console.WriteLine("Executing Document's Read Method
                               for IStorable");
        }

        public void Write(object o)
        {
            Console.WriteLine("Executing Document's Write Method
                               for IStorable");
        }

        public int Status{ get; set;}

        #endregion     // IStorable

        #region ICompressible

        public void Compress( )
        {
            Console.WriteLine("Executing Document's Compress Method
                               for ICompressible");
        }
        public void Decompress( )
        {
            Console.WriteLine("Executing Document's Decompress Method
                               for ICompressible");
        }
        #endregion  // ICompressible

    }

    class Tester
    {
        public void Run( )
        {
            Document doc = new Document("Test Document");
            doc.Status = -1;
            doc.Read( );          // invoke method from IStorable
            doc.Compress( );      // invoke method from ICompressible
            Console.WriteLine("Document Status: {0}", doc.Status);
        }

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

The output looks like this:

Creating document with: Test Document
Executing Document's Read Method for IStorable
Executing Document's Compress Method for ICompressible
Document Status: -1

As Example 13.2, "Implementing multiple interfaces isn't much more difficult than implementing a single one; you just have to implement the required methods for both interfaces" 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 Decompress( ), 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("Executing Document's Compress
                       Method for ICompressible");
}
public void Decompress( )
{
    Console.WriteLine("Executing Document's Decompress
                       Method for ICompressible");
}

Once again, these methods don't really do anything other than output a message announcing their intentions; that's deliberate, to keep the example short.

As you can see, implementing multiple interfaces isn't hard at all; each interface mandates additional methods that your class has to provide. You could implement several interfaces in this way.

Casting to an Interface

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, as we mentioned earlier. You can create a reference of type IStorable, and assign that to each member in the array, accessing the IStorable methods and property. You cannot, however, access the Document-specific methods because all the compiler knows is that you have an IStorable, not a Document.

As we mentioned before, 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 automatic property Status. However, myStorable would not have access to the Document's private member variables, if it had any. The IStorable reference knows only about the IStorable interface, not about the Document's internal members.

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

The is and as Operators

Sometimes, however, you may not know at compile time whether an object supports a particular interface. For instance, if you have an array 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.

Warning

Anytime you see casting, you can question the design of the program. It is common for casting to be the result of poor or lazy design. That being said, sometimes casting is unavoidable, especially when dealing with collections that you did not create. This is one of those situations where experience over time will help you tell good designs from bad.

You could try casting each member blindly to ICompressible. If the object in question doesn't implement ICompressible, an error will be raised. You could then handle that error, using techniques we'll explain in Chapter 16, Throwing and Catching Exceptions. That's a sloppy and ineffective way to do it, though. The is and as operators provide a much better way.

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 ( expression is type )

The is operator evaluates true if the expression (which must be a reference type, such as an instance of a class) can be safely cast to type without throwing an exception.

The as operator is similar to is, but it goes a step further. The as operator tries to cast the object to the type, and if an exception would be thrown, it instead returns null:

ICompressible myCompressible = myObject as ICompressible
if ( myCompressible != null )

Warning

The is operator is slightly less efficient than using as, so the as operator is slightly preferred over the is operator, except when you want to do the test but not actually do the cast (a rare situation).

Example 13.3, "The is and as operators allow you to determine whether an object can be cast to an interface" 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 (which could be either Notes or Documents) and then, if you want to access either ICompressible or the ID, you'll need to test the Note to see whether 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 allow you to determine whether an object can be cast to an interface

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Example_13_3_ _ _ _is_and_as
{
    interface IStorable
    {
        void Read( );
        void Write(object obj);
        int Status { get; set; }
    }

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

    public class Note : IStorable
    {
        private string myString;

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

        public override string ToString( )
        {
            return myString;
        }

        #region IStorable

        public void Read( )
        {
            Console.WriteLine("Executing Note's Read Method
                               for IStorable");
        }

        public void Write(object o)
        {
            Console.WriteLine("Executing Note's Write Method
                               for IStorable");
        }

        public int Status { get; set; }

        #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("Executing Document's Compress Method
                               for ICompressible");
        }
        public void Decompress( )
        {
            Console.WriteLine("Executing Document's Decompress Method
                               for ICompressible");
        }
        #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);
                }
            }

            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
}

The output looks like this:

Testing String 0 with IS
Executing Note's Read Method for IStorable
Executing Document's Compress Method for ICompressible
my documentID is 50

Testing String 1 with IS
Executing Note's Read Method for IStorable
This storable object is not compressible.

Testing String 2 with IS
Executing Note's Read Method for IStorable
Executing Document's Compress Method for ICompressible
my documentID is 70

Testing String 0 with AS
Executing Document's Compress Method for ICompressible
My documentID is 50

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

Testing String 2 with AS
Executing Document's Compress Method for ICompressible
My documentID is 70

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

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

Within the Run( ) method of the Tester class, you create an array of Note objects, and you add to that array two Document instances and one Note instance. You use the counter i of the for loop as a control-if i is even, you create a Document object; if it's odd, you create a Note.

You then iterate through the array, extracting each Note in turn, and use the is operator to test first whether the Note can safely be assigned to an ICompressible reference:

if (theNote is ICompressible)
{
    ICompressible myCompressible = theNote as ICompressible;
    myCompressible.Compress( );
}
else
{
    Console.WriteLine("This storable object is not compressible.");
}

If it can, you cast theNote to ICompressible, and call the Compress( ) method.

Then you check whether the Note can safely be cast to a Document:

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

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

In the case shown, these tests amount to the same thing, but you can imagine that you could have a collection with many types derived from Note, some of which implement ICompressible and some of which do not.

You can use the interim variable as we've done here:

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, as you do in the second loop:

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. (The second foreach loop actually generates less intermediate language code, and thus is slightly more efficient.)

Extending Interfaces

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

Example 13.4, "You can extend an interface to create a new interface with additional methods or members" extends ICompressible to create ILoggedCompressible, and then casts the Document first to be of type IStorable and 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).

Example 13.4. You can extend an interface to create a new interface with additional methods or members

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

namespace Example_13_4_ _ _ _Extending_Interfaces
{
    interface ICompressible
    {
        void Compress( );
        void Decompress( );
    }

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

    public class Document : ILoggedCompressible
    {

        public Document(string s)
        {

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

        #region

        public void Compress( )
        {
            Console.WriteLine("Executing Compress");
        }

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

        public void LogSavedBytes( )
        {
            Console.WriteLine("Executing LogSavedBytes");
        }

        #endregion //ILoggedCompressible
    }

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

            ILoggedCompressible myLoggedCompressible =
                                doc as ILoggedCompressible;
            if (myLoggedCompressible != null)
            {
                Console.Write("\nCalling both ICompressible and ");
                Console.WriteLine("ILoggedCompressible methods...");
                myLoggedCompressible.Compress( );
                myLoggedCompressible.LogSavedBytes( );
            }
            else
            {
                Console.WriteLine("Something went wrong!
                                   Not ILoggedCompressible");
            }
        }

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

The output looks like this:

Creating document with: Test Document

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

Example 13.4, "You can extend an interface to create a new interface with additional methods or members" 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 anICompressible object.)

Combining Interfaces

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 precompressed 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 storableDoc = doc as IStorable;
ILoggedCompressible logCompressDoc = doc as ILoggedCompressible; 
ICompressible compressDoc = doc as ICompressible;
IStorableCompressible storCompressDoc = 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 IStorableCompressible extends or combines.

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

Overriding Interface Methods

When you create an implementing class, you're 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, "You can override an interface implementation in the same way that you would override any virtual method of a parent class" uses the Note and Document classes, but we've taken out the extra complexity we added in the last few examples, to focus on overriding an interface implementation. Note implements the IStorable-required Read( ) method as a virtual method, and Document overrides that implementation.

Tip

Notice that Note does not mark Write( ) as virtual. You'll see the implications of this decision in the analysis that follows Example 13.5, "You can override an interface implementation in the same way that you would override any virtual method of a parent class".

The complete listing is shown in Example 13.5, "You can override an interface implementation in the same way that you would override any virtual method of a parent class".

Example 13.5. You can override an interface implementation in the same way that you would override any virtual method of a parent class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Example_13_5_ _ _ _Overriding_Interface_Implementation
{
    interface IStorable
    {
        void Read( );
        void Write( );
    }

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

        // Note's version of Read( ) is virtual
        public virtual void Read( )
        {
            Console.WriteLine("Note Read Method for IStorable");
        }

        // Note's version of Write( ) is 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 a new 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 a new Write method for Document!

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

In Example 13.5, "You can override an interface implementation in the same way that you would override any virtual method of a parent class", 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( )

Tip

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

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 reference created from the Note class reference to the Document object

  • Through a Document object

  • Through an interface reference created from the Document object

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

When you call the nonpolymorphic 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 nonvirtual.

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( ), and the nonvirtual Write( ) method of the Note class is invoked because the Write( ) method 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 nonvirtual 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 nonvirtual 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 theDoc = new Document("Second Test");

Call the methods on the Document object:

theDoc.Read( );
theDoc.Write( );

Again, the virtual Read( ) method is polymorphic and the nonvirtual 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 a new Write method for Document!

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

IStorable isDocument2 = theDoc 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

Explicit Interface Implementation

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 understands 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:

void ITalk .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 keyword, either.

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, "Explicit implementation allows you to avoid conflicts when two interfaces have methods with the same name". 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.

Example 13.6. Explicit implementation allows you to avoid conflicts when two interfaces have methods with the same name

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

namespace Example_13_6_ _ _ _Explicit_Interfaces
{
    interface IStorable
    {
        void Read( );
        void Write( );
    }

    interface ITalk
    {
        void Talk( );
        void Read( );
    }

    // Modify Document to also implement ITalk
    public class Document : IStorable, ITalk
    {

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

        // Implicit implementation
        public virtual void Read( )
        {
            Console.WriteLine("Document Read Method for IStorable");
        }

        public void Write( )
        {
            Console.WriteLine("Document Write Method for IStorable");
        }

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

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

    class Tester
    {
        public void Run( )
        {
            // Create a Document object
            Document theDoc = new Document("Test Document");
            IStorable isDoc = theDoc as IStorable;
            if (isDoc != null)
            {
                isDoc.Read( );
            }

            // Cast to an ITalk interface
            ITalk itDoc = theDoc as ITalk;
            if (itDoc != null)
            {
                itDoc.Read( );
            }

            theDoc.Read( );
            theDoc.Talk( );

        }

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

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

The first thing the program does is create an IStorable reference to a Document. Then it invokes the Read( ) method of IStorable:

IStorable isDoc = theDoc as IStorable;
if (isDoc != null)
{
    isDoc.Read( );
}

Then you cast the document to an ITalk interface, and invoke the Read( ) method of ITalk:

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

The output shows that both of these calls work as expected.

The next thing you do is to call the Read( ) and Talk( ) methods directly on the Document:

theDoc.Read( );
theDoc.Talk( );

As you can see in the output, the Read( ) method defaults to the version from IStorable, because that version is implicit. The Talk( ) method is the version from ITalk, because that's the only interface here with a Talk( ) method.

Summary

  • An interface is a contract through which a class guarantees that it will implement certain methods, provide certain properties and indexers, and support certain events, all of which are specified in the interface definition.

  • You cannot create an instance of an interface. To access the interface methods, you need to create an instance of a class that implements that interface.

  • You declare an interface much like you would a class, but using the keyword interface. You can apply access modifiers to the interface, as you would with a class.

  • In the interface definition, the method declarations cannot have access modifiers.

  • To implement an interface on a class, you use the colon operator, followed by the name of the interface, similar to the syntax for inheritance.

  • Classes can derive from no more than one class, but can implement any number of interfaces. If a class has a base class and one or more interfaces, the base class must be listed first (after the colon). Separate the base class and interface names by commas.

  • When you define a class that implements an interface, you must then implement all the required members of that interface.

  • In situations where you don't know what type of object you have, and you know only that the object implements a specific interface, you can create a reference to the interface and assign the object to that reference, providing you with access to the implemented interface methods.

  • You can use the is operator to determine whether an object derives from a base class or implements an interface. The is operator returns a Boolean value indicating whether the cast is valid, but it does not perform the cast.

  • The as operator attempts to cast a reference to a base type or an interface, and returns null if the cast is not valid.

  • You can extend an interface to add new methods or members. In the new interface definition, use the colon operator followed by the name of the original interface. This is very similar to derivation in classes.

  • The extended interface subsumes the original interface, so any class that implements the extended interface must also implement the original interface as well.

  • A class that implements an interface may mark any of the interface methods as virtual. These methods may then be overridden by derived classes.

  • When a class implements two or more interfaces with methods that have the same name, you resolve the conflict by prefixing the method name with the name of the interface and the dot operator (for example, IStorable.Write( )). If you do this, you cannot specify an access modifier, as the method is implicitly public.

You saw in this chapter how interfaces encourage polymorphism, allowing you to dictate the methods your classes implement, while still providing flexibility in your designs. They're admittedly somewhat tricky to understand at first, but as you get more practice with them, you'll get more comfortable with them. In this chapter, you used arrays to demonstrate the polymorphic features of interfaces, and that's a pretty common use. However, the array isn't the only collection class in C#. In fact, although arrays are simplest to understand, which is why we introduced them first, in many ways they're the most limited of the collection classes. In the next chapter, you'll learn about the other collection classes, and how you can use them with generics to get even more flexibility.

Test Your Knowledge: Quiz

Question 13-1. What is the difference between an interface and a class that implements an interface?

Question 13-2. What is the difference between an interface and an abstract base class?

Question 13-3. How do you create an instance of an interface?

Question 13-4. How do you indicate that class MyClass derives from class MyBase and implements the interfaces ISuppose and IDo?

Question 13-5. What two operators can tell you whether an object's class implements an interface?

Question 13-6. What is the difference between the is and as operators?

Question 13-7. What does it mean to "extend" an interface?

Question 13-8. What is the syntax for extending an interface?

Question 13-9. What does it mean to override an interface implementation?

Question 13-10. What is explicit interface implementation and why would you use it?

Test Your Knowledge: Exercises

Exercise 13-1. Define an interface IConvertible that indicates that the class can convert a block of code to C# or VB. The interface should have two methods: ConvertToCSharp and ConvertToVB. Each method should take a string and return a string.

Exercise 13-2. Implement that interface and test it by creating a class ProgramHelper that implements IConvertible. You don't have to write methods to convert the string; just use simple string messages to simulate the conversion. Test your new class with a string of fake code to make sure it works.

Exercise 13-3. Extend the IConvertible interface by creating a new interface, ICodeChecker. The new interface should implement one new method, CodeCheckSyntax, which takes two strings: the string to check and the language to use. The method should return a bool. Revise the ProgramHelper class from Exercise 13-2 to use the new interface.

Exercise 13-4. Demonstrate the use of is and as. Create a new class, ProgramConverter, which implements IConvertible. ProgramConverter should implement the ConvertToCSharp( ) and ConvertToVB( ) methods. Revise ProgramHelper so that it derives from ProgramConverter and implements ICodeChecker. Test your class by creating an array of ProgramConverter objects, some of which are ProgramConverters and some of which are ProgramHelpers. Then call the conversion methods and the code check methods on each item in the array to test which ones implement ICodeChecker and which ones do not.