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.
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:
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.
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 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.
The syntax for defining an interface is very similar to the syntax for defining a class:
[attributes
] [access-modifier
] interfaceinterface-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.
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.
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.
using System;namespace InterfaceDemo { interface IStorable { void Read( ); void Write( object obj ); int Status { get; set; } } public class Document : IStorable { // store the value for the IStorable required property private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } #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 } 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 Implementing the Read Method for IStorable Document Status: -1
In Example
13-1, 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
(object obj
), and, of course, you
end all statements with a semicolon.
An interface can also require that the implementing class
provide a property (see Chapter
8 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 is a get( )
and a set( )
:
int Status { get; set; }
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.
You can make a reference to an interface, but you must assign an actual implementing object to that reference:
IStorable myStorable = new Document( );
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).
Classes can derive from only one class (and if it doesn’t
explicitly derive from a class, then it implicitly derives from Object
).
Some languages, such as C++, support implementation from multiple base classes (called Multiple Inheritance or MI). In 10 years of working with C++, I never used MI except to demonstrate that it could be done, and in 6 years of working with C#, I’ve never missed MI.
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.
Both IStorable
and ICompressible
are interfaces created for
this book and are not part of the standard .NET Framework.
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.
using System; namespace InterfaceDemo { interface IStorable { void Read( ); void Write( object obj ); int Status { get; set; } }// here's the new interface interface ICompressible { void Compress( ); void Decompress( ); } public class Document : IStorable, ICompressible { private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } #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 #region ICompressible public void Compress( ) { Console.WriteLine( "Implementing Compress" ); } public void Decompress( ) { Console.WriteLine( "Implementing Decompress" ); } #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
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.
Any time you see casting, you must question the design of the program. It is common for casting to be the result of poor or lazy design. That said, there are times that casting is unavoidable, especially when dealing with nongeneric collections that you did not create.
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 )
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
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.
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
).
using System; namespace ExtendingInterfaces { 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( "Implementing Compress" ); } public void Decompress( ) { Console.WriteLine( "Implementing Decompress" ); } public void LogSavedBytes( ) { Console.WriteLine( "Implementing 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... 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.
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.
The complete listing is shown in Example 13-5.
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( )
In a real-world application, you would almost certainly mark both methods as virtual, but I’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 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.
using System; namespace OverridingInterfaces { 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( ); } [STAThread] 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
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 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 zero or one class, and can implement any number of interfaces. If a class has a base class and one or more interface, the base class must be listed first (after the colon). Separate base classes and implementation 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, just 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 if an object derives from a base class or implements an
interface. The is
operator
returns a Boolean value indicating whether or not 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( )
).
What is the difference between an interface and a class that implements an interface?
What is the difference between an interface and an abstract base class?
How do you indicate that class MyClass
derives from class MyBase
and implements the interfaces
ISuppose
and IDo
?
What two operators can tell you if an object’s class implements an interface?
What is the difference between the is
and as
operators?
What does it mean to “extend” an interface?
What is the syntax for extending an interface?
What does it mean to override an interface implementation?
What is explicit interface implementation and why would you use it?
Define an interface IConvertible
that indicates that the
class can convert a string to C# or VB2005. The interface should
have two methods: ConvertToCSharp
and ConvertToVB2005
. Each method should take
a string, and return a string.
Implement that interface and test it by creating a class
ProgramHelper
that implements
IConvertible
. You can use
simple string messages to simulate the conversion.
Extend the 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.
Demonstrate the use of is
and as
. Create a new class,
ProgramConverter
, that
implements IConvertible
.
ProgramConverter
should
implement the ConvertToC-Sharp( )
and ConvertToVB( )
methods. Revise ProgramHelper
so that it derives from ProgramConverter
, and implements
ICodeChecker
.
[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#.