132
Chapter 8
CHAPTER 8
Interfaces 8
An interface is a contract that guarantees to a client how a class or struct will behave
(I’ll just use the term class for the rest of this chapter, though everything I say will
apply to structs as well).
When a class implements an interface, it tells any potential client “I guarantee I’ll
support all the methods, properties, events, and indexers of the named interface.”
(See Chapter 4 for information about methods and properties, Chapter 12 for infor-
mation about events, and Chapter 9 for coverage of indexers.) See also the sidebar
“Abstract Class Versus Interface Versus Mix-Ins.”
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
events that will (and must!) be implemented by any class that implements the 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).
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 inter-
faces, as well as how to test whether a class has implemented an interface.
Defining and Implementing an Interface
The syntax for defining an interface is as follows:
[attributes] [access-modifier] interface interface-name[:base-list]
{interface-body}
Don’t worry about attributes for now; I cover them in Chapter 20.
Defining and Implementing an Interface
|
133
I discussed access modifiers, including public, private, protected, internal, and
protected internal, 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, “Implementing More Than One Interface”).
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
{
Abstract Class Versus Interface Versus Mix-Ins
An interface offers an alternative to an abstract class for creating contracts among
classes and their clients; the difference is that abstract classes serve as the top of an
inheritance hierarchy, whereas interfaces may add their contract to numerous inherit-
ance trees.
Thus, for example, you might have an interface named
IPrintable (by convention,
interface names begin with a capital I, such as
IPrintable, IStorable, IClaudius).
IPrintable defines all the methods, events, and so on that a class must implement to
be printable, and any number of classes (notes, documents, calendar items, email,
spreadsheet documents) might implement that interface without having to share a
common root element.
Further, because a subset of these
IPrintable types might also be IStorable, using
interfaces rather than abstract classes keeps your inheritance tree much cleaner. This
allows inheritance to define the is-a relationship (a note is a document) rather than the
implements relationship (both notes and email implement
IPrintable).
Historical Note of Interest to East Coast Geeks: In Somerville, Massachusetts, there
was, at one time, an ice cream parlor where you could have candies and other goodies
“mixed in” with your chosen ice cream flavor. This seemed like a good metaphor to
some of the object-oriented pioneers from nearby MIT who were working on the for-
tuitously named SCOOPS programming language. They appropriated the term mix-in
for classes that mixed in additional capabilities. These mix-in—or capability—classes
serve much the same role as interfaces do in C#.
134
|
Chapter 8: Interfaces
void Read( );
void Write(object);
}
The purpose of an interface is to define the capabilities 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 though 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 mean-
ingful 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. I illustrate this in Example 8-1, in which the
Document class implements the IStorable interface.
Example 8-1. Using a simple interface
using System;
namespace SimpleInterface
{
interface IStorable
{
// 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
{
public Document(string s)
{
Console.WriteLine("Creating document with: {0}", s);
}
Defining and Implementing an Interface
|
135
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 declara-
tion 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 (e.g.,
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
// 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");
}
public int Status { get; set; }
}
// 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. Using a simple interface (continued)
136
|
Chapter 8: Interfaces
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 com-
pletely.
Document must provide both a Read( ) and a Write( ) method and the Status
property. How it fulfills these requirements, however, is entirely up to the Document
class. Although IStorable dictates that Document must have a Status property, it
doesn’t know or care whether
Document stores the actual status as a member variable
or looks it up in a database. The details are up to the implementing class.
Implementing More Than One Interface
Classes can implement more than one interface. For example, if your Document class
can be stored and it also can be compressed, you might choose to implement both
the
IStorable and ICompressible interfaces, shown here:
interface ICompressible
{
void Compress( );
void Decompress( );
}
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:
public void Compress( )
{
Console.WriteLine("Implementing the Compress Method");
}
public void Decompress( )
{
Console.WriteLine("Implementing the Decompress Method");
}
Extending Interfaces
It is possible to extend an existing interface to add new methods or members, or to
modify how existing members work. For example, you might extend
ICompressible
with a new interface, ILoggedCompressible, which extends the original interface with
methods to keep track of the bytes saved:
interface ILoggedCompressible : ICompressible
{
void LogSavedBytes( );
}
Defining and Implementing an Interface
|
137
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 to ILoggedCompressible or to
ICompressible.
Combining Interfaces
Similarly, you can create new interfaces by combining existing interfaces and,
optionally, adding new methods or properties. For example, you might decide to cre-
ate
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
using System;
namespace ExtendAndCombineInterface
{
interface IStorable
{
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( );
}
138
|
Chapter 8: Interfaces
// 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; set; }
// implement ICompressible
public void Compress( )
{
Console.WriteLine("Implementing Compress");
}
public void Decompress( )
{
Console.WriteLine("Implementing Decompress");
}
Example 8-2. Extending and combining interfaces (continued)
Defining and Implementing an Interface
|
139
// 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");
doc.Read( );
doc.Compress( );
doc.LogSavedBytes( );
doc.Compress( );
doc.LogOriginalSize( );
doc.LogSavedBytes( );
doc.Compress( );
doc.Read( );
doc.Encrypt( );
}
}
}
Output
Creating document with: Test Document
Implementing the Read Method for IStorable
Implementing Compress
Implementing LogSavedBytes
Implementing Compress
Example 8-2. Extending and combining interfaces (continued)
140
|
Chapter 8: Interfaces
Polymorphism with Interfaces
The problem with the approach we’ve taken so far is that you could well have a collec-
tion of
Document objects, some implementing IStorable, some implementing
ICompressible, some implementing ILoggedCompressible, some implementing
IStorableCompressible, and some implementing IEncryptable. If you just call
methods from each interface, sooner or later you’re going to throw an exception.
Let’s build such an example slowly, because this problem is very real, very confus-
ing, and very likely to cause a nasty bug in your program if it isn’t fully understood.
Start by declaring the interfaces just as you did in the previous example (I won’t
repeat them here). Next, rather than declaring a simple
Document class, let’s declare
an abstract
Document class, and two derived Document classes:
public abstract class Document { }
public class BigDocument : Document, IStorableCompressible, IEncryptable
{
//
}
The implementation of BigDocument is identical to the implementation of Document in
the previous example. There’s no change whatsoever, except that the constructor
must be named
BigDocument, and note that it now inherits from our abstract class.
Finally, let’s add a smaller type of
Document:
class LittleDocument : Document, IEncryptable
{
public LittleDocument(string s)
{
Console.WriteLine("Creating document with: {0}", s);
}
void IEncryptable.Encrypt( )
{
Console.WriteLine("Implementing Encrypt");
}
void IEncryptable.Decrypt( )
{
Console.WriteLine("Implementing Decrypt");
}
}
Implementing LogOriginalSize
Implementing LogSavedBytes
Implementing Compress
Implementing the Read Method for IStorable
Implementing Encrypt
Example 8-2. Extending and combining interfaces (continued)
Defining and Implementing an Interface
|
141
Notice that LittleDocument also inherits from Document, but it implements only one
interface:
IEncryptable.
Let’s change
Main, now to create a collection of Documents:
for (int i = 0; i < 5; i++)
{
if (i % 2 == 0)
{
folder[i] = new BigDocument("Big Document # " + i);
}
else
{
folder[i] = new LittleDocument("Little Document # " + i);
}
}
We create five documents, with the even-numbered ones being “big” and the odd-
numbered ones being “little.” If you now iterate through the “folder” (the array of
Document objects) and try to call various methods of the interface, you have a problem:
foreach (Document doc in folder)
{
doc.Read( );
doc.Compress( );
doc.LogSavedBytes( );
doc.Compress( );
doc.LogOriginalSize( );
doc.LogSavedBytes( );
doc.Compress( );
doc.Read( );
doc.Encrypt( );
}
This won’t compile—nor should it. The compiler cannot know which kind of
Document it has: a BigDocument (which can Read and Compress), or a LittleDocument
(which can’t).
To solve this problem, we need to see whether the
Document in question implements
the interface we want to use, as shown in Example 8-3.
Example 8-3. Collections of Documents
using System;
namespace ExtendAndCombineInterface
{
interface IStorable
{
void Read( );
void Write(object obj);
int Status { get; set; }
}
142
|
Chapter 8: Interfaces
// 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 abstract class Document { }
public class BigDocument : Document, IStorableCompressible, IEncryptable
{
// hold the data for IStorable's Status property
private int status = 0;
// the document constructor
public BigDocument(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");
Example 8-3. Collections of Documents (continued)
Defining and Implementing an Interface
|
143
}
public int Status { get; set; }
// 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");
}
}
class LittleDocument : Document, IEncryptable
{
public LittleDocument(string s)
{
Console.WriteLine("Creating document with: {0}", s);
}
void IEncryptable.Encrypt( )
{
Console.WriteLine("Implementing Encrypt");
}
Example 8-3. Collections of Documents (continued)
144
|
Chapter 8: Interfaces
void IEncryptable.Decrypt( )
{
Console.WriteLine("Implementing Decrypt");
}
}
public class Tester
{
static void Main( )
{
Document[] folder = new Document[5];
for (int i = 0; i < 5; i++)
{
if (i % 2 == 0)
{
folder[i] = new BigDocument("Big Document # " + i);
}
else
{
folder[i] = new LittleDocument("Little Document # " + i);
}
}
foreach (Document doc in folder)
{
// cast the document to the various interfaces
IStorable isStorableDoc = doc as IStorable;
if (isStorableDoc != null)
{
isStorableDoc.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( );
}
Example 8-3. Collections of Documents (continued)
Defining and Implementing an Interface
|
145
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");
} // end for
} // end main
} // end class
} // end namespace
Output:
Creating document with: Big Document # 0
Creating document with: Little Document # 1
Creating document with: Big Document # 2
Creating document with: Little Document # 3
Creating document with: Big Document # 4
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
IStorable not supported
Compressible not supported
LoggedCompressible not supported
StorableCompressible not supported
Implementing Encrypt
Implementing the Read Method for IStorable
Example 8-3. Collections of Documents (continued)
146
|
Chapter 8: Interfaces
A quick examination of the output shows that we created three big documents and two
little ones; that in fact, three of the documents are able to implement the interfaces and
two are not; and that with the exception of
Encrypt, all are able to implement, just as
we have every right to expect.
Interface Versus Abstract Class
Interfaces are very similar to abstract classes. In fact, you could change the declaration
of
IStorable to be an abstract class:
abstract class Storable
{
Implementing Compress
Implementing LogSavedBytes
Implementing Compress
Implementing LogOriginalSize
Implementing LogSavedBytes
Implementing Compress
Implementing the Read Method for IStorable
Implementing Encrypt
IStorable not supported
Compressible not supported
LoggedCompressible not supported
StorableCompressible not supported
Implementing Encrypt
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
as Operator
Example 8-3 makes use of the as operator to determine whether a document imple-
ments the interfaces required for its encryption. The
as operator casts the left operand
to the type specified by the right operand and returns null if the cast fails.
The
as operator is like two operators rolled into one. In Example 8-3, it’s used first to
check whether
doc implements, for example, the IStorableCompressible interface, and
if it does, it converts
doc to an instance of that type.
Otherwise, it returns
null. It is a common programming practice to then check
whether the result,
isc, is null before using it, as demonstrated in this example.
Example 8-3. Collections of Documents (continued)
Overriding Interface Implementations
|
147
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 List and Storable. But in C#,
you’re stuck; you can’t inherit from the Storable abstract class and 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 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) { }
//
}
Overriding Interface Implementations
An implementing class is free to mark any or all of the methods that implement the
interface as virtual. Derived classes can
override these implementations to achieve
polymorphism. For example, a
Document class might implement the IStorable inter-
face 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 is imple-
mented by
Document. Read( ) is then overridden in a Note type that derives from
Document.
Example 8-4. Overriding an interface implementation
using System;
namespace overridingInterface
{
interface IStorable
{
void Read( );
void Write( );
}
148
|
Chapter 8: Interfaces
// 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( )
Example 8-4. Overriding an interface implementation (continued)
Overriding Interface Implementations
|
149
{
// 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("\n");
// direct call to the methods
theNote.Read( );
theNote.Write( );
Console.WriteLine("\n");
// create a note object
Note note2 = new Note("Second Test");
IStorable isNote2 = note2 as IStorable;
if (isNote2 != null)
{
isNote2.Read( );
isNote2.Write( );
}
Console.WriteLine("\n");
// 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!
Example 8-4. Overriding an interface implementation (continued)
150
|
Chapter 8: Interfaces
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 cer-
tainly 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 out-
put reveals that the
Read( ) method is responded to polymorphically and the Write( )
method is not, just as you 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( );
Explicit Interface Implementation
|
151
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 sec-
ond
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.
However, when methods are called directly on the Note object:
note2.Read( );
note2.Write( );
the output reflects that you’ve called a Note and not an overridden Document:
Overriding the Read method for Note!
Implementing the Write method for Note!
Explicit Interface Implementation
In the implementation shown so far, the implementing class (in this case, Document)
creates a member method with the same signature and return type as the method
detailed in the interface. It is not necessary to explicitly state that this is an imple-
mentation of an interface; the compiler understands this 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. Unfortu-
nately, 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.
152
|
Chapter 8: Interfaces
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 modifier.
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( );
Example 8-5 demonstrates explicit implementation.
Example 8-5. Explicit implementation
using System;
namespace ExplicitImplementation
{
interface IStorable
{
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");
}
Explicit Interface Implementation
|
153
Selectively Exposing Interface Methods
A class designer can take advantage of the fact that when an interface is imple-
mented 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
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
Example 8-5. Explicit implementation (continued)
154
|
Chapter 8: Interfaces
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 implementa-
tion 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() explic-
itly implemented so that you don’t expose
Read( ) as a method of Document.
Note that because explicit interface implementation prevents the use of the
virtual
keyword, a derived class would be forced to reimplement the method. Thus, if Note
derived from Document, it would be forced to reimplement ITalk.Read( ) because the
Document implementation of ITalk.Read( ) couldn’t be virtual.
Member Hiding
It is possible for an interface member to become hidden. For example, suppose you
have an interface
IBase that has a property P:
interface IBase
{
int P { get; set; }
}
Suppose you derive from that interface a new interface, IDerived, which 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( ) { }
}
Explicit Interface Implementation
|
155
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( ) { }
}
156
Chapter 9
CHAPTER 9
Arrays, Indexers, and Collections 9
The .NET Framework provides a rich suite of collection classes. With the advent of
Generics in .NET 2.0, most of these collection classes are now type-safe, making for
a greatly enhanced programming experience. These classes include the
Array, List,
Dictionary, Sorted Dictionary, Queue, and Stack.
The simplest collection is the
Array, the only collection type for which C# provides
built-in support. In this chapter, you will learn to work with single, multidimen-
sional, and jagged arrays. Arrays have built-in indexers, allowing you to request the
nth member of the array. In this chapter, you will also be introduced to creating your
own indexers, a bit of C# syntactic sugar that makes it easier to access class proper-
ties as though the class were indexed like an array.
The .NET Framework provides a number of interfaces, such as
IEnumerable and
ICollection, whose implementation provides you with standard ways to interact
with collections. In this chapter, you will see how to work with the most essential of
these. The chapter concludes with a tour of commonly used .NET collections,
including
List, Dictionary, Queue, and Stack.
In previous versions of C#, the collection objects were not type-safe
(you could, for example, mix strings and integers in a
Dictionary). The
nontype-safe versions of
List (ArrayList), Dictionary, Queue, and
Stack are still available for backward compatibility, but we won’t
cover them in this book because their use is similar to the Generics-
based versions, and because they are obsolete and deprecated.
Arrays
An array is an indexed collection of objects, all of the same type. C# arrays are
somewhat different from arrays in C++ because they are objects. This provides them
with useful methods and properties.