There are times when you may not want to create a new type, but you do want to describe a set of behaviors that any number of types might implement. For example, you might want to describe what it means to be storable (capable of being written to disk) or printable.
Such a description is called an interface. An interface is a contract. When you design an interface, you’re saying “if you want to provide this capability, you must implement these methods, provide these properties and indexers, and support these events.” The implementer of the interface agrees to the contract and implements the required elements.
See Chapter 8 for information about methods and properties, Chapter 17 for information about events, and Chapter 14 for coverage of indexers.
When specifying interfaces, it is easy to get confused about who is responsible for what. There are three concepts to keep clear:
This is the contract. By convention, interface names begin
with a capital I, so your interface might have a name such as
IPrintable
. The IPrintable
interface might require, among
other things, a Print( )
method.
This states that any class that wants to implement IPrintable
must implement a Print( )
method, but it does
not specify how that method works. That is up
to the designer of the implementing class.
This is the class that agrees to the contract described by the
interface. For example, Document
might be a class that implements IPrintable
and thus implements the
Print( )
method in whatever way
the designer of the Document
class thinks is appropriate.
The client calls methods on the implementing class. For example, you might have an Editor
class that has a collection of
IPrintable
objects (every object
in the class is an instance of a type that implements IPrintable
). The
client can expect to be able to call Print( )
on each, and while each may implement the method
differently, each will do so appropriately and without
complaint.
Interfaces are a critical addition to any framework, and they are used extensively throughout .NET. For example, the collection classes (stacks, queues, dictionaries) are defined, in large measure, by the interfaces they implement. (The collection classes are reviewed in detail in Chapter 14.)
In this chapter, you will learn how to create, implement, and use interfaces. You’ll learn how one class can implement multiple interfaces, and you will also learn how to make new interfaces by combining or deriving from existing interfaces. Finally, you will learn how to test whether a class has implemented an interface.
The syntax for defining an interface is very similar to the syntax for defining a class:
[attributes
] [access-modifier
] interfaceinterface-name
[:
base-list
] {interface-body
}
The optional attributes are well beyond the scope of this book (however, see the sidebar, "Attributes“).
Access modifiers (public
,
private
, etc.) work just as they do
with classes. (See Chapter 7
for more about access modifiers.) The interface
keyword is followed by an identifier
(the interface name). It is common (but not required) to begin the name
of your interface with a capital I (IStorable
, ICloneable
, IGetNoKickFromChampagne
, etc.). The optional
base list is discussed later in this chapter.
Attributes are most often used in one of two ways: either for
interacting with legacy COM objects or for creating controls that will
be fully recognized by the Visual Studio development environment. You
can create your own custom attributes , but this is unusual and not covered in this book (for
more on this, see Programming C#, Fourth Edition
[O’Reilly, 2005]). Now suppose you are the author of a Document
class, which specifies that Document
objects can be stored in a database.
You decide to have Document
implement
the IStorable
interface. It isn’t
required that you do so, but by implementing the IStorable
interface, you signal to potential
clients that the Document
class can
be used just like any other IStorable
object. This will, for example, allow your clients to add your Document
objects to an array of Notes
IStorable[] myStorableArray = new IStorable[3];
and to otherwise interact with your Document
in this very general and
well-understood way.
To implement the IStorable
interface, use the same syntax as if the new Document
class were inheriting from IStorable
—a colon (:
) followed by the interface name:
public class Document : IStorable
You can read this as “define a public class named Document
that implements the IStorable
interface.” The compiler
distinguishes whether the colon indicates inheritance or implementation
of an interface by checking to see if IStorable
is defined, and whether it is an
interface or base class.
If you derive from a base class and you also implement one or more interfaces, you use a single colon and separate the base class and the interfaces by commas. The base class must be listed first; the interfaces may be listed in any order.
public MyBigClass : TheBaseClass, IPrintable, IStorable, IClaudius, IAndThou
In this declaration, the new class MyBigClass
derives from TheBaseClass
and implements four
interfaces.
Your definition of the Document
class that implements the IStorable
interface might look like this:
public class Document : IStorable { public void Read( ) {...} public void Write(object obj) {...} // ... }
It is now your responsibility, as the author of the Document
class, to provide a meaningful
implementation of the IStorable
methods. Having designated Document
as implementing IStorable
, you must
implement all the IStorable
methods,
or you will generate an error when you compile. Example 13-1 illustrates
defining and implementing the IStorable
interface.
Example 13-1. Document class implementing IStorable
using System;namespace InterfaceDemo { interface IStorable { void Read( ); void Write( object obj ); int Status { get; set; } } public class Document : IStorable { // store the value for the IStorable required property private int status = 0; public Document( string s ) { Console.WriteLine( "Creating document with: {0}", s ); } #region IStorable public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable" ); } public void Write( object o ) { Console.WriteLine( "Implementing the Write Method for IStorable" ); } public int Status { get { return status; } set { status = value; } } #endregion } class Tester { public void Run( ) { Document doc = new Document( "Test Document" ); doc.Status = -1; doc.Read( ); Console.WriteLine( "Document Status: {0}", doc.Status ); } static void Main( ) { Tester t = new Tester( ); t.Run( ); } } }
The output looks like this:
Creating document with: Test Document Implementing the Read Method for IStorable Document Status: -1
In Example
13-1, the first few lines define an interface, IStorable
, which has two methods (Read( )
and Write( )
) and a property (Status
) of type int
:
interface IStorable { void Read( ); void Write(object obj); int Status { get; set; } }
Notice that the IStorable
method declarations for Read( )
and
Write( )
do not include access
modifiers (public
, protected
, internal
, private
). In fact, providing an access
modifier generates a compile error. Interface methods are implicitly
public because an interface is a contract meant to be used by other
classes. In addition, you must declare these methods to be public
, and not static
, when you implement the
interface.
In the interface declaration, the methods are otherwise defined
just like methods in a class: you indicate the return type (void
), followed by the identifier (Write
), followed by the parameter list
(object obj
), and, of course, you
end all statements with a semicolon.
An interface can also require that the implementing class
provide a property (see Chapter
8 for a discussion of properties). Notice that the declaration
of the Status
property does not
provide an implementation for get( )
and set( )
, but simply
designates that there is a get( )
and a set( )
:
int Status { get; set; }
Once you’ve defined the IStorable
interface, you can define classes
that implement your interface. Keep in mind that you cannot create an
instance of an interface; instead, you instantiate a class that
implements the interface.
You can make a reference to an interface, but you must assign an actual implementing object to that reference:
IStorable myStorable = new Document( );
The class implementing the interface must fulfill the contract
exactly and completely. Thus, your Document
class must provide both a Read( )
and a Write( )
method and the Status
property.
public class Document : IStorable {
This statement defines Document
as a class that defines IStorable
. I also like to separate the
implementation of an interface in a region—this
is a Visual Studio 2005 convenience that allows you to collapse and
expand the code within the region to make reading the code
easier:
#region IStorable //... #endregion
Within the region, you place the code that implements the two
required methods and the required property. Exactly how your Document
class fulfills the requirements of
the interface, however, is entirely up to you.
Although IStorable
dictates
that Document
must have a Status
property, it does not know or care whether Document
stores the actual status as a
member variable or looks it up in a database. Example 13-1 implements the
Status
property by returning (or
setting) the value of a private member variable, status
. Another class that implements
IStorable
could provide the
Status
property in an entirely
different manner (such as by looking it up in a database).