Sometimes, however, you may not know at compile time whether an object supports a particular interface. For instance, if you have an array 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.
Anytime you see casting, you can question the design of the program. It is common for casting to be the result of poor or lazy design. That being said, sometimes casting is unavoidable, especially when dealing with collections that you did not create. This is one of those situations where experience over time will help you tell good designs from bad.
You could try casting each member blindly to ICompressible
. If the object in question doesn’t implement ICompressible
, an error will be raised. You could then handle that error, using techniques we’ll explain in Chapter 16. That’s a sloppy and ineffective way to do it, though. The is
and as
operators provide a much better way.
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 (expression
is
type
)
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.
The as
operator is similar to is
, but it goes a step further. The as
operator tries to cast the object to the type, and if an exception would be thrown, it instead returns null:
ICompressible myCompressible = myObject as
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 (which could be either Notes
or Document
s) and then, if you want to access either ICompressible
or the ID
, you’ll need to test the Note
to see whether 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 allow you to determine whether an object can be cast to an interface
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Example_13_3_ _ _ _is_and_as { interface IStorable { void Read( ); void Write(object obj); int Status { get; set; } } interface ICompressible { void Compress( ); void Decompress( ); } public class Note : IStorable { private string myString; public Note(string theString) { myString = theString; } public override string ToString( ) { return myString; } #region IStorable public void Read( ) { Console.WriteLine("Executing Note's Read Method for IStorable"); } public void Write(object o) { Console.WriteLine("Executing Note's Write Method for IStorable"); } public int Status { get; set; } #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("Executing Document's Compress Method for ICompressible"); } public void Decompress( ) { Console.WriteLine("Executing Document's Decompress Method for ICompressible"); } #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); } } 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 }
Testing String 0 with IS Executing Note's Read Method for IStorable Executing Document's Compress Method for ICompressible my documentID is 50 Testing String 1 with IS Executing Note's Read Method for IStorable This storable object is not compressible. Testing String 2 with IS Executing Note's Read Method for IStorable Executing Document's Compress Method for ICompressible my documentID is 70 Testing String 0 with AS Executing Document's Compress Method for ICompressible My documentID is 50 Testing String 1 with AS This storable object is not compressible. Not a document. Testing String 2 with AS Executing Document's Compress Method for ICompressible My documentID is 70
The best way to understand this program is to take it apart piece by piece.
Within the namespace, you declare two interfaces, IStorable
and ICompressible
, and then two classes: Note
, which implements IStorable
; and Document
, which derives from Note
(and thus inherits the implementation of IStorable
) and which also implements ICompressible
. Finally, you add the class Tester
to test the program.
Within the Run( )
method of the Tester
class, you create an array of Note
objects, and you add to that array two Document
instances and one Note
instance. You use the counter i
of the for
loop as a control—if i
is even, you create a Document
object; if it’s odd, you create a Note
.
You then iterate through the array, extracting each Note
in turn, and use the is
operator to test first whether the Note
can safely be assigned to an ICompressible
reference:
if (theNote is ICompressible) { ICompressible myCompressible = theNote as ICompressible; myCompressible.Compress( ); } else { Console.WriteLine("This storable object is not compressible."); }
If it can, you cast theNote
to ICompressible
, and call the Compress( )
method.
Then you check whether the Note
can safely be cast to a Document
:
if (theNote is Document) { Document myDoc = theNote as Document; // clean cast myDoc = theNote as Document; Console.WriteLine("my documentID is {0}", myDoc.ID); }
In the case shown, these tests amount to the same thing, but you can imagine that you could have a collection with many types derived from Note
, some of which implement ICompressible
and some of which do not.
You can use the interim variable as we’ve done here:
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, as you do in the second loop:
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. (The second foreach
loop actually generates less intermediate language code, and thus is slightly more efficient.)