Interfaces
As mentioned earlier, by deriving from an
interface, a class is declaring that it implements certain
functions. Because not all object-oriented languages support
interfaces, this section examines C#’s implementation of interfaces
in detail.
|
|
Tip |
Developers familiar with COM should be aware
that, although conceptually C# interfaces are similar to COM
interfaces, they are not the same thing. The underlying
architecture is different. For example, C# interfaces are not
derived from IUnknown. A C# interface
provides a contract stated in terms of .NET functions. Unlike a COM
interface, a C# interface does not represent any kind of binary
standard.
|
This section illustrates interfaces by presenting
the complete definition of one of the interfaces that has been
predefined by Microsoft, System.IDisposable. IDisposable contains one method, Dispose(), which is intended to be implemented by
classes to clean up code:
This code shows that declaring an interface works
syntactically in pretty much the same way as declaring an abstract
class. You should be aware, however, that it is not permitted to
supply implementations of any of the members of an interface. In
general, an interface can only contain declarations of methods,
properties, indexers, and events.
You can never instantiate an interface; it only
contains the signatures of its members. An interface has neither
constructors (how can you construct something that you can’t
instantiate?) nor fields (because that would imply some internal
implementation). An interface definition is also not allowed to
contain operator overloads, although that’s not because there is
any problem in principle with declaring them - there isn’t; it is
because interfaces are usually intended to be public contracts, and
having operator over-loads would cause some incompatibility
problems with other .NET languages, such as Visual Basic .NET,
which do not support operator overloading.
It is also not permitted to declare modifiers on
the members in an interface definition. Interface members are
always implicitly public, and cannot be
declared as virtual or static. That’s up to implementing classes to decide.
It is therefore fine for implementing classes to declare access
modifiers, as is done in the example in this section.
Take for example IDisposable. If a class wants to declare publicly
that it implements the Dispose() method,
it must implement IDisposable - which in
C# terms means that the class derives from IDisposable.
In this example, if SomeClass derives from IDisposable but doesn’t contain a Dispose() implementation with the exact same
signature as defined in IDisposable, you
get a compilation error because the class would be breaking its
agreed-on contract to implement IDisposable. Of course, there’s no problem for the
compiler about a class having a Dispose() method but not deriving from IDisposable. The problem, then, would be that other
code would have no way of recognizing that SomeClass has agreed to support the IDisposable features.
|
|
Tip |
IDisposable is a
relatively simple interface because it defines only one method.
Most interfaces will contain more members.
|
Another good example of an interface is provided by
the foreach loop in C#. In principle,
the foreach loop works internally by
querying the object to find out whether it implements an interface
called System.Collections.IEnumerable.
If it does, the C# compiler will inject IL code, which uses the
methods on this interface to iterate through the members of the
collection. If it doesn’t, foreach will
raise an exception. The IEnumerable
interface is examined in more detail in Chapter 10, “Collections.” It’s worth pointing out that both
IEnumerable and IDisposable are somewhat special interfaces to the
extent that they are actually recognized by the C# compiler, which
takes account of these interfaces in the code that it generates.
Obviously, any interfaces that you define yourself won’t be so
privileged!
Defining and Implementing Interfaces
This section illustrates how to define and
use interfaces through developing a short program that follows the
interface inheritance paradigm. The example is based on bank
accounts. Assume that you are writing code that will ultimately
allow computerized transfers between bank accounts. And assume for
this example that there are many companies that may implement bank
accounts, but they have all mutually agreed that any classes that
represent bank accounts will implement an interface, IBankAccount, which exposes methods to deposit or
withdraw money, and a property to return the balance. It is this
interface that will allow outside code to recognize the various
bank account classes implemented by different bank accounts.
Although the aim is to allow the bank accounts to talk to each
other to allow transfers of funds between accounts, we won’t
introduce that feature just yet.
To keep things simple, you will keep all the code
for the example in the same source file. Of course, if something
like the example were used in real life, you could surmise that the
different bank account classes would not only be compiled to
different assemblies but would be hosted on different machines
owned by the different banks. (How .NET assemblies hosted on
different machines can communicate is explored in Chapter
37, “.NET Remoting.”) That’s all much too complicated for
our purposes here. However, to maintain some attempt at realism,
you will define different namespaces for the different
companies.
To begin, you need to define the IBank interface:
Notice the name of the interface, IBankAccount. It’s a convention that an interface
name traditionally starts with the letter I, so that you know that
it’s an interface.
|
|
Tip |
Chapter 2, “C#
Basics,” pointed out that, in most cases, .NET usage guidelines
discourage the so-called Hungarian notation in which names are
preceded by a letter that indicates the type of object being
defined. Interfaces are one of the few exceptions in which
Hungarian notation is recommended.
|
The idea is that you can now write classes that
represent bank accounts. These classes don’t have to be related to
each other in any way; they can be completely different classes.
They will, however, all declare that they represent bank accounts
by the mere fact that they implement the IBankAccount interface.
Let’s start off with the first class, a saver
account run by the Royal Bank of Venus:
It should be pretty obvious what the implementation
of this class does. You maintain a private field, balance, and adjust this amount when money is
deposited or withdrawn. You display an error message if an attempt
to withdraw money fails because there is insufficient money in the
account. Notice also that, because we want to keep the code as
simple as possible, you are not implementing extra properties, such
as the account holder’s name! In real life that would be pretty
essential information, but for this example it’s unnecessarily
complicated.
The only really interesting line in this code is
the class declaration:
You’ve declared that SaverAccount is derived from one interface,
IBankAccount, and you have not
explicitly indicated any other base classes (which of course means
that SaverAccount is derived directly
from System.Object). By the way,
derivation from interfaces acts completely independently from
derivation from classes.
Being derived from IBankAccount means that SaverAccount gets all the members of IBankAccount. But because an interface doesn’t
actually implement any of its methods, SaverAccount must provide its own implementations of
all of them. If any implementations are missing, you can rest
assured that the compiler will complain. Recall also that the
interface just indicates the presence of its members. It’s up to
the class to decide if it wants any of them to be virtual or abstract
(though abstract functions are of course
only allowed if the class itself is abstract). For this particular example, you don’t
have any reason to make any of the interface functions virtual.
To illustrate how different classes can implement
the same interface, assume that the Planetary Bank of Jupiter also
implements a class to represent one of its bank accounts - a Gold
Account:
We won’t present details of the GoldAccount class here; in the sample code, it’s
basically identical to the implementation of SaverAccount. We stress that GoldAccount has no connection with VenusAccount, other than that they happen to
implement the same interface.
Now that you have your classes, you can test them
out. You first need a couple of using
statements:
Now you need a Main()
method:
This code (which if you download the sample, you
can find in the file BankAccounts.cs)
produces this output:
The main point to notice about this code is the way
that you have declared both your reference variables as
IBankAccount references. This means that
they can point to any instance of any class that implements this
interface. It does, however, mean that you can only call methods
that are part of this interface through these references - if you
want to call any methods implemented by a class that are not part
of the interface, you need to cast the reference to the appropriate
type. In the example code, you were able to call ToString() (not implemented by IBankAccount) without any explicit cast, purely
because ToString() is a System.Object method, so the C# compiler knows that
it will be supported by any class (put differently, the cast from
any interface to System.Object is
implicit). Chapter 6, “Operators and Casts,”
covers the syntax for how to perform casts.
Interface references can in all respects be treated
like class references - but the power of an interface reference is
that it can refer to any class that implements that interface. For
example, this allows you to form arrays of interfaces, where each
element of the array is a different class:
This causes a compilation error similar to
this:
Derived Interfaces
It’s possible for interfaces to inherit from
each other in the same way that classes do. This concept is
illustrated by defining a new interface, ITransferBankAccount, which has the same features as
IBankAccount but also defines a method to transfer money directly
to a different account:
Because ITransferBankAccount is derived from IBankAccount, it gets all the members of
IBankAccount as well as its own. That
means that any class that implements (derives from) ITransferBankAccount must implement all the methods
of IBankAccount, as well as the new
TransferTo() method defined in
ITransferBankAccount. Failure to
implement all of these methods will result in a compilation
error.
Note that TransferTo()
method uses an IBankAccount interface
reference for the destination account. This illustrates the
usefulness of interfaces: when implementing and then invoking this
method, you don’t need to know anything about what type of object
you are transferring money to - all you need to know is that this
object implements IBankAccount.
To illustrate ITransferBankAccount, assume that the Planetary Bank
of Jupiter also offers a current account. Most of the
implementation of the CurrentAccount
class is identical to the implementations of SaverAccount and GoldAccount (again, this is just in order to keep
this example simple - that won’t normally be the case), so in the
following code just the differences are highlighted:
The class can be demonstrated with this code:
This code (CurrentAccount.cs) produces the following output,
which, as you can verify, shows that the correct amounts have been
transferred: