Learning C# and OOP, Polymorphism and the Object Class

Baldwin discusses 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:  November 5, 2002
By Richard G. Baldwin

C# Programming Notes # 124


Preface

This lesson is one of a series of lessons designed to teach you about the essence of Object-Oriented Programming (OOP) using C#.

The first lesson in the group was entitled Learning C# and OOP, Getting Started, Objects and Encapsulation.  That lesson, and each of the lessons following that one, has provided explanations of certain aspects of the essence of Object-Oriented Programming using C#.  The previous lesson was entitled Learning C# and OOP, Runtime Polymorphism through Inheritance.

Necessary and significant aspects

This miniseries will describe and discuss the necessary and significant aspects of OOP using C#.

If you have a general understanding of computer programming, you should be able to read and understand the lessons in this miniseries, even if you don't have a strong background in the C# programming language.

Viewing tip

You may find it useful to open another copy of this lesson in a separate browser window.  That will make it easier for you to scroll back and forth among the different listings while you are reading about them.

Supplementary material

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. 

Preview

What is polymorphism?

The meaning of the word polymorphism is something like one name, many forms.

How does C# implement polymorphism?

Polymorphism manifests itself in C# in the form of multiple methods having the same name.

In some cases, multiple methods have the same name, but different formal argument lists (overloaded methods, which were discussed in a previous lesson).

In other cases, multiple methods have the same name, same return type, and same formal argument list (overridden methods).

Three distinct forms of polymorphism

From a practical programming viewpoint, polymorphism manifests itself in three distinct forms in C#:

  • Method overloading
  • Method overriding through inheritance
  • Method overriding through the C# interface

I covered method overloading as one form of polymorphism (compile-time polymorphism) in a previous lesson.  I also explained automatic type conversion and the use of the cast operator for type conversion in a previous lesson.

I discussed runtime polymorphism implemented through method overriding and class inheritance in a previous lesson.  However, before leaving that topic, I need to discuss an important special case.

The generic type Object

In this lesson, I will discuss 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.

I will cover method overriding through the C# interface in a subsequent lesson.

Discussion and Sample Code

References of type Object

The Object type is a completely generic type, which can be used to store a reference to any object that can be instantiated in C#.

Methods in the Object class

In an earlier lesson, I told you that the class named Object defines default versions of the following methods:

  • ReferenceEquals - Determines whether two object instances are the same instance (whether two references refer to the same object).
  • Equals - Determines whether two object instances are equal (same methods, same data, etc.).
  • GetHashCode - Serves as a hash function for a particular type.
  • GetType - Gets the Type of the current instance.
  • ToString - Returns a string that represents the current object.
  • Finalize - Used to perform cleanup operations before an object is reclaimed by garbage collection.  Expressed using destructor syntax in C#.
  • MemberwiseClone - Creates a shallow copy of the current object.

Some 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.

However, some of them, such as GetType, are intended to be used directly without overriding.

Invoking methods of the Object class

You can store a reference to any object in a reference variable of type Object.  If you have read the previous lessons, you also know how runtime polymorphism based on class inheritance works.

Given the above, you should be able to figure out that you can invoke any of the methods defined in the Object class on any reference to any object stored in a reference variable of type Object (or in a reference variable of any other type, for that matter).

And the behavior will be ...

If the class from which that object is instantiated inherits or defines an overridden version of one of the methods in the above list, invocation of that method on the reference will cause the overridden version to be executed.

Otherwise, invocation of that method on the reference will cause the default version defined in the Object class to be executed.

A sample program

This is illustrated in the program named Poly04, 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 just as well with the other methods in the list.

The class named A

Listing 1 defines a class named A, which explicitly extends the class named Object.

(Recall that this is the default extension, and it is not necessary to explicitly show that a class extends Object.  I showed that here to remind you that all classes in C# are rooted in the class named Object.)

/*File Poly04.cs
Copyright 2002, R.G.Baldwin
**************************************/
using System;

class A : Object{
  //This class is empty
}//end class A

Listing 1

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.

For purposes of this illustration, we will say that 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 contains the definition of a class named B.  This class extends the class named A.
 
class B : A{
  public override String ToString(){
    return "ToString in class B";
  }//end overridden ToString()
}//end class B

Listing 2

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.

(Although not particularly significant for the purposes of this lesson, it inherits the default version of the 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 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.)

class C : B{
  public override String ToString(){
    return "ToString in class C";
  }//end overridden ToString()
}//end class B

Listing 3

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 Poly04.
 
public class Poly04{
  public static void Main(){
    Object varA = new A();
    String v1 = varA.ToString();
    System.Console.WriteLine(v1);
    
Listing 4

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.)

Invoke ToString method on the reference

Then the code invokes 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 following text to be displayed:

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 was 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 invoked on a reference to an object of the class A, the default version of the method is executed, producing the output shown above.

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.
 
    Object varB = new B();
    String v2 = varB.ToString();
    System.Console.WriteLine(v2);
    
Listing 5

Invoke ToString and display the result

The code in Listing 5 invokes 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 following text to be displayed on the computer screen.

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.

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.

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 invoked.

An object of the class C

Finally, the code in Listing 6

  • Instantiates a new object of the class C
  • Stores the object's reference in a reference variable of type Object
  • Invokes the ToString method on the reference and saves the returned string
  • Displays the returned string on the computer screen
    Object varC = new C();
    String v3 = varC.ToString();
    System.Console.WriteLine(v3);

Listing 6

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 following text to be displayed:

ToString in class C

Overridden version of ToString was invoked

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 invoke 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 invoked 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 invoke 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 invoked.

Collections

The C# library also provides a number of generic data structure classes, such as Hashtable, 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.

Summary

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#:

  • Method overloading
  • Method overriding through inheritance
  • Method overriding through the C# interface

In this lesson, I have continued my discussion of the implementation of polymorphism using method overriding through 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, which illustrates the overriding of the ToString() method, which is one of the methods defined in the Object class.

What's Next?

In the next lesson, I will embark on an explanation of runtime polymorphic behavior based on the C# interface and method overriding.

Complete Program Listing

A complete listing of the program is shown in Listing 7 below.
 

/*File Poly04.cs
Copyright 2002, R.G.Baldwin

This program illustrates polymorphic 
behavior

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 Poly04{
  public static void Main(){
    Object varA = new A();
    String v1 = varA.ToString();
    System.Console.WriteLine(v1);
    
    Object varB = new B();
    String v2 = varB.ToString();
    System.Console.WriteLine(v2);
    
    Object varC = new C();
    String v3 = varC.ToString();
    System.Console.WriteLine(v3);

  }//end Main
}//end class Poly04
//===================================//

Listing 7


Copyright 2002, Richard G. Baldwin.  Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.

About the author

Richard Baldwin is a college professor (at Austin Community College in Austin, Texas) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web.

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 has gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.

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-


© 1996, 1997, 1998, 1999, 2000, 2001, 2002 Richard G. Baldwin