Learning C# and OOP, Runtime Polymorphism through InheritanceWith runtime polymorphism, the selection of a method for execution is based on the actual type of the object whose reference is stored in a reference variable, and not on the type of the reference variable on which the method is invoked. Published: November 5, 2002
C# Programming Notes # 120 PrefaceThis 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, Polymorphism, Type Conversion, Casting, etc. 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. PreviewWhat 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#:
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. In this lesson ... I will begin the discussion of runtime polymorphism through method overriding and inheritance in this lesson. I will cover interfaces in a subsequent lesson. The essence of runtime polymorphic behavior Methods are invoked on references to objects. Typically, those references are stored in reference variables, or in the elements of a collection such as an array or a hashtable. With runtime polymorphism based on method overriding, the decision as to which version of a method will be executed is based on the actual type of the object whose reference is stored in the reference variable or element, and not on the type of the reference variable or element on which the method is invoked. Late binding The decision as to which version of the method to invoke cannot be made at compile time. That decision must be deferred and made at runtime. This is sometimes referred to as late binding, although everyone may not agree with this interpretation of that term. Discussion and Sample CodeOperational description of runtime polymorphism Here is an operational description of runtime polymorphism as implemented in C# through inheritance and method overriding:
This is runtime polymorphism. Runtime polymorphism is very powerful As you gain more experience with C#, you will learn that much of the power of OOP using C# is centered on runtime polymorphism using class inheritance, interfaces, and method overriding. (The use of interfaces for polymorphism will be discussed in a subsequent lesson.) An important attribute of runtime polymorphism The decision as to which version of the method to execute is based on the actual type of object whose reference is stored in the reference variable, and not on the type of the reference variable on which the method is invoked. Why is it called runtime polymorphism? The reason that this type of polymorphism is often referred to as runtime polymorphism is because the decision as to which version of the method to execute cannot be made until runtime. The decision cannot be made at compile time (as is the case with overloaded methods). Why defer the decision? The decision cannot be made at compile time because the compiler has no way of knowing (when the program is compiled) the actual type of the object whose reference will be stored in the reference variable. In an extreme case, for example, the object might be obtained at runtime from a network connection of which the compiler has no knowledge. Could be either type For the situation described above, that object could just as easily be of type SuperClass as of type SubClass. In either case, it would be valid to assign the object's reference to the same superclass reference variable. If the object were of the SuperClass type, then an invocation of the method named method on the reference would cause the version of the method defined in SuperClass, and not the version defined in SubClass, to be executed. (The version executed is determined by the type of the object and not by the type of the reference variable containing the reference to the object.) Sample Program Let's take a look at a sample program that illustrates runtime polymorphism using class inheritance and overridden methods. The name of the program is Poly03. A complete listing of the program is shown in Listing 7 near the end of the lesson. The class named A I will discuss this program in fragments. Listing 1 shows the definition of a class named A, which extends the class named Object by default.
The class named A defines a simple method named m(). A virtual method Note that the method named m is declared to be virtual. This means that it is allowable to override this method in a subclass.
Behavior of the method The behavior of the method, as defined in the class named A, is to display a message indicating that it has been invoked, and that it is defined in the class named A. This message will allow us to determine which version of the method is executed in each case discussed later. The class named B Listing 2 shows the definition of a class named B that extends
the class named A.
The class named B overrides (redefines) the method named m(), which it inherits from the class named A. The override declaration Note the use of the override declaration in Listing 2. In C#, if one method overrides another, it is necessary to declare that fact.
A compiler warning If you fail to make the override declaration in Listing 2, you will get a compiler warning that reads something like the following: warning CS0114: 'B.m()' hides inherited member 'A.m()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword. I'm not going to get into a discussion of the difference between overriding and hiding in this lesson. Perhaps I will find the time to provide such a discussion in a future lesson. Behavior of the overridden method Like the inherited version, the overridden version displays a message indicating that it has been invoked. However, the message is different from the message displayed by the inherited version discussed above. The overridden version tells us that it is defined in the class named B.
Again, this message will allow us to determine which version of the method is executed in each case discussed later. The driver class Listing 3 shows the beginning of the driver class named Poly03.
A new object of the class B Note that the code in the Main method begins by instantiating a new object of the class named B, and assigning the object's reference to a reference variable of type Object.
Downcast and invoke the method If you read the previous lesson, it will come as no surprise to you that the second statement in the Main method, which casts the reference down to type B and invokes the method named m() on it, will compile and execute successfully. Which version was executed? The execution of the method produces the following output on the computer screen: m in class B By examining the output, you can confirm that the version of the method that was overridden in the class named B is the version that was executed. Why was this version executed? This should also come as no surprise to you. The cast converts the type of the reference from type Object to type B. You can always invoke a public method belonging to an object using a reference to the object whose type is the same as the class from which the object was instantiated. Not runtime polymorphic behavior Just for the record, the above invocation of the method does not constitute runtime polymorphism (in my opinion). I included that invocation of the method to serve as a backdrop for what follows.
This is runtime polymorphic behavior However, the invocation of the method in Listing 4 does constitute runtime
polymorphism.
The statement in Listing 4 casts the reference down to type A and invokes the method named m() on that reference. It may, or may not come as a surprise to you that the invocation of the method shown in Listing 4 also compiles and runs successfully. The method output Here is the punch line. Not only does the statement in Listing 4 compile and run successfully, it produces the following output, (which is exactly the same output as before): m in class B Same method executed in both cases It is extremely important to note that this output, (produced by casting the reference variable to type A instead of type B), is exactly the same as that produced by the earlier invocation of the method when the reference was cast to type B. This means that the same version of the method was executed in both cases. This confirms that even though the type of the reference was converted to type A, (rather than type Object or type B), the overridden version of the method defined in class B was actually executed.
The version of the method that was executed was based on the actual type of the object, B, and not on the type of the reference, A. This is an extremely powerful and useful concept. Another invocation of the method Now take a look at the statement in Listing 5. Will this statement
compile and execute successfully? (Obviously, the answer to the
question is given in Listing 5). If so, which version of the method
will be executed?
Compiler error The code in Listing 5 attempts, unsuccessfully, to invoke the method named m() using the reference variable named var, which is of type Object. The result is a compiler error, which reads something like the following: error CS0117: 'object' does not contain a definition for 'm' Some important rules The Object class does not define a method named m(). Therefore, the overridden method named m() in the class named B is not an overridden version of a method that is defined in the class named Object. Necessary, but not sufficient Runtime polymorphism based on class inheritance requires that the type of the reference variable be a superclass of the class from which the object (on which the method will be invoked) is instantiated. (At least this requirement is true if a significant decision among methods is to be made.) However, while necessary, that is not sufficient to ensure runtime polymorphic behavior. The type of the reference variable must also be the name of a class that either defines or inherits the method that will ultimately be invoked on the object. This method is not defined in the Object class Since the class named Object does not define (or inherit) the method named m(), a reference of type Object does not qualify as a participant in runtime polymorphic behavior in this case. The attempt to use it as a participant results in the compiler error given above. One additional scenario Before leaving this topic, let's look at one additional scenario to
help you distinguish what is, and what is not, runtime polymorphism.
Consider the code shown in Listing 6.
A new object of type A The code in Listing 6 instantiates a new object of the class named A, and stores the object's reference in the original reference variable named var of type Object.
Downcast and invoke the method Then the code in Listing 6 casts the reference down to type A, (the type of the object to which the reference refers), and invokes the method named m() on the downcast reference. The output As you would probably predict, this produces the following output on the computer screen: m in class A In this case, the version of the method defined in the class named A, (not the version defined in B) was executed. Not polymorphic behavior Again, in my view, this is not runtime polymorphic behavior (at least it isn't a very useful form of polymorphic behavior). This code simply converts the type of the reference from type Object to the type of the class from which the object was instantiated, and invokes one of its methods. Nothing special takes place regarding a selection among different versions of the method.
Once again, what is runtime polymorphism? As I have discussed in this lesson, runtime polymorphic behavior based on inheritance occurs when
More than you ever wanted to hear And that is probably more than you ever wanted to hear about runtime polymorphism based on inheritance. A future lesson will discuss runtime polymorphism based on the C# interface. From a practical viewpoint, you will find the rules to be similar but somewhat different in the case of the C# interface. SummaryPolymorphism 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#:
This lesson discusses method overriding through inheritance. With runtime polymorphism based on method overriding, the decision as to which version of a method will be executed is based on the actual type of object whose reference is stored in the reference variable, and not on the type of the reference variable on which the method is invoked. The decision as to which version of the method to invoke cannot be made at compile time. That decision must be deferred and made at runtime. This is sometimes referred to as late binding. This is illustrated in the sample program discussed in this lesson. What's Next?In the next lesson, I will continue my discussion of the implementation of polymorphism using method overriding through inheritance, and I will concentrate on a special case in that category. Specifically, I will discuss the use of the Object class as a completely generic type for storing references to objects of subclass types, and explain how that results in a very useful form of runtime polymorphism. Complete Program ListingA complete listing of the program is shown in Listing 7 below.
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 authorRichard 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 |