Baldwin explains the use of the Object class as a completely generic type for storing references to objects of subclass types, and explains how that results in a very useful form of runtime polymorphism.
Published: January 26, 2010
Validated with Amaya
By Richard G. Baldwin
XNA Programming Notes # 0116
|
This tutorial lesson is part of a continuing series dedicated to programming with the XNA Game Studio. I am writing this series of lessons primarily for the benefit of students enrolled in an introductory XNA game programming course that I teach. However, everyone is welcome to study and benefit from the lessons.
An earlier lesson titled Getting Started (see Resources) provided information on how to get started programming with Microsoft's XNA Game Studio. (See Baldwin's XNA programming website in Resources.)
The three main characteristics of an object-oriented program
Object-oriented programs exhibit three main characteristics:
I have explained encapsulation, inheritance, compile-time polymorphism, and runtime polymorphism using method overriding and class inheritance in earlier lessons. This lesson will explain the importance of the Object class in polymorphic behavior. I will defer an explanation of polymorphism using interface inheritance until a future lesson.
I recommend that you open another copy of this document in a separate browser window and use the following links to easily find and view the figures and listings while you are reading about them.
I recommend that you also study the other lessons in my extensive collection of online programming tutorials. You will find a consolidated index at www.DickBaldwin.com.
The generic type Object
In this lesson, I will explain the use of the Object class as a completely generic type for storing references to objects of subclass types, and will explain how that results in a very useful form of runtime polymorphism.
I will briefly discuss the default versions of the methods defined in the Object class, and will explain that in many cases, those default versions are meant to be overridden.
References of type Object
The Object type is a completely generic type that can be used to store a reference to any object that can be instantiated in C#.
Methods in the Object class
The Object class defines the following eight methods, which are inherited by every other class:
Because these eight methods are inherited by every other class, they are always available for you to use in your code. (Possibly the most frequently used of these methods is the ToString method.)
|
Two of the methods in this list are defined in overloaded versions (same name, different formal argument lists).
Every class inherits these methods
Because every class is either a direct or indirect subclass of Object, every class in C#, (including new classes that you define), inherit these methods.
To be overridden...
Some of these methods are intended to be overridden for various purposes. This includes Equals, Finalize, GetHashCode, and ToString, which are all declared virtual.
However, some of them, such as GetType, are not declared virtual and therefore are intended to be used directly without overriding.
Calling methods of the Object class
You can store a reference to any object in a reference variable of type Object.
Because every new class inherits these methods, you can call any of these methods on any reference to any object stored in a reference variable of type Object or in a reference variable of any other type.
And the behavior will be...
If the class from which the object was instantiated inherits or defines an overridden version of one of the methods in the above list, calling that method on the reference will cause the overridden version to be executed.
Otherwise, calling that method on the reference will cause the default version defined in the Object class to be executed.
A sample program
The behavior described above is illustrated in the sample program named Polymorph04, which you can view in its entirety in Listing 7 near the end of this lesson.
For purposes of illustration, this program deals specifically with the method named ToString from the above list, but could deal with the other virtual methods in the list as well.
The class named A
Listing 1 defines a class named A, which explicitly extends the class named Object.
(Recall that classes extend Object by default. It is not necessary to explicitly show that a class extends Object. I showed that here simply to remind you that all classes in C# are rooted in the class named Object.)
Listing 1. Definition of class A. using System; class A : Object { //This class is empty }//end class A |
Does not override the ToString method
The most important thing to note about the class named A is that it does not override any of the methods that it inherits from the class named Object.
Therefore, it inherits the default version of the method named ToString from the class named Object.
(We will see an example of the behavior of the default version of that method shortly.)
The class named B
Listing 2 defines the class named B. This class extends the class named A.
Listing 2. Definition of class B. class B : A { public override String ToString() { return "ToString in class B"; }//end overridden ToString() }//end class B |
Overrides the ToString method
Of particular interest, (for purposes of this lesson), is the fact that the class named B overrides the inherited ToString method.
(It inherits the default version of the ToString method, because its superclass named A, which extends Object, does not override the ToString method.)
Purpose of the ToString method
The purpose of the ToString method is to return a reference to an object of the class String that represents an object instantiated from a class that overrides the method.
Here is part of what Microsoft has to say about the ToString method:
"This method returns a human-readable string that is culture-sensitive. For example, for an instance of the Double class whose value is zero, the implementation of Double.ToString might return "0.00" or "0,00" depending on the current UI culture."
Can be overridden
The ToString method can be overridden in a subclass to return values that are meaningful for that type. Again, according to Microsoft:
"For example, the base data types, such as Int32, implement ToString so that it returns the string form of the value that the object represents."
Behavior of the default and overridden versions
The default implementation of the ToString method, as defined in the Object class, returns the fully qualified name of the type of the object.
I didn't override the ToString method in the class named A, but I did override it in the class named B, which is a subclass of A.
The behavior of my overridden version of the method in the class named B returns a reference to a String object, containing text that indicates that the overridden version of the method in the class named B has been executed.
Will be useful later
The reference to the String object returned by the overridden version of the method will prove useful later when we need to determine which version of the method is actually executed.
The class named C
Listing 3 shows the definition of a class named C, which extends the class named B, and overrides the method named ToString again.
(An inherited virtual method can be overridden by every class that inherits it, resulting in potentially many different overridden versions of a given method in a class hierarchy.)
Listing 3. Definition of class C. class C : B { public override String ToString() { return "ToString in class C"; }//end overridden ToString() }//end class B |
Behavior of overridden version
The behavior of this overridden version of the method is similar to, but different from the overridden version in the class B.
In this case, the method returns a reference to a String object that can be used to confirm that this overridden version of the method has been executed.
The driver class
Listing 4 shows the beginning of the driver class named Polymorph04.
Listing 4. Beginning of the driver class. public class Polymorph04 { public static void Main() { Object varA = new A(); String v1 = varA.ToString(); Console.WriteLine(v1); |
A new object of the class A
The Main method of the driver class begins by instantiating a new object of the class A, and saving the object's reference in a reference variable of type Object, named varA.
(It is very important to note that even though the object was instantiated from the class named A, the reference was saved as type Object, not type A.)
Call ToString method on the reference
Then the code calls the ToString method on the reference variable named varA, saving the returned reference to the String object in a reference variable of type String named v1.
Display the returned String
Finally, that reference is passed to the WriteLine method, causing the String returned by the ToString method to be displayed on the computer screen. This causes the text shown in Figure 1 to be displayed:
Figure 1. Text output for ToString method and class A. |
---|
A |
Default ToString behavior
What you are seeing here is the String produced by the default version of the ToString method, as defined by the class named Object. The default implementation of the ToString method returns the fully qualified name of the type of the object. In this case, the object was instantiated from the class named A (so the type of the object is A).
Class A does not override ToString
Recall that our new class named A does not override the ToString method. Therefore, when the ToString method is called on a reference to an object of the class A, the default version of the method is executed, producing the output shown in Figure 1.
A new object of the class B
Now consider the code shown in Listing 5, which instantiates a new object of the class named B, and stores the object's reference in a reference variable of type Object.
Listing 5. A new object of the class B. Object varB = new B(); String v2 = varB.ToString(); Console.WriteLine(v2); |
Call ToString and display the result
The code in Listing 5 calls the ToString method on the reference of type Object, saving the returned reference in the reference variable named v2. (Recall that the ToString method is overridden in the class named B.)
As before, the reference is passed to the WriteLine method, which causes the text shown in Figure 2 to be displayed on the computer screen.
Figure 2. Text output for overridden ToString method and class B. |
---|
ToString in class B |
Do you recognize this?
You should recognize this as the text that was encapsulated in the String object returned by the overridden version of the ToString method defined in the class named B. (See Listing 2.)
Overridden version of ToString was executed
This verifies that even though the reference to the object of the class B was stored in a reference variable of type Object, the overridden version of the ToString method in the class named B was executed (instead of the default version defined in the class named Object). This is runtime polymorphic behavior, as described in a previous lesson.
Once again, what is the rule?
The selection of a method for execution is based on the actual type of object whose reference is stored in a reference variable, and not on the type of the reference variable on which the method is called.
An object of the class C
Finally, the code in Listing 6
Listing 6. An object of the class C. Object varC = new C(); String v3 = varC.ToString(); Console.WriteLine(v3); //Pause until user presses any key. Console.ReadKey(); }//end Main }//end class Polymorph04 |
What will the output look like?
By now, you should know what to expect in the way of text appearing on the computer screen. The code in Listing 6 causes the text shown in Figure 3 to be displayed.
Figure 3. Text output for overridden ToString method and class C. |
---|
ToString in class C |
Overridden version of ToString was called
This confirms what you should already have known by now. In particular, even though the reference to the object of the class C was stored in a reference variable of type Object, the overridden version of the ToString method defined in the class named C was executed. Again, this is runtime polymorphic behavior based on class inheritance and method overriding.
No downcasting was required
It is also very important to note that no downcasting was required in order to call the ToString method in any of the cases shown above.
Because a default version of the ToString method is defined in the Object class, that method can be called without a requirement for downcasting on a reference to any object stored in a variable of type Object. This holds true for any of the methods defined in the class named Object.
A generic array object
Therefore, if we create an array object designed to store references of type Object, we can store (potentially mixed) references to any type of object in that array. We can later call any of the methods defined in the Object class on any of the references stored in the array.
The result will be the execution of the overridden version of the method as defined in the class from which the object was instantiated, or the version inherited into that class if the method is not overridden in that class. Current jargon would say that the behavior of the method is appropriate for the type of object on which it is called.
Collections
The C# library provides a number of generic data structure classes, such as Stack, and Queue, which store and retrieve references to objects as type Object. These classes can be used to create objects that can store and retrieve objects of any class.
I encourage you to copy the code from Listing 7. Use that code to create a C# console project. Compile and run the project. Experiment with the code, making changes, and observing the results of your changes. Make certain that you can explain why your changes behave as they do.
Polymorphism manifests itself in C# in the form of multiple methods having the same name.
From a practical programming viewpoint, polymorphism manifests itself in three distinct forms in C#:
In this lesson, I have continued my discussion of the implementation of polymorphism using method overriding through class inheritance, and have concentrated on a special case in that category.
More specifically, I have discussed the use of the Object class as a completely generic type for storing references to objects of subclass types, and have explained how that results in a very useful form of runtime polymorphism.
I briefly mentioned the default version of the methods defined in the Object class, and explained that in many cases, those default versions are meant to be overridden.
I provided a sample program that illustrates the overriding of the ToString method, which is one of the methods defined in the Object class.
A complete listing of the program discussed in this lesson is provided in Listing 7.Listing 7. Project Polymorph04. /*Project Polymorph04 Copyright 2009, R.G.Baldwin This program illustrates polymorphic behavior and the Object class Program output is: A ToString in class B ToString in class C *********************************************************/ using System; class A : Object { //This class is empty }//end class A //======================================================// class B : A { public override String ToString() { return "ToString in class B"; }//end overridden ToString() }//end class B //======================================================// class C : B { public override String ToString() { return "ToString in class C"; }//end overridden ToString() }//end class B //======================================================// public class Polymorph04 { public static void Main() { Object varA = new A(); String v1 = varA.ToString(); Console.WriteLine(v1); Object varB = new B(); String v2 = varB.ToString(); Console.WriteLine(v2); Object varC = new C(); String v3 = varC.ToString(); Console.WriteLine(v3); //Pause until user presses any key. Console.ReadKey(); }//end Main }//end class Polymorph04 |
Copyright 2009, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is object-oriented programming using Java and other OOP languages.
Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which have gained a worldwide following among experienced and aspiring programmers.
In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor's degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-