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( );
or you can create an instance of the interface by casting the document to the interface type, and then use that interface to access the methods:
IStorable isDoc = (IStorable) 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.
As stated earlier, you cannot instantiate an interface directly. That is, you cannot 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 = (IStorable) doc;
You can combine these steps by writing:
IStorable isDoc = (IStorable) new Document("Test Document");
In general, it is a better design decision to access the interface
methods through an interface reference. Thus, it is better to use
isDoc.Read( )
, than doc.Read( )
, in the previous example. 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 given interface. For example, suppose you have a
collection of Documents
, some of which can be
stored and others of which cannot. Suppose you add a second
interface, ICompressible
, for those objects that
can compress themselves for quick transmission via email:
interface ICompressible { void Compress( ); void Decompress( ); }
Given a Document
type, you might not know whether
it supports IStorable
or
Icompressible
or both. You
can just cast to the interfaces:
Document doc = new Document("Test Document"); 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
would still compile
because ICompressible
is a valid interface.
However, because of the illegal cast, when the program is run an
exception will be 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, in order 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 is
type
The
is
operator
evaluates true
if the expression (which must be a
reference type) can be safely cast to type
without throwing an exception. 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
using System; interface IStorable { 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 { public Document(string s) { Console.WriteLine( "Creating document with: {0}", s); } // IStorable.Read public void Read( ) { Console.WriteLine( "Implementing the Read Method for IStorable"); } // IStorable.Write public void Write(object o) { Console.WriteLine( "Implementing the Write Method for IStorable"); } // IStorable.Status public int Status { get { return status; } set { status = value; } } private int status = 0; } public class Tester { static void Main( ) { Document doc = new Document("Test Document");// only cast if it is safe
if (doc is IStorable)
{
IStorable isDoc = (IStorable) doc;
isDoc.Read( );
}
// this test will fail
if (doc is ICompressible)
{
ICompressible icDoc = (ICompressible) doc;
icDoc.Compress( );
} } }
Example 8-3 is identical to Example 8-2 except that it adds the declaration for the
ICompressible
interface. Main( )
now determines whether the 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.
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.0IL_002b: castclass ICompressible
IL_0030: stloc.2 IL_0031: ldloc.2 IL_0032: callvirt instance void ICompressible::Compress( ) IL_0037: br.s IL_0043 IL_0039: ldstr "Compressible not supported"
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( ) { Document doc = new Document("Test Document"); 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"); }
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 you will
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# does not 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) {...} // ... }
3.145.87.161