Losing and Regaining Type Information

   


If we assign an object of a derived class (Rectangle) to a variable of type base class (Shape) as in the following:

 Shape myShape = new Rectangle(); 

the Rectangle object looses part of its identity. It is impossible to tell by looking at myShape in the source code whether it references an object of type Rectangle, Circle, or Triangle; we only know it's a Shape.

Even if we happen to know that myShape contains a Rectangle object in a particular place in our code and want to access its Height property (assuming that Rectangle contains a Height property) with the following call:

 myShape.Height = 20.4;     //Invalid 

the compiler returns an error message, because it cannot determine at compile time whether myShape will reference an object of type Rectangle, Triangle, or Circle. If it happens to, for example, be a Circle, havoc would break loose because Circle does not contain a Height property (it would, instead, contain a Radius property). We can only call functions defined in the Shape class, even if its variable contains an object of a subclass with other added functions.

As long as we only need to call the class members of the Rectangle class that also are defined in the Shape class, the information loss and limited access do not pose a problem. For example, in our code in the drawing program case study, we were happy to let the drawing array with the Shape elements hide their real content because the only call we needed to make was DrawYourself, which was channeled through Shape and, via dynamic binding, brought to the DrawYourself method of the right subclass.

However, the story is different if we need to call a function member specific to the Rectangle class. For example, what if we want to calculate the total height of all the Rectangles in our drawing (again assuming that Rectangle contains a Height property)? Our first attempt

 double totalHeight; for(int i = 0; i < drawing.Length; i++) {     totalHeight += drawing[i].Height;     //Invalid } 

fails, because the drawing elements are of type Shape and do not support a call to Height. If we could somehow detect whether an element contains a Rectangle object and then somehow transform it back into a full-blown Rectangle, we could then call its Height property.

Fortunately, C# keeps a close eye on which object (and its type) a variable is referencing at any point in time. (Otherwise, it wouldn't be able to support, for example, the dynamic binding mechanism.) It is possible to access this information programmatically with the is and as operators that are presented in the following sections.

The is Operator

The is operator can test whether a variable (such as myShape) is referencing an object of a specific type (such as Rectangle). The result of this test is one of the bool values true or false. To test whether myShape holds a Rectangle object, we can write the following Boolean expression:

 (myShape is Rectangle) 

which returns true if myShape is referencing a Rectangle object; otherwise, it returns false.

If myShape is, in fact, holding a Rectangle object, we might want to transform myShape into a variable of type Rectangle because this will allow us to use the Height property. To do this, we can use the cast operator as described in the next section.

Casting Objects

The following assignment from earlier

 Car myCar = new FamilyCar(); 

demonstrates our ability to assign an object of class FamilyCar to a variable myCar that is declared to reference objects of type Car. This is possible without further ado because FamilyCar is a descendant of Car, which means that all the class members that Car allows us to call will also be available to call in a FamilyCar. Because Car is higher up in the inheritance hierarchy than FamilyCar, this type of assignment requires what is called an up cast, as shown in Figure 17.4.

Figure 17.4. Down casting and up casting.
graphics/17fig04.gif

If we move in the opposite direction and cast a variable type into a descendant type, it is called down casting. However, down casting is more problematic. For example, we can't just write

 FamilyCar myFamilyCar = myCar;    //Invalid 

because myCar could also contain a SportsCar or a RacingCar, of which neither contain the same class members as can be called through a FamilyCar. However, if we are certain that myCar is, in fact, referencing a FamilyCar object, we can explicitly cast myCar into a FamilyCar object, as shown in the following line:

 FamilyCar myFamilyCar = (FamilyCar) myCar; 

Note

graphics/common.gif

If you cast an object of class A to class B and class A is a descendant of class B, it is called an up cast. An up cast does not need an explicit cast operator.

If class A is an ancestor of class B, it is called a down cast. A down cast requires an explicit cast operator such as (<Type>).


Armed with the is operator to check whether a reference variable is referencing a particular kind of object type and the cast operator to perform down casts, we can solve our Rectangle problem stated earlier and sum up the total height of the Rectangle objects contained in the drawing array. This has been done in Listing 17.6.

Listing 17.6 DownCastingRectangles.cs
01: using System; 02: 03: abstract public class Shape 04: { 05:     public abstract void DrawYourself(); 06: } 07: 08: public class Rectangle : Shape 09: { 10:     private double height; 11: 12:     public Rectangle(double initialHeight) 13:     { 14:         height = initialHeight; 15:     } 16: 17:     public override void DrawYourself() 18:     { 19:         Console.WriteLine("Draw a rectangle"); 20:     } 21: 22:     public double Height 23:     { 24:         get 25:         { 26:             return height; 27:         } 28:         set 29:         { 30:             height = value; 31:         } 32:     } 33: } 34:  35: class Circle : Shape 36: { 37:     public override void DrawYourself() 38:     { 39:         Console.WriteLine("Draw a circle"); 40:     } 41: } 42: 43: class Tester 44: { 45:     private static Shape[] drawing; 46: 47:     public static void Main() 48:     { 49:         Rectangle myRectangle; 50:         double totalHeight = 0; 51:         drawing = new Shape[3]; 52: 53:         drawing[0] = new Rectangle(10.6); 54:         drawing[1] = new Circle(); 55:         drawing[2] = new Rectangle(30.8); 56: 57:         for(int i = 0; i < drawing.Length; i++) 58:         { 59:             if(drawing[i] is Rectangle) 60:             { 61:                 myRectangle = (Rectangle)drawing[i]; 62:                 totalHeight += myRectangle.Height; 63:             } 64:         } 65:         Console.WriteLine("Total height of rectangles: { 0} ", totalHeight); 66:     } 67: } Total height of rectangles: 41.4 

To keep the source code short, the Triangle subclass has not been included, and each of the overriding DrawYourself methods only contain one simple call to the WriteLine method. This does not interfere with how the is operator and the cast operator are demonstrated in the Main method of the Tester class.

The drawing array (lines 53 55) assigned three objects, two of which are of type Rectangle. The i loop counter of the for loop (lines 57 64) iterates through each index of the drawing array. Line 59 utilizes the is operator to ask the question, "Does the drawing element with index i reference a Rectangle object?" Only if this is true will lines 61 and 62 be executed. Consequently, we can confidently perform the down cast in line 61 that lets us call the Height property in line 62 and add the height of myRectangle to totalHeight.

Tip

graphics/bulb.gif

In general, you should only perform up casts (and thereby loose type information) if the lost type information will not be needed again. In most parts of your program, you should not need to use the is and the down cast operators. If you find the is and down cast operators in many places of your program, it is likely because of a design flaw.


Not only is the object type of the drawing element checked by the is operator, it is once again checked by the cast operator in line 61, even though it is needless (this is done automatically for any cast). This is a waste of computer resources. By using the as operator presented in the next section, we can avoid this redundant check.

The as Operator

The as operator is tailor-made to perform the type of down casts demonstrated in Listing 17.6. It combines the is operator object type check, the if statement, and the cast operator into one simple operation, as demonstrated in the following lines that can replace lines 57 64 of Listing 17.6:

 for(int i = 0; i < drawing.Length; i++) {     myRectangle = drawing[i] as Rectangle;     if(myRectangle != null)     {         totalHeight += myRectangle.Height;     } } 

By positioning as between drawing[i] and Rectangle in the third line, we are asking to have the drawing[i] element down cast to class Rectangle. If this is possible (because drawing[i] contains an object of class Rectangle, which is automatically checked by the as operator), the Rectangle object is returned and, in this case, is assigned to the myRectangle variable. On the other hand, if the request fails because drawing[i] does not contain a Rectangle object, the null value is returned from the as operator and assigned to myRectangle. So, before we can call Height on myRectangle, we must check (as in line 4) that it does not hold the value null.


   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net