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 information 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 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; I cover them in Chapter 20.
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 { 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 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. I illustrate this in Example 8-1, in which the Document
class implements the IStorable
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 interfacepublic class Document : IStorable { 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"); } 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 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 (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 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, 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"); }
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 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.
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( ); } // 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"); } // 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 Implementing LogOriginalSize Implementing LogSavedBytes Implementing Compress Implementing the Read Method for IStorable Implementing Encrypt
The problem with the approach we’ve taken so far is that you could well have a collection 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 confusing, 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"); } }
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.
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( ); } // 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"); } 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"); } 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( ); } 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 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
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.
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 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) {...} // ... }
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 is implemented by Document
. Read( )
is then overridden in a Note
type that derives from Document
.
using System; namespace overridingInterface { interface IStorable { 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 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( );
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. 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!
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; 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. 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
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.
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"); } 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
, 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( ) {...} } 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( ) {...} }
3.138.192.92