Creating Polymorphic Types
Because a ListBox is a Windowand aButtonis a Window, you expect to be able to use either of these types in situations that call for a Window. For example, a form might want to keep a collection of all the derived instances of Window it manages (buttons, lists, and so on), so that when the form is opened, it can tell each of its Windows to draw itself. For this operation, the form does not want to know which elements areListBoxes and which areButtons; it just wants to tick through its collection and tell each one to “draw.” In short, the form wants to treat all its Window objects polymorphically.
You implement polymorphism in two steps:
- Create a base class with virtual methods.
- Create derived classes that override the behavior of the base class’s virtual methods.
To create a method in a base class that supports polymorphism, mark the method asvirtual. For example, to indicate that the methodDrawWindow()of class Window in Example 11-1 is polymorphic, add the keywordvirtual to its declaration, as follows:
public virtual void DrawWindow()
Each derived class is free to inherit and use the base class’sDrawWindow()method as is or to implement its own version ofDrawWindow(). If a derived class does override theDrawWindow()method, that overridden version will be invoked for each instance of the derived class. You override the base class virtual method by using the keyword override in the derived class method definition, and then add the modified code for that overridden method.
Example 11-2 shows how to override virtual methods.
Example 11-2. Virtual methods
using System;
public class Window
{
// constructor takes two integers to
// fix location on the console
public Window( int top, int left )
{
this.top = top;
this.left = left;
}
// simulates drawing the window
public virtual void DrawWindow()
{
Console.WriteLine( "Window: drawing Window at {0}, {1}",
top, left );
}
// these members are protected and thus visible
// to derived class methods. We'll examine this
// later in the chapter. (Typically, these would be private
// and wrapped in protected properties, but the current approach
// keeps the example simpler.)
protected int top;
protected int left;
} // end Window
// ListBox derives from Window
public class ListBox : Window
{
// constructor adds a parameter
// and calls the base constructor
public ListBox(
int top,
int left,
string contents ) : base( top, left )
{
listBoxContents = contents;
}
// an overridden version (note keyword) because in the
// derived method we change the behavior
public override void DrawWindow()
{
base.DrawWindow(); // invoke the base method
Console.WriteLine( "Writing string to the listbox: {0}",
listBoxContents );
}
private string listBoxContents; // new member variable
} // end ListBox
public class Button : Window
{
public Button(
int top,
int left ) : base( top, left )
{}
// an overridden version (note keyword) because in the
// derived method we change the behavior
public override void DrawWindow()
{
Console.WriteLine( "Drawing a button at {0}, {1}\n",
top, left );
}
} // end Button
public class Tester
{
static void Main()
{
Window win = new Window( 1, 2 );
ListBox lb = new ListBox( 3, 4, "Stand alone list box" );
Button b = new Button( 5, 6 );
win.DrawWindow();
lb.DrawWindow();
b.DrawWindow();
Window[] winArray = new Window[3];
winArray[0] = new Window( 1, 2 );
winArray[1] = new ListBox( 3, 4, "List box in array" );
winArray[2] = new Button( 5, 6 );
for ( int i = 0; i <>
The output looks like this:
Window: drawing Window at 1, 2
Window: drawing Window at 3, 4
Writing string to the listbox: Stand alone list box
Drawing a button at 5, 6
Window: drawing Window at 1, 2
Window: drawing Window at 3, 4
Writing string to the listbox: List box in array
Drawing a button at 5, 6
In Example 11-2,ListBox derives from Window and implements its own version ofDrawWindow():
public override void DrawWindow()
{
base.DrawWindow(); // invoke the base method
Console.WriteLine ("Writing string to the listbox: {0}",
listBoxContents);
}
The keywordoverridetells the compiler that this class has intentionally overridden howDrawWindow()works. Similarly, you’ll overrideDrawWindow()in another class that derives fromWindow: theButton class.
In the body of the example, you create three objects: aWindow, aListBox, and aButton. Then you callDrawWindow()on each:
Window win = new Window(1,2);
ListBox lb = new ListBox(3,4,"Stand alone list box");
Button b = new Button(5,6);
win.DrawWindow();
lb.DrawWindow();
b.DrawWindow();
This works much as you might expect. The correctDrawWindow()method is called for each. So far, nothing polymorphic has been done (after all, you called theButtonversion ofDrawWindowon aButtonobject). The real magic starts when you create an array ofWindowobjects.
Because aListBoxis a Window, you are free to place aListBoxinto an array ofWindows. Similarly, you can add aButton to a collection ofWindows, because aButtonis a Window.
Window[] winArray = new Window[3];
winArray[0] = new Window(1,2);
winArray[1] = new ListBox(3,4,"List box in array");
winArray[2] = new Button(5,6);
The first line of code declares an array namedwinArray that will hold threeWindowobjects. The next three lines add newWindowobjects to the array. The first adds an object of typeWindow. The second adds an object of typeListBox(which is aWindow becauseListBoxderives fromWindow), and the third adds an object of typeButton, which is also a type ofWindow.
What happens when you callDrawWindow()on each of these objects?
for (int i = 0; i <>
This code usesias a counter variable. It callsDrawWindow()on each element in the array in turn. The valueiis evaluated each time through the loop, and that value is used as an index into the array.
All the compiler knows is that it has threeWindowobjects and that you’ve calledDrawWindow()on each. If you had not markedDrawWindow()asvirtual,Window’s originalDrawWindow()method would be called three times.
However, because you did markDrawWindow()as virtual, and because the derived classes override that method, when you callDrawWindow()on the array, the right thing happens for each object in the array. Specifically, the compiler determines the runtime type of the actual objects (aWindow, aListBox, and aButton) and calls the right method on each. This is the essence of polymorphism.
The runtime type of an object is the actual (derived) type. At compile time, you do not have to decide what kind of objects will be added to your collection, so long as they all derive from the declared type (in this case,Window). At runtime, the actual type is discovered and the right method is called. This allows you to pick the actual type of objects to add to the collection while the program is running.
Note that throughout this example, the overridden methods are marked with the keywordoverride:
public override void DrawWindow()
The compiler now knows to use the overridden method when treating these objects polymorphically. The compiler is responsible for tracking the real type of the object and for handling the late binding, so thatListBox.DrawWindow()is called when the Window reference really points to aListBox object.
In C#, the programmer’s decision to override a virtual method is made explicit with the override keyword. This helps you release new versions of your code; changes to the base class will not break existing code in the derived classes. The requirement to use theoverridekeyword helps prevent that problem. Here’s how: assume for a moment that Company A wrote theWindowbase class in Example 11-2. Suppose also that theListBoxandRadioButtonclasses were written by programmers from Company B using a purchased copy of the Company A Window class as a base. The programmers in Company B have little or no control over the design of the Window class, including future changes that Company A might choose to make. Now suppose that one of the programmers for Company B decides to add aSort()method toListBox: public class ListBox : Window This presents no problems until Company A, the author ofWindow, releases Version 2 of itsWindowclass, and the programmers in Company A also add aSort()method to their public classWindow: public class Window In other object-oriented languages (such as C++), the new virtualSort()method inWindow would now act as a base virtual method for theSort()method inListBox, which is not what the developer ofListBoxintended. C# prevents this confusion. In C#, avirtualfunction is always considered to be the root of virtual dispatch; that is, once C# finds a virtual method, it looks no further up the inheritance hierarchy. If a new virtualSort()function is introduced intoWindow, the runtime behavior of ListBoxis unchanged. WhenListBoxis compiled again, however, the compiler generates a warning: ...\class1.cs(54,24): warning CS0114: 'ListBox.Sort()' hides Never ignore warnings. Treat them as errors until you have satisfied yourself that you understand the warning and that it is not only innocuous but that there is nothing you can do to eliminate the warning. Your goal, (almost) always, is to compile warning-free code. To remove the warning, the programmer must indicate what she intends.* She can mark theListBox Sort()methodnew to indicate that it is not an override of the virtual method in Window: public class ListBox : Window *In standard English, one uses “he” when the pronoun might refer either to a male or a female. Nonetheless, this assumption has such profound cultural implications, especially in the male-dominated programming profession, that I will use the term “she” for the unknown programmer from time to time. I apologize if this causes you to falter a bit when reading; consider it an opportunity to reflect on the linguistic implications of a patriarchal society. This action removes the warning. If, on the other hand, the programmer does want to override the method in Window, she need only use theoverride keyword to make that intention explicit: public class ListBox : Window To avoid this warning, it might be tempting to add thenewkeyword to all your virtual methods. This is a bad idea. Whennewappears in the code, it ought to document the versioning of code. It points a potential client to the base class to see what it is that you are intentionally not overriding. Usingnewscattershot undermines this documentation and reduces the utility of a warning that exists to help identify a real issue. If the programmer now creates any new classes that derive fromListBox, those derived classes will inherit theSort()method fromListBox, not from the baseWindowclass. Each type of Window has a different shape and appearance. Drop-down listboxes look very different from buttons. Clearly, every subclass of Window should implement its ownDrawWindow()method—but so far, nothing in the Window class enforces that they must do so. To require subclasses to implement a method of their base, you need to designate that method as abstract. An abstract method has no implementation. It creates a method name and signature that must be implemented in all derived classes. Furthermore, making at least one method of any class abstract has the side effect of making the class abstract. Abstract classes establish a base for derived classes, but it is not legal to instantiate an object of an abstract class. Once you declare a method to be abstract, you prohibit the creation of any instances of that class. Thus, if you were to designateDrawWindow()as an abstract method in theWindowclass, theWindow class itself would become abstract. Then you could derive fromWindow, but you could not create anyWindowinstances. If theWindowclass is an abstraction, there is no such thing as a simpleWindowobject, only objects derived fromWindow. MakingWindow.DrawWindow()abstract means that each class derived fromWindow would have to implement its ownDrawWindow()method. If the derived class failed to implement the abstract method, that derived class would also be abstract, and again no instances would be possible. The Idea Behind Abstraction Abstract classes should not just be an implementation trick; they should represent the idea of an abstraction that establishes a “contract” for all derived classes. In other words, abstract classes mandate the public methods of the classes that will implement the abstraction. The idea of an abstract Window class ought to lay out the common characteristics and behaviors of all windows, even though you never intend to instantiate the abstraction Window itself. The idea of an abstract class is implied in the word “abstract.” It serves to implement the abstraction “Window” that will be manifest in the various concrete instances of Window, such as browser window, frame, button, listbox, drop-down, and so forth. The abstract class establishes what a Window is, even though we never intend to create a “Window” per se. An alternative to usingabstractis to define an interface, as described in Chapter 13. Designating a method as abstract is accomplished by placing theabstractkeyword at the beginning of the method definition: abstract public void DrawWindow(); (Because the method can have no implementation, there are no braces, only a semicolon.) If one or more methods are abstract, the class definition must also be markedabstract, as in the following: abstract public class Window Example 11-3 illustrates the creation of an abstract Window class and an abstractDrawWindow()method. Example 11-3. Abstract methods using System; public abstract class Window // simulates drawing the window // ListBox derives from Window listBoxContents = contents; // an overridden version implementing the public class Button : Window // implement the abstract method public class Tester for ( int i = 0; i < 3; i++ ) The output looks like this: Writing string to the listbox: First List Box In Example 11-3, theWindowclass has been declared abstract and therefore cannot be instantiated. If you replace the first array member: winArray[0] = new ListBox(1,2,"First List Box"); with this code: winArray[0] = new Window(1,2); the program generates the following error at compile time: Cannot create an instance of the abstract class or interface 'Window' You can instantiate theListBoxandButtonobjects because these classes override the abstract method, thus making the classes concrete (that is, not abstract). Often an abstract class will include non-abstract methods. Typically, these will be marked virtual, providing the programmer who derives from your abstract class the choice of using the implementation provided in the abstract class, or overriding it. Once again, however, all abstract methods must, eventually, be overridden in order to make an instance of the (derived) class. Sealed Classes The opposite side of the design coin from abstract is sealed. In contrast to an abstract class, which is intended to be derived from and to provide a template for its subclasses to follow, a sealed class does not allow classes to derive from it at all. The sealed keyword placed before the class declaration precludes derivation. Classes are most often marked sealed to prevent accidental inheritance. If you changed the declaration ofWindowin Example 11-3 fromabstracttosealed(eliminating theabstractkeyword from theDrawWindow()declaration as well), the program fails to compile. If you try to build this project, the compiler returns the following error message: 'ListBox' cannot inherit from sealed type 'Window' among many other complaints (such as that you cannot create a new protected member in a sealed class). Microsoft recommends using sealed when you know that you won’t need to create derived classes, and also when your class consists of nothing but static methods and properties. All C# classes, of any type, ultimately derive from a single class: Object.Objectis the base class for all other classes. A base class is the immediate “parent” of a derived class. A derived class can be the base to further derived classes, creating an inheritance tree or hierarchy. A root class is the topmost class in an inheritance hierarchy. In C#, the root class isObject. The nomenclature is a bit confusing until you imagine an upside-down tree, with the root on top and the derived classes below. Thus, the base class is considered to be “above” the derived class. Object provides a number of methods that subclasses can override. These includeEquals(), which determines if two objects are the same, andToString(), which returns a string to represent the current object. Specifically,ToString()returns a string with the name of the class to which the object belongs. Table 11-1 summarizes the methods ofObject. Table 11-1. The Object class In Example 11-4, theDog class overrides theToString()method inherited fromObject, to return the weight of theDog. Example 11-4. Overriding ToString using System; public class Dog // constructor public class Tester Dog milo = new Dog( 62 ); Output: Some classes (such asConsole) have methods that expect a string (such asWriteLine()). These methods will call theToString()method on your class if you’ve overridden the inheritedToString()method fromObject. This lets you pass aDog toConsole.WriteLine, and the correct information will display. This example also takes advantage of the startling fact that intrinsic types (int,long, etc.) can also be treated as if they derive fromObject, and thus you can callToString()on anintvariable! CallingToString()on an intrinsic type returns a string representation of the variable’s value. The documentation forObject.ToString()reveals its signature: public virtual string ToString(); It is a public virtual method that returns a string and takes no parameters. All the built-in types, such asint, derive fromObjectand so can invokeObject’s methods. TheConsole class’sWrite()andWriteLine()methods callToString()for you on objects that you pass in for display. Thus, by overridingToString()in theDogclass, you did not have to pass inmilo. ToString()but rather could just pass inmilo! If you comment out the overridden function, the base method will be invoked. The base class default behavior is to return a string with the name of the class itself. Thus, the output would be changed to the meaningless: My dog Milo weighs Dog pounds Classes do not need to declare explicitly that they derive fromObject; the inheritance is implicit. Boxing and unboxing are the processes that enable value types (such as, integers) to be treated as reference types (objects). The value is “boxed” inside an Object and subsequently “unboxed” back to a value type. It is this process that allowed you to call theToString()method on the integer in Example 11-4. Boxing Is Implicit Boxing is an implicit conversion of a value type to the type Object. Boxing a value allocates an instance ofObjectand copies the value into the new object instance, as shown in Figure 11-4. Boxing is implicit when you provide a value type where a reference is expected. The runtime notices that you’ve provided a value type and silently boxes it within an object. You can, of course, first cast the value type to a reference type, as in the following: int myIntegerValue = 5; This is not necessary, however, as the compiler boxes the value for you silently and with no action on your part: int myIntegerValue = 5; Unboxing Must Be Explicit To return the boxed object back to a value type, you must explicitly unbox it. For the unboxing to succeed, the object being unboxed must really be of the type you indicate when you unbox it. You should accomplish unboxing in two steps: Example 11-5 illustrates boxing and unboxing. Example 11-5. Boxing and unboxing using System; //Boxing // unboxing (must be explicit) Output: Figure 11-5 illustrates unboxing. Example 11-5 creates an integermyIntegerVariableand implicitly boxes it when it is assigned to the objectmyObjectVariable; then, to exercise the newly boxed object, its value is displayed by callingToString(). The object is then explicitly unboxed and assigned to a new integer variable,anotherIntegerVariable, whose value is displayed to show that the value has been preserved. Avoiding Boxing with Generics The most common place that value types were boxed in C# 1.x was in collections that expected Objects. Now that C# supports generics, collections that hold integers need not box and unbox them, and that can increase performance when you have a very large collection. Quiz Question 11-1. What is the relationship between specialization and generalization? Question 11-2. How is specialization implemented in C#? Question 11-3. What is the syntax for inheritance in C#? Question 11-4. How do you implement polymorphism? Question 11-5. What are the two meanings of the keywordnew? Question 11-6. How do you call a base class constructor from a derived class? Question 11-7. What is the difference between public, protected, and private? Question 11-8. What is an abstract method? Question 11-9. What is a sealed class? Question 11-10. What is the base class of Int32? Question 11-11. What is the base class of any class you create if you do not otherwise indicate a base class? Question 11-12. What is boxing? Question 11-13. What is unboxing? Exercises Exercise 11-1. Create a base class, Telephone, and derive a class ElectronicPhonefrom it. InTelephone, create a protected string memberphonetype, and a public methodRing()that outputs a text message like this: “Ringing the Exercise 11-2. Extend Exercise 11-1 to illustrate a polymorphic method. Have the derived class override theRing()method to display a different message. Exercise 11-3. Change theTelephoneclass to abstract, and makeRing()an abstract method. Derive two new classes fromTelephone:DigitalPhoneandTalkingPhone. Each derived class should set thephonetype, and override theRing()method.Versioning with new and override
{
public virtual void Sort() {...}
}
{
// ...
public virtual void Sort() {...}
}
inherited member 'Window.Sort()'.
To make the current member override that implementation,
add the override keyword. Otherwise add the new keyword.
{
public new virtual void Sort() {...}
{
public override void Sort() {...} Abstract Classes
{
// constructor takes two integers to
// fix location on the console
public Window( int top, int left )
{
this.top = top;
this.left = left;
}
// notice: no implementation
public abstract void DrawWindow();
protected int top;
protected int left;
} // end class Window
public class ListBox : Window
{
// constructor adds a parameter
public ListBox(
int top,
int left,
string contents ) : base( top, left ) // call base constructor
{
}
// abstract method
public override void DrawWindow()
{
Console.WriteLine( "Writing string to the listbox: {0}",
listBoxContents );
}
private string listBoxContents; // new member variable
} // end class ListBox
{
public Button(
int top,
int left ) : base( top, left ) { }
public override void DrawWindow()
{
Console.WriteLine( "Drawing a button at {0}, {1}\n",
top, left );
}
} // end class Button
{
static void Main()
{
Window[] winArray = new Window[3];
winArray[0] = new ListBox( 1, 2, "First List Box" );
winArray[1] = new ListBox( 3, 4, "Second List Box" );
winArray[2] = new Button( 5, 6 );
{
winArray[i].DrawWindow();
} // end for loop
} // end main
} // end class Tester
Writing string to the listbox: Second List Box
Drawing a button at 5, 6 The Root of All Classes: Object
Method What it does Equals() Evaluates whether two objects are equivalent GetHashCode() Allows objects to provide their own hash function for use in collections (see Chapter 14) GetType() Provides access to the Type object ToString() Provides a string representation of the object Finalize() Cleans up nonmemory resources; implemented by a destructor (finalizer)
{
private int weight;
public Dog( int weight )
{
this.weight = weight;
}
// override Object.ToString
public override string ToString()
{
return weight.ToString();
}
}
{
static void Main()
{
int i = 5;
Console.WriteLine( "The value of i is: {0}", i.ToString() );
Console.WriteLine( "My dog Milo weighs {0} pounds", milo);
}
}
The value of i is: 5
My dog Milo weighs 62 pounds Boxing and Unboxing Types
Figure 11-4. Boxing value types
object myObject = myIntegerValue; // cast to an object
myObject.ToString();
myIntegerValue.ToString(); // myIntegerValue is boxed
public class UnboxingTest
{
public static void Main()
{
int myIntegerVariable = 123;
object myObjectVariable = myIntegerVariable;
Console.WriteLine( "myObjectVariable: {0}",
myObjectVariable.ToString() );
int anotherIntegerVariable = (int)myObjectVariable;
Console.WriteLine( "anotherIntegerVariable: {0}",
anotherIntegerVariable );
}
}
myObjectVariable: 123 anotherIntegerVariable: 123
Figure 11-5. Unboxing Summary
No comments:
Post a Comment