There may be times, however, in which you do not know at
compile time whether or not an object supports a particular interface.
For instance, given a List
of
IStorable
objects, you might not know
whether any given object in the collection also implements ICompressible
(some do, some do not). Let’s
set aside the question of whether this is a good design, and move on to
how we solve the problem.
Any time you see casting, you must question the design of the program. It is common for casting to be the result of poor or lazy design. That said, there are times that casting is unavoidable, especially when dealing with nongeneric collections that you did not create.
You could cast each member blindly to ICompressible
, and then catch the exception
that will be thrown for those that are not ICompressible
, but this is ugly, and there are
two better ways to do so: the is
and
the as
operators .
The is
operator lets you query
whether an object implements an interface (or derives from a base
class). The form of the is
operator
is:
if ( myObject is ICompressible )
The is
operator evaluates true
if the expression
(which must be a reference
type, such as an instance of a class) can be safely cast to
type
without throwing an exception.[9]
The as
operator tries to cast
the object to the type, and if an exception would be thrown, it instead
returns null:
ICompressible myCompressible = myObjectas
ICompressible
if ( myCompressible != null )
The is
operator is slightly
less efficient than using as
, so
the as
operator is slightly
preferred over the is
operator,
except when you want to do the test but not actually do the cast (a
rare situation).
Example 13-3
illustrates the use of both the is
and the as
operators by creating two
classes. The Note
class implements
IStorable
. The Document
class derives from Note
(and thus inherits the implementation of
IStorable
) and adds a property
(ID
) along with an implementation of
ICompressible
.
In this example, you’ll create an array of Note
objects and then, if you want to access
either ICompressible
or the ID
, you’ll need to test the Note
to see if it is of the correct type. Both
the is
and the as
operators are demonstrated. The entire
program is documented fully immediately after the source code.
Example 13-3. The is and as operators
using System; namespace InterfaceDemo { interface IStorable { void Read( ); void Write( object obj ); int Status { get; set; } } interface ICompressible { void Compress( ); void Decompress( ); } public class Note : IStorable { private int status = 0; // IStorable private string myString; public Note( string theString ) { myString = theString; } public override string ToString( ) { return myString; } #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 // IStorable } public class Document : Note, ICompressible { private int documentID; public int ID { get { return this.documentID; } } public Document( string docString, int documentID ) : base( docString ) { this.documentID = documentID; } #region ICompressible public void Compress( ) { Console.WriteLine( "Compressing..." ); } public void Decompress( ) { Console.WriteLine( "Decompressing..." ); } #endregion // ICompressible } // end Document class class Tester { public void Run( ) { string testString = "String "; Note[] myNoteArray = new Note[3]; for ( int i = 0; i < 3; i++ ) { string docText = testString + i.ToString( ); if ( i % 2 == 0 ) { Document myDocument = new Document( docText, ( i + 5 ) * 10 ); myNoteArray[i] = myDocument; } else { Note myNote = new Note( docText ); myNoteArray[i] = myNote; } } foreach ( Note theNote in myNoteArray ) { Console.WriteLine( " Testing {0} with IS", theNote ); theNote.Read( ); // all notes can do this if ( theNote is ICompressible ) { ICompressible myCompressible = theNote as ICompressible; myCompressible.Compress( ); } else { Console.WriteLine( "This storable object is not compressible." ); } if ( theNote is Document ) { Document myDoc = theNote as Document; // clean cast myDoc = theNote as Document; Console.WriteLine( "my documentID is {0}", myDoc.ID ); // old fashioned cast! Console.WriteLine( "My documentID is {0}", ( ( Document ) theNote ).ID ); } } foreach ( Note theNote in myNoteArray ) { Console.WriteLine( " Testing {0} with AS", theNote ); ICompressible myCompressible = theNote as ICompressible; if ( myCompressible != null ) { myCompressible.Compress( ); } else { Console.WriteLine( "This storable object is not compressible." ); } // end else Document theDoc = theNote as Document; if ( theDoc != null ) { Console.WriteLine( "My documentID is {0}", ( ( Document ) theNote ).ID ); } else { Console.WriteLine( "Not a document." ); } } } static void Main( ) { Tester t = new Tester( ); t.Run( ); } } // end class Tester } // end Namespace InterfaceDemo
The output looks like this:
Testing String 0 with IS Implementing the Read Method for IStorable Compressing... my documentID is 50 My documentID is 50 Testing String 1 with IS Implementing the Read Method for IStorable This storable object is not compressible. Testing String 2 with IS Implementing the Read Method for IStorable Compressing... my documentID is 70 My documentID is 70 Testing String 0 with AS Compressing... My documentID is 50 Testing String 1 with AS This storable object is not compressible. Not a document. Testing String 2 with AS Compressing... My documentID is 70
The best way to understand this program is to take it apart piece by piece.
Within the namespace, we declare two interfaces, IStorable
and ICompressible
, and then three classes:
Note
, which implements IStorable
; and Document
, which derives from Note
(and thus inherits the implementation of
IStorable
) and which also implements
ICompressible
). Finally, we add the
class Tester
to test the
program.
Within the Run( )
method of the
Tester class, we create an array of Note
objects, and we add to that array two
Document
and one Note
instances (using the expedient that each
time through the for
loop, we check
whether the counter variable i
is
even, and if so, we create a Document
; otherwise, we create a Note
).
We then iterate through the array, extract each Note
in turn, and use the is
operator to test first if the Note
can safely be assigned to an ICompressible
reference and then to check if
the Note
can safely be cast to a
Document
. In the case shown, these
tests amount to the same thing, but you can imagine that we could have a
collection with many types derived from Note
, some of which implement ICompressible
and some of which do not.
We have a choice as to how we cast to a document. The old-fashioned way is to use the C-style cast:
myDoc = (Document) theNote;
The preferred way is to use the as
operator:
myDoc = theNote as Document;
The advantage of the latter is that it will return null (rather than throwing an exception) if the cast fails, and it may be a good idea to get in the habit of using this new form of casting.
In any case, you can use the interim variable:
myDoc = theNote as Document; Console.WriteLine( "my documentID is {0}", myDoc.ID );
Or you can cast and access the property all in one ugly but effective line:
Console.WriteLine( "My documentID is {0}", ( ( Document ) theNote ).ID );
The extra parentheses are required to ensure that the cast is done before the attempt at accessing the property.
The second foreach
loop uses
the as
operator to accomplish the
same work, and the results are identical. (If you bother to look at the
actual IL code, you’ll see that the second foreach
loop actually generates less code, and
thus is slightly more efficient.)
[9] Historical footnote: “It depends on what the meaning of the word ‘is’ is. If the—if he—if ‘is’ means is and never has been, that is not—that is one thing.”—But not in C#.