An interface is a contract that guarantees to a client how a class or struct will behave. When a class (or struct) implements an interface, it tells any potential client “I guarantee I’ll support the methods, properties, events, and indexers of the named interface.” (See Chapter 4 for information about methods and properties, Chapter 12 for information about events, and Chapter 9 for coverage of indexers.)
An interface offers an alternative to an abstract class for creating
contracts among classes and their clients. These contracts are made
manifest using the interface
keyword, which
declares a reference type that encapsulates the contract.
When you define an interface, you may define methods, properties, indexers, and/or events that will be implemented by the class that implements the interface.
Interfaces are often compared to abstract classes. An abstract class serves as the base class for a family of derived classes, while interfaces are meant to be mixed in with other inheritance trees.
For the rest of this chapter, wherever you see the word class, assume the text applies equally to structs, unless noted otherwise.
When a class implements an interface, it must implement all the parts of that interface (methods, properties, etc.); in effect, the class says “I agree to fulfill the contract defined by this interface.”
Java programmers take note: C# doesn’t support the use of constant fields (member constants) in interfaces. The closest analog is the use of enumerated constants (enums).
You will remember from Chapter 5 that
inheriting from an abstract class implements the
is-a
relationship. Implementing an interface,
on the other hand, defines a different relationship that
we’ve not seen until now, called (not surprisingly)
the implements
relationship. These two relationships are subtly different. A car
is-a vehicle, but it might
implement the
CanBeBoughtWithABigLoan
capability (as can a
house, for example).
In this chapter, you will learn how to create, implement, and use interfaces. You’ll learn how to implement multiple interfaces, and how to combine and extend interfaces, as well as how to test whether a class has implemented an interface.
The syntax for defining an interface is as follows:
[attributes
] [access-modifier
] interfaceinterface-name
[:base-list
] {interface-body
}
Don’t worry about attributes for now; they’re covered in Chapter 18.
Access modifiers, including public
,
private
, protected
,
internal
, and protected
internal
, were discussed in Chapter 4.
The
interface
keyword is
followed by the name of the interface. It is common (but not
required) to begin the name of your interface with a capital
I
(thus, IStorable
,
ICloneable
, IClaudius
, etc.).
The base-list
lists the interfaces that this
interface extends (as described in the next section,
Section 8.1.1).
The interface-body
describes the methods,
properties, and so forth that must be implemented by the implementing
class.
Suppose you wish to create an interface that describes the methods
and properties a class needs, to be stored to and retrieved from a
database or other storage such as a file. You decide to call this
interface IStorable
.
In this interface you might specify two methods:
Read()
and Write()
, which
appear in the interface-body
.
interface IStorable { void Read(); void Write(object); }
The purpose of an interface is to define the capabilities that you want to have available in a class.
For example, you might create a class, Document
.
It turns out that Document
types can be stored in
a database, so you decide to have Document
implement the IStorable
interface.
To do so, use the same syntax as if the new
Document
class were inheriting from
IStorable
—a colon (:),
followed by the interface name:
public class Document : IStorable { public void Read() {...} public void Write(object obj) {...} // ... }
It is now your responsibility, as the author of the
Document
class, to provide a meaningful
implementation of the IStorable
methods. Having
designated Document
as implementing
IStorable
, you must implement all the
IStorable
methods, or you will generate an error
when you compile. This is illustrated in Example 8-1, in which the Document
class implements the IStorable
interface.
Example 8-1. Using a simple interface
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace SimpleInterface { // declare the interface interfaceIStorable { // no access modifiers, methods are public // no implementation void Read( ); void Write( object obj ); int Status { get; set; } } // create a class which implements the IStorable interface public class Document : IStorable { // store the value for the property private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // implement the Read method public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable" ); } // implement the Write method public void Write( object o ) { Console.WriteLine( "Implementing the Write Method for IStorable" ); } // implement the property public int Status { get { return status; } set { status = value; } } } // Take our interface out for a spin public class Tester { static void Main( ) { // access the methods in the Document object Document doc = new Document( "Test Document" ); doc.Status = -1; doc.Read( ); Console.WriteLine( "Document Status: {0}", doc.Status ); } } } Output: Creating document with: Test Document Implementing the Read Method for IStorable Document Status: -1
Example 8-1 defines a simple interface,
IStorable
, with two methods
(Read()
and Write()
) and a
property (Status
) of type
integer
. Notice that the property declaration
doesn’t provide an implementation for
get( )
and set( )
, but simply
designates that there is a
get( )
and a set( )
:
int Status { get; set; }
Notice also that the IStorable
method declarations
don’t include access modifiers (for example,
public
, protected
,
internal
, private
). In fact,
providing an access modifier generates a compile error. Interface
methods are implicitly public
because an interface
is a contract meant to be used by other classes. You
can’t create an instance of an interface; instead
you instantiate a class that implements the interface.
The class implementing the interface must fulfill the contract
exactly and completely. Document
must provide both
a Read()
and a Write()
method
and the Status
property. How
it fulfills these requirements, however, is entirely up to the
Document
class. Although
IStorable
dictates that
Document
must have a Status
property, it doesn’t know or care whether
Document
stores the actual status as a member
variable or looks it up in a database. The details are up to the
implementing class.
Classes can implement more than one
interface. For example, if your Document
class can
be stored and it also can be compressed, you might choose to
implement both the IStorable
and
ICompressible
interfaces. To do so, change the
declaration (in the base list) to indicate that both interfaces are
implemented, separating the two interfaces with commas:
public class Document : IStorable, ICompressible
Having done this, the Document
class must also
implement the methods specified by the
ICompressible
interface (which is declared in
Example 8-2):
public void Compress() { Console.WriteLine("Implementing the Compress Method"); } public void Decompress( ) { Console.WriteLine("Implementing the Decompress Method"); }
It
is possible to extend an existing interface to add
new methods or members, or to modify how existing members work. For
example, you might extend ICompressible
with a new
interface, ILoggedCompressible
, which extends the
original interface with methods to keep track of the bytes saved:
interface ILoggedCompressible : ICompressible { void LogSavedBytes(); }
Effectively, by extending ICompressible
in this
way, you are saying that anything that implements
ILoggedCompressible
must also implement
ICompressible
.
Classes are now free to implement either
ICompressible
or
ILoggedCompressible
, depending on whether they
need the additional functionality. If a class does implement
ILoggedCompressible
, it must implement all the
methods of both ILoggedCompressible
and
ICompressible
. Objects of that type can be cast
either to ILoggedCompressible
or to
ICompressible
.
Similarly,
you can create new interfaces by combining existing interfaces and,
optionally, adding new methods or properties. For example, you might
decide to create IStorableCompressible
. This
interface would combine the methods of each of the other two
interfaces, but would also add a new method to store the original
size of the precompressed item:
interface IStorableCompressible : IStorable, ILoggedCompressible { void LogOriginalSize(); }
Example 8-2 illustrates extending and combining interfaces.
Example 8-2. Extending and combining interfaces
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ExtendAndCombineInterface { interfaceIStorable { void Read( ); void Write( object obj ); int Status { get; set; } } // here's the new interface interface ICompressible { void Compress( ); void Decompress( ); } // Extend the interface interface ILoggedCompressible : ICompressible { void LogSavedBytes( ); } // Combine Interfaces interface IStorableCompressible : IStorable, ILoggedCompressible { void LogOriginalSize( ); } // yet another interface interface IEncryptable { void Encrypt( ); void Decrypt( ); } public class Document : IStorableCompressible, IEncryptable { // hold the data for IStorable's Status property private int status = 0; // the document constructor public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // implement IStorable public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable" ); } public void Write( object o ) { Console.WriteLine( "Implementing the Write Method for IStorable" ); } public int Status { get { return status; } set { status = value; } } // implement ICompressible public void Compress( ) { Console.WriteLine( "Implementing Compress" ); } public void Decompress( ) { Console.WriteLine( "Implementing Decompress" ); } // implement ILoggedCompressible public void LogSavedBytes( ) { Console.WriteLine( "Implementing LogSavedBytes" ); } // implement IStorableCompressible public void LogOriginalSize( ) { Console.WriteLine( "Implementing LogOriginalSize" ); } // implement IEncryptable public void Encrypt( ) { Console.WriteLine( "Implementing Encrypt" ); } public void Decrypt( ) { Console.WriteLine( "Implementing Decrypt" ); } } public class Tester { static void Main( ) { // create a document object Document doc = new Document( "Test Document" ); // cast the document to the various interfaces IStorable isDoc = doc as IStorable; if ( isDoc != null ) { isDoc.Read( ); } else Console.WriteLine( "IStorable not supported" ); ICompressible icDoc = doc as ICompressible; if ( icDoc != null ) { icDoc.Compress( ); } else Console.WriteLine( "Compressible not supported" ); ILoggedCompressible ilcDoc = doc as ILoggedCompressible; if ( ilcDoc != null ) { ilcDoc.LogSavedBytes( ); ilcDoc.Compress( ); // ilcDoc.Read( ); } else Console.WriteLine( "LoggedCompressible not supported" ); IStorableCompressible isc = doc as IStorableCompressible; if ( isc != null ) { isc.LogOriginalSize( ); // IStorableCompressible isc.LogSavedBytes( ); // ILoggedCompressible isc.Compress( ); // ICompressible isc.Read( ); // IStorable } else { Console.WriteLine( "StorableCompressible not supported" ); } IEncryptable ie = doc as IEncryptable; if ( ie != null ) { ie.Encrypt( ); } else Console.WriteLine( "Encryptable not supported" ); } } } Output: Creating document with: Test Document Implementing the Read Method for IStorable Implementing Compress Implementing LogSavedBytes Implementing Compress Implementing LogOriginalSize Implementing LogSavedBytes Implementing Compress Implementing the Read Method for IStorable Implementing Encrypt
Example 8-2 starts by implementing the
IStorable
interface and the
ICompressible
interface. The latter is extended to
ILoggedCompressible
and then the two are combined
into IStorableCompressible
. Finally, the example
adds a new interface, IEncryptable
.
The Tester
program creates a new
Document
object and then uses it as an instance of
the various interfaces. You are free to cast:
ICompressible icDoc = doc as ICompressible;
But this is unnecessary. The compiler knows that
doc
implements ICompressible
and so can make the implicit cast for you:
ICompressible icDoc = doc;
On the other hand, if you are uncertain whether your class does
implement a specific interface, you can cast using the
as
operator (described in detail later in this
chapter), and then test whether the cast object is null (indicating
that the cast was not legal) instead of assuming the cast and risk
raising an exception.
ICompressible icDoc = doc as ICompressible; if ( icDoc != null ) { icDoc.Compress( ); } else Console.WriteLine( "Compressible not supported" );
When the object is cast to ILoggedCompressible
,
you can use the interface to call methods on
ICompressible
because
ILoggedCompressible
extends (and thus subsumes)
the methods from the base interface:
ILoggedCompressible ilcDoc = doc as ILoggedCompressible; if (ilcDoc != null) { ilcDoc.LogSavedBytes( ); ilcDoc.Compress( ); // ilcDoc.Read( ); }
You can’t call Read( )
, however,
because that is a method of IStorable
, an
unrelated interface. And if you uncomment out the call to
Read( )
, you will receive a compiler error.
If you cast to IStorableCompressible
(which
combines the extended interface with the Storable
interface), you can then call methods of
IStorableCompressible
,
ICompressible
, and IStorable
:
IStorableCompressible isc = doc as IStorableCompressible if (isc != null) { isc.LogOriginalSize( ); // IStorableCompressible isc.LogSavedBytes( ); // ILoggedCompressible isc.Compress( ); // ICompressible isc.Read( ); // IStorable }
You can access the
members
of the IStorable
interface as if they were members
of the Document
class:
Document doc = new Document("Test Document"); doc.status = -1; doc.Read();
You can also create an instance of the interface[1] by casting the document to the interface type, and then use that interface to access the methods:
IStorable isDoc = doc; isDoc.status = 0; isDoc.Read( );
In this case, in Main( )
you
know that Document
is in fact
an IStorable
, so you can take advantage of that
knowledge and not explicitly cast or test the cast.
As stated earlier, you can’t instantiate an interface directly. That is, you can’t say:
IStorable isDoc = new IStorable();
You can, however, create an instance of the implementing class, as in the following:
Document doc = new Document("Test Document");
You can then create an instance of the interface by casting the
implementing object to the interface type
, which
in this case is IStorable
:
IStorable isDoc = doc;
You can combine these steps by writing:
IStorable isDoc = new Document("Test Document");
Access through an interface allows you to treat the interface polymorphically. In other words, you can have two or more classes implement the interface, and then by accessing these classes only through the interface, you can ignore their real runtime type and treat them interchangeably. See Chapter 5 for more information about polymorphism.
In many cases, you
don’t know in advance that an object supports a
particular interface. Given a collection of objects, you might not
know whether a particular object supports
IStorable
or ICompressible
or
both. You can just cast to the interfaces:
Document doc = myCollection[0]; IStorable isDoc = (IStorable) doc; isDoc.Read( ); ICompressible icDoc = (ICompressible) doc; icDoc.Compress( );
If it turns out that Document
implements only the
IStorable
interface:
public class Document : IStorable
the cast to ICompressible
still compiles because
ICompressible
is a valid interface. However,
because of the illegal cast, when the program is run, an exception is
thrown:
An exception of type System.InvalidCastException was thrown.
Exceptions are covered in detail in Chapter 11.
You would like to be able to ask the
object if it supports the interface, to then invoke the appropriate
methods. In C# there are two ways to accomplish this. The first
method is to use the is
operator. The form of the
is
operator is:
expression
istype
The is
operator evaluates true
if the expression (which must be a reference type) can be safely cast
to type
without throwing an exception.[2]
Example 8-3 illustrates
the use of the is
operator to test whether a
Document
implements the
IStorable
and ICompressible
interfaces.
Example 8-3. Using the is operator
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace IsOperator { interfaceIStorable { void Read( ); void Write( object obj ); int Status { get; set; } } // here's the new interface interface ICompressible { void Compress( ); void Decompress( ); } // Document implements IStorable public class Document : IStorable { private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // IStorable.Read public void Read( ) { Console.WriteLine( "Reading..."); } // IStorable.Write public void Write( object o ) { Console.WriteLine( "Writing..."); } // IStorable.Status public int Status { get { return status; } set { status = value; } } } // derives from Document and implements ICompressible public class CompressibleDocument : Document, ICompressible { public CompressibleDocument(String s) : base(s) { } public void Compress( ) { Console.WriteLine("Compressing..."); } public void Decompress( ) { Console.WriteLine("Decompressing..."); } } public class Tester { static void Main( ) { // A collection of Documents Document[] docArray = new Document[2]; // First entry is a Document docArray[0] = new Document( "Test Document" ); // Second entry is a CompressibleDocument (ok because // CompressibleDocument is a Document) docArray[1] = new CompressibleDocument("Test compressibleDocument"); // don't know what we'll pull out of this hat foreach (Document doc in docArray) { // report your name Console.WriteLine("Got: {0}", doc); // Both pass this test if (doc is IStorable) { IStorable isDoc = (IStorable)doc; isDoc.Read( ); } // fails for Document // passes for CompressibleDocument if (doc is ICompressible) { ICompressible icDoc = (ICompressible)doc; icDoc.Compress( ); } } } } } Output: Creating document with: Test Document Creating document with: Test compressibleDocument Got: IsOperator.Document Reading... Got: IsOperator.CompressibleDocument Reading... Compressing...
Example 8-3 differs from Example 8-2 in that Document
no longer
implements the ICompressible
interface, but a
class derived from Document
named
CompressibleDocument
does.
Main()
checks whether each cast is legal
(sometimes referred to as safe
) by evaluating the
following if
clause:
if (doc is IStorable)
This is clean and nearly self-documenting. The if
statement tells you that the cast will happen only if the object is
of the right interface type.
The Document
class passes this test, but fails the
next:
if (doc is ICompressible)
but the CompressibleDocument
passes both tests.
We put both types of documents into an array (you can imagine such an
array being handed to a method which can’t know its
contents). Before you try to call the
ICompressible
methods, you must be sure that the
type of Document
you have does implement
ICompressible
. The is
operator
makes that test for you.
Unfortunately, this use of the is
operator turns
out to be inefficient. To understand why, you need to dip into the
MSIL code that this generates. Here is a small excerpt (note that the
line numbers are in hexadecimal notation):
IL_0023: isinst ICompressible IL_0028: brfalse.s IL_0039 IL_002a: ldloc.0 IL_002b: castclass ICompressible IL_0030: stloc.2 IL_0031: ldloc.2 IL_0032: callvirt instance void ICompressible::Compress( )
What is most important here is the test for
ICompressible
on line 23. The keyword
isinst
is the MSIL code for the
is
operator. It tests to see if the object
(doc
) is in fact of the right type. Having passed
this test we continue on to line 2b, in which
castclass
is called. Unfortunately,
castclass
also tests the type of the object. In
effect, the test is done twice. A more efficient solution is to use
the as
operator.
The
as
operator
combines the is
and cast operations by testing
first to see whether a cast is valid (i.e., whether an
is
test would return true
) and
then completing the cast when it is. If the cast is not valid (i.e.,
if an is
test would return
false)
, the as
operator returns
null
.
Using the as
operator eliminates the need to
handle cast exceptions. At the same time you avoid the overhead of
checking the cast twice. For these reasons, it is optimal to cast
interfaces using as
.
The form of the as
operator is:
expression as type
The following code adapts the test code from Example 8-3, using the as
operator and
testing for null:
static void Main() { // A collection of DocumentsDocument[] docArray = new Document[2]; // First entry is a Document docArray[0] = new Document( "Test Document" ); // Second entry is a CompressibleDocument (ok because // CompressibleDocument is a Document) docArray[1] = new CompressibleDocument("Test compressibleDocument"); // don't know what we'll pull out of this hat foreach (Document doc in docArray) { // report your name Console.WriteLine("Got: {0}", doc); // Both pass this test IStorable isDoc = doc as IStorable; if (isDoc != null) { isDoc.Read( ); } // fails for Document // passes for CompressibleDocument ICompressible icDoc = doc as ICompressible; if (icDoc != null) { icDoc.Compress( ); } } }
A quick look at the comparable MSIL code shows that the following version is in fact more efficient:
IL_0023: isinst ICompressible IL_0028: stloc.2 IL_0029: ldloc.2 IL_002a: brfalse.s IL_0034 IL_002c: ldloc.2 IL_002d: callvirt instance void ICompressible::Compress( )
If your design pattern is to test the
object to see if it is of the type you need, and if so to immediately
cast it, the as
operator is more efficient. At
times, however, you might want to test the type of an operator but
not cast it immediately. Perhaps you want to test it but not cast it
at all; you simply want to add it to a list if it fulfills the right
interface. In that case, the is
operator will be a
better choice.
Interfaces are very similar to
abstract classes. In fact, you could change the declaration of
IStorable
to be an abstract class:
abstract class Storable { abstract public void Read(); abstract public void Write( ); }
Document
could now inherit from
Storable
, and there would not be much difference
from using the interface.
Suppose, however, that you purchase a List
class
from a third-party vendor whose capabilities you wish to combine with
those specified by Storable
. In C++, you could
create a StorableList
class and inherit from both
List
and Storable
. But in C#,
you’re stuck; you can’t inherit
from both the Storable
abstract class and also the
List
class because C# doesn’t
allow multiple inheritance with classes.
However, C# does allow you to implement any number of interfaces and
derive from one base class. Thus, by making
Storable
an interface, you can inherit from the
List
class and also from
IStorable
, as StorableList
does
in the following example:
public class StorableList : List, IStorable { // List methods here ... public void Read( ) {...} public void Write(object obj) {...} // ... }
Some designers at Microsoft discourage the use of interfaces and prefer abstract base classes because the latter do better with versioning.
For example, suppose you design an interface and programmers in your
shop start using it. You now want to add a new member to that
interface. You have two bad choices: you can either change the
interface and break existing code, or you can treat the interface as
immutable and create, for example, IStore2
or
IStorageExtended
. If you do that often enough,
however, you will soon have dozens of closely related interfaces and
a mess on your hands.
With an abstract base class, you can just add a new virtual method with a default implementation. Hey! Presto! Existing code continues to work, but no new class is introduced into the namespace.
The best practice seems to be that if you are creating a class library that will be reused by many people (especially outside your company), you might want to favor abstract base classes. If you are creating classes for a single project, however, interfaces may make for easier-to-understand and more flexible code.
An
implementing class is free to mark
any or all of the methods that implement the interface as virtual.
Derived classes can override
these implementations
to achieve polymorphism. For example, a Document
class might implement the IStorable
interface and
mark the Read()
and Write()
methods as virtual
. The
Document
might Read()
and
Write( )
its contents to a File
type. The developer might later derive new types from
Document
, such as a Note
or
EmailMessage
type, and he might decide that
Note
will read and write to a database rather than
to a file.
Example 8-4 strips down the complexity of Example 8-3 and illustrates overriding an interface
implementation. The Read()
method is marked as
virtual
and implemented by
Document
. Read()
is then
overridden in a Note
type that derives from
Document
.
Example 8-4. Overriding an interface implementation
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace overridingInterface { interfaceIStorable { void Read( ); void Write( ); } // Simplify Document to implement only IStorable public class Document : IStorable { // the document constructor public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // Make read virtual public virtual void Read( ) { Console.WriteLine( "Document Read Method for IStorable" ); } // NB: Not virtual! public void Write( ) { Console.WriteLine( "Document Write Method for IStorable" ); } } // Derive from Document public class Note : Document { public Note( string s ): base(s) { Console.WriteLine( "Creating note with: {0}", s ); } // override the Read method public override void Read( ) { Console.WriteLine( "Overriding the Read method for Note!" ); } // implement my own Write method public new void Write( ) { Console.WriteLine( "Implementing the Write method for Note!" ); } } public class Tester { static void Main( ) { // create a document reference to a Note object Document theNote = new Note( "Test Note" ); IStorable isNote = theNote as IStorable; if ( isNote != null ) { isNote.Read( ); isNote.Write( ); } Console.WriteLine( " " ); // direct call to the methods theNote.Read( ); theNote.Write( ); Console.WriteLine( " " ); // create a note object Note note2 = new Note( "Second Test" ); IStorable isNote2 = note2 as IStorable; if ( isNote2 != null ) { isNote2.Read( ); isNote2.Write( ); } Console.WriteLine( " " ); // directly call the methods note2.Read( ); note2.Write( ); } } } Output: Creating document with: Test Note Creating note with: Test Note Overriding the Read method for Note! Document Write Method for IStorable Overriding the Read method for Note! Document Write Method for IStorable Creating document with: Second Test Creating note with: Second Test Overriding the Read method for Note! Document Write Method for IStorable Overriding the Read method for Note! Implementing the Write method for Note!
In this example, Document
implements a simplified
IStorable
interface (simplified to make the
example clearer):
interface IStorable { void Read(); void Write( ); }
The designer of Document
has opted to make the
Read()
method virtual, but not to make the
Write( )
method virtual:
public virtual void Read()
In a real-world application, if you were to mark one as virtual, you
would almost certainly mark both as virtual
, but
I’ve differentiated them to demonstrate that the
developer is free to pick and choose which methods are made virtual.
The Note
class derives from
Document
:
public class Note : Document
It’s not necessary for Note
to
override Read()
, but it is free to do so and has
in fact done so here:
public override void Read()
In Tester
, the Read
and
Write
methods are called in four ways:
Through the base class reference to a derived object
Through an interface created from the base class reference to the derived object
Through a derived object
Through an interface created from the derived object
To accomplish the first two calls, a Document
(base class) reference is created, and the address of a new
Note
(derived) object created on the heap is
assigned to the Document
reference:
Document theNote = new Note("Test Note");
An interface reference is created and the as
operator is used to cast the Document
to the
IStorable
reference:
IStorable isNote = theNote as IStorable;
You then invoke the Read()
and
Write( )
methods through that interface. The output
reveals that the Read()
method is responded to
polymorphically and the Write( )
method is not,
just as we would expect:
Overriding the Read method for Note! Document Write Method for IStorable
The Read( )
and Write( )
methods
are then called directly on the object itself:
theNote.Read(); theNote.Write();
and once again you see the polymorphic implementation has worked:
Overriding the Read method for Note! Document Write Method for IStorable
In both cases, the Read( )
method of
Note
is called and the Write()
method of Document
is called.
To prove to yourself that this is a result of the overriding method,
next create a second Note
object, this time
assigning its address to a reference to a Note
.
This will be used to illustrate the final cases (i.e., a call through
a derived object and a call through an interface created from the
derived object):
Note note2 = new Note("Second Test");
Once again, when you cast to a reference, the overridden
Read( )
method is called. When, however, methods
are called directly on the Note
object:
note2.Read(); note2.Write();
the output reflects that you’ve called a
Note
and not an overridden
Document
:
Overriding the Read method for Note! Implementing the Write method for Note!
In the implementation
shown so far, the implementing class (in this case,
Document
) creates a member method with the same
signature and return type as the method detailed in the interface. It
is not necessary to explicitly state that this is an implementation
of an interface; this is understood by the compiler implicitly.
What happens, however, if the class implements two
interfaces, each of which has a
method with the same signature? Example 8-5 creates
two interfaces: IStorable
and
ITalk
. The latter implements a
Read( )
method that reads a book aloud.
Unfortunately, this conflicts with the Read()
method in IStorable
.
Because both IStorable
and
ITalk
have a Read()
method, the
implementing Document
class must use
explicit
implementation for
at least one of the methods. With explicit implementation, the
implementing class (Document
) explicitly
identifies the interface for the method:
void ITalk.Read()
This resolves the conflict, but it creates a series of interesting side effects.
First, there is no need to use explicit implementation with the other
method of Talk()
:
public void Talk( )
Because there is no conflict, this can be declared as usual.
More important, the explicit implementation method can’t have an access modifier:
void ITalk.Read( )
This method is implicitly public.
In fact, a method declared through explicit implementation
can’t be declared with the
abstract
, virtual
,
override
, or new
modifiers.
Most important, you can’t access the explicitly implemented method through the object itself. When you write:
theDoc.Read( );
the compiler assumes you mean the implicitly implemented interface
for IStorable
. The only way to access an
explicitly implemented interface is through a cast to an interface:
ITalk itDoc = theDoc; itDoc.Read();
Explicit implementation is demonstrated in Example 8-5.
Example 8-5. Explicit implementation
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ExplicitImplementation { interfaceIStorable { void Read( ); void Write( ); } interface ITalk { void Talk( ); void Read( ); } // Modify Document to implement IStorable and ITalk public class Document : IStorable, ITalk { // the document constructor public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } // Make read virtual public virtual void Read( ) { Console.WriteLine( "Implementing IStorable.Read" ); } public void Write( ) { Console.WriteLine( "Implementing IStorable.Write" ); } void ITalk.Read( ) { Console.WriteLine( "Implementing ITalk.Read" ); } public void Talk( ) { Console.WriteLine( "Implementing ITalk.Talk" ); } } public class Tester { static void Main( ) { // create a document object Document theDoc = new Document( "Test Document" ); IStorable isDoc = theDoc; isDoc.Read( ); ITalk itDoc = theDoc; itDoc.Read( ); theDoc.Read( ); theDoc.Talk( ); } } } Output: Creating document with: Test Document Implementing IStorable.Read Implementing ITalk.Read Implementing IStorable.Read Implementing ITalk.Talk
A class designer can take advantage of the fact that when an interface is implemented through explicit implementation, the interface is not visible to clients of the implementing class except through casting.
Suppose the semantics of your Document
object
dictate that it implement the IStorable
interface,
but you don’t want the Read()
and
Write( )
methods to be part of the public interface
of your Document
. You can use
explicit implementation to ensure that
they aren’t available except through casting. This
allows you to preserve the public API of your
Document
class while still having it implement
IStorable
. If your client wants an object that
implements the IStorable
interface, it can make a
cast, but when using your document as a Document
,
the API will not include Read( )
and
Write()
.
In fact, you can select which methods to make visible through
explicit implementation so that you can expose some implementing
methods as part of Document
but not others. In
Example 8-5, the Document
object
exposes the Talk()
method as a method of
Document
, but the ITalk.Read( )
method can be obtained only through a cast. Even if
IStorable
didn’t have a
Read()
method, you might choose to make
Read( )
explicitly implemented so that you
don’t expose Read()
as a method
of Document
.
Note that because explicit interface implementation prevents the use
of the virtual
keyword, a derived class would be
forced to reimplement the method. Thus, if Note
derived from Document
, it would be forced to
reimplement ITalk.Read()
because the
Document
implementation of
ITalk.Read()
couldn’t be virtual.
It is possible for an
interface member to become hidden. For
example, suppose you have an interface IBase
that
has a property P
:
interface IBase { int P { get; set; } }
Suppose you derive from that interface a new interface,
IDerived
, that hides the property
P
with a new method P()
:
interface IDerived : IBase { new int P(); }
Setting aside whether this is a good idea, you have now hidden the
property P
in the base interface. An
implementation of this derived interface will require at least one
explicit interface member. You can use explicit implementation for
either the base property or the derived method,
or you can use explicit implementation for both. Thus, any of the
following three versions would be legal:
class myClass : IDerived { // explicit implementation for the base property int IBase.P { get {...} } // implicit implementation of the derived method public int P( ) {...} } class myClass : IDerived { // implicit implementation for the base property public int P { get {...} } // explicit implementation of the derived method int IDerived.P( ) {...} } class myClass : IDerived { // explicit implementation for the base property int IBase.P { get {...} } // explicit implementation of the derived method int IDerived.P( ) {...} }
Generally, it is preferable to access the methods of an interface through an interface cast. The exception is with value types (e.g., structs) or with sealed classes. In that case, it is preferable to invoke the interface method through the object.
When you implement an interface in a struct, you are implementing it
in a value type. When you cast to an interface reference, there is an
implicit boxing of the object. Unfortunately,
when you use that interface to modify the object, it is the boxed
object, not the original value object, that is modified. Further, if
you change the value of the struct from inside the method, the boxed
type will remain unchanged (this is considered quite funny when it is
in someone else’s code). Example 8-6 creates a struct that implements
IStorable
and illustrates the impact of implicit
boxing when you cast the struct to an interface reference.
Example 8-6. References on value types
using System; #region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ReferencesOnValueTypes { // declare a simple interface interfaceIStorable { void Read( ); int Status { get;set;} } // Implement through a struct public struct myStruct : IStorable { public void Read( ) { Console.WriteLine( "Implementing IStorable.Read" ); } public int Status { get { return status; } set { status = value; } } private int status; } public class Tester { static void Main( ) { // create a myStruct object myStruct theStruct = new myStruct( ); theStruct.Status = -1; // initialize Console.WriteLine( "theStruct.Status: {0}", theStruct.Status ); // Change the value theStruct.Status = 2; Console.WriteLine( "Changed object." ); Console.WriteLine( "theStruct.Status: {0}", theStruct.Status ); // cast to an IStorable // implicit box to a reference type IStorable isTemp = ( IStorable ) theStruct; // set the value through the interface reference isTemp.Status = 4; Console.WriteLine( "Changed interface." ); Console.WriteLine( "theStruct.Status: {0}, isTemp: {1}", theStruct.Status, isTemp.Status ); // Change the value again theStruct.Status = 6; Console.WriteLine( "Changed object." ); Console.WriteLine( "theStruct.Status: {0}, isTemp: {1}", theStruct.Status, isTemp.Status ); } } } Output: theStruct.Status: -1 Changed object. theStruct.Status: 2 Changed interface. theStruct.Status: 2, isTemp: 4 Changed object. theStruct.Status: 6, isTemp: 4
In Example 8-6, the IStorable
interface has a method (Read
) and a property
(Status
).
This interface is implemented by the struct named
myStruct
:
public struct myStruct : IStorable
The interesting code is in Tester
. Start by
creating an instance of the structure and initializing its property
to -1
. The status value is then printed:
myStruct theStruct = new myStruct(); theStruct.status = -1; // initialize Console.WriteLine( "theStruct.Status: {0}", theStruct.status);
The output from this shows that the status was set properly:
theStruct.Status: -1
Next access the property to change the status, again through the value object itself:
// Change the value theStruct.status = 2; Console.WriteLine("Changed object."); Console.WriteLine( "theStruct.Status: {0}", theStruct.status);
The output shows the change:
Changed object. theStruct.Status: 2
No surprises so far. At this point, create a reference to the
IStorable
interface. This causes an implicit
boxing of the value object
theStruct
. Then use that interface to change the
status value to 4
:
// cast to an IStorable // implicit box to a reference type IStorable isTemp = (IStorable) theStruct; // set the value through the interface reference isTemp.status = 4; Console.WriteLine("Changed interface."); Console.WriteLine("theStruct.Status: {0}, isTemp: {1}", theStruct.status, isTemp.status);
Here the output can be a bit surprising:
Changed interface. theStruct.Status: 2, isTemp: 4
Aha! The object to which the interface reference points has been
changed to a status value of 4
, but the struct
value object is unchanged. Even more interesting, when you access the
method through the object itself:
// Change the value again theStruct.status = 6; Console.WriteLine("Changed object."); Console.WriteLine("theStruct.Status: {0}, isTemp: {1}", theStruct.status, isTemp.status);
the output reveals that the value object has been changed, but the boxed reference value for the interface reference has not:
Changed object. theStruct.Status: 6, isTemp: 4
A quick look at the MSIL code (Example 8-7) reveals what’s going on under the hood.
Example 8-7. MSIL code resulting from Example 8-6
.method private hidebysig static void Main() cil managed { .entrypoint // Code size 194 (0xc2) .maxstack 3 .locals init ([0] valuetype ReferencesOnValueTypes.myStruct theStruct, [1] class ReferencesOnValueTypes.IStorable isTemp) IL_0000: ldloca.s theStruct IL_0002: initobj ReferencesOnValueTypes.myStruct IL_0008: ldloca.s theStruct IL_000a: ldc.i4.m1 IL_000b: call instance void ReferencesOnValueTypes.myStruct:: set_Status(int32) IL_0010: ldstr "theStruct.Status: {0}" IL_0015: ldloca.s theStruct IL_0017: call instance int32 ReferencesOnValueTypes.myStruct:: get_Status( ) IL_001c: box [mscorlib]System.Int32 IL_0021: call void [mscorlib]System.Console::WriteLine(string, object) IL_0026: nop IL_0027: ldloca.s theStruct IL_0029: ldc.i4.2 IL_002a: call instance void ReferencesOnValueTypes.myStruct:: set_Status(int32) IL_002f: ldstr "Changed object." IL_0034: call void [mscorlib]System.Console::WriteLine(string) IL_0039: nop IL_003a: ldstr "theStruct.Status: {0}" IL_003f: ldloca.s theStruct IL_0041: call instance int32 ReferencesOnValueTypes.myStruct:: get_Status( ) IL_0046: box [mscorlib]System.Int32 IL_004b: call void [mscorlib]System.Console::WriteLine(string, object) IL_0050: nop IL_0051: ldloc.0 IL_0052: box ReferencesOnValueTypes.myStruct IL_0057: stloc.1 IL_0058: ldloc.1 IL_0059: ldc.i4.4 IL_005a: callvirt instance void ReferencesOnValueTypes.IStorable:: set_Status(int32) IL_005f: ldstr "Changed interface." IL_0064: call void [mscorlib]System.Console::WriteLine(string) IL_0069: nop IL_006a: ldstr "theStruct.Status: {0}, isTemp: {1}" IL_006f: ldloca.s theStruct IL_0071: call instance int32 ReferencesOnValueTypes.myStruct:: get_Status( ) IL_0076: box [mscorlib]System.Int32 IL_007b: ldloc.1 IL_007c: callvirt instance int32 ReferencesOnValueTypes.IStorable:: get_Status( ) IL_0081: box [mscorlib]System.Int32 IL_0086: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_008b: nop IL_008c: ldloca.s theStruct IL_008e: ldc.i4.6 IL_008f: call instance void ReferencesOnValueTypes.myStruct:: set_Status(int32) IL_0094: ldstr "Changed object." IL_0099: call void [mscorlib]System.Console::WriteLine(string) IL_009e: nop IL_009f: ldstr "theStruct.Status: {0}, isTemp: {1}" IL_00a4: ldloca.s theStruct IL_00a6: call instance int32 ReferencesOnValueTypes.myStruct:: get_Status( ) IL_00ab: box [mscorlib]System.Int32 IL_00b0: ldloc.1 IL_00b1: callvirt instance int32 ReferencesOnValueTypes.IStorable:: get_Status( ) IL_00b6: box [mscorlib]System.Int32 IL_00bb: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_00c0: nop IL_00c1: ret } // end of method Tester::Main
On line IL_000b
, set_Status( )
was called on the value object. We see the second call on line
IL_0017
. Notice that the calls to
WriteLine()
cause boxing of the integer value
status so that the GetString( )
method can be
called.
The key line is IL_001c
(highlighted) where the
struct itself is boxed. It is that boxing that creates a reference
type for the interface reference. Notice on line
IL_005a
that this time
IStorable::set_Status
is called rather than
myStruct::set_Status
.
The design guideline is if you are implementing an interface with a value type, be sure to access the interface members through the object rather than through an interface reference.
[1] Or more accurately, a properly cast reference to the object that implements the interface.
[2] Both the is
and the as
operator (described next) can be used to evaluate types through
inheritance, in addition to evaluating implementation of interfaces.
Thus, you can use is
to check whether a dog is a
mammal.
18.220.124.177