Learning C# and OOP, Polymorphism, Type Conversion, Casting, etc.

Baldwin teaches you about assignment compatibility, type conversion, and casting for both primitive and reference types.  He also teaches you about the relationships among reference types, method invocations, and the location in the class hierarchy where a method is defined.

Published:  November 4, 2002
By Richard G. Baldwin

C# Programming Notes # 116


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, Polymorphism Based on Overloaded Methods.

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

Type conversion

This lesson discusses type conversion for both primitive and reference types.  (Some people refer to primitive types as value types.)

Assignment compatibility

A value of a particular type may be assignment-compatible with variables of other types.  If so, the value can be assigned directly to the variable. 

If not, it may be possible to perform a cast on the value to change its type and assign it to the variable as the new type.

Successful cast depends on class hierarchy

With regard to reference types, whether or not a cast can be successfully performed depends on the relationship of the classes involved in the class hierarchy.

The generic type Object

A reference to any object can be assigned to a reference variable of the type Object, because the Object class is a superclass of every other class.  (In other words, it is assignment-compatible with the type Object.)

What is a downcast?

When we cast a reference along the class hierarchy in a direction away from the root class Object toward the leaves, we often refer to it as a downcast.

Invoking a method on an object

Whether or not a method can be invoked on a reference to an object depends on the current type of the reference and the location in the class hierarchy where the method is defined. 

In order to use a reference of a class type to invoke a method, the method must be defined at or above that class in the class hierarchy.

A sample program is provided that illustrates much of the detail involved in type conversion, method invocation, and casting with respect to reference types.

Discussion and Sample Code

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.

(This results in 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.

(This results in overridden methods, which will be discussed in detail in subsequent lessons.)

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 in a previous lesson.

In this lesson, I will backtrack a bit and discuss the conversion of references from one type to another.

I will begin the discussion of polymorphism through method overriding and inheritance in the next lesson.  I will cover interfaces in a subsequent lesson.

Assignment compatibility and type conversion

As a background for polymorphism, you need to understand something about assignment compatibility and type conversion.

A value of a given type is assignment-compatible with another type if a value of the first type can be successfully assigned to a variable of the second type.

Type conversion and the cast operator

In some cases, type conversion happens automatically.  In other cases, type conversion must be forced through the use of a cast operator.

A cast operator is a unary operator, which has a single right operand.  The physical representation of the cast operator is the name of a type enclosed by a pair of matched parentheses, as in:

(int)

Applying a cast operator

Applying a cast operator to the name of a variable doesn't actually change the type of the variable.  However, it does cause the contents of the variable to be treated as a different type for the evaluation of the expression in which the cast operator is contained.

Primitive values and type conversion

Assignment compatibility issues come into play for both primitive types and reference types.

To begin with, values of type bool can only be assigned to variables of type bool (you cannot change the type of a bool).  Thus, a value of type bool is not assignment-compatible with a variable of any other type.

In general, numeric primitive values can be assigned to (are assignment-compatible with) a variable of a type whose numeric range is as wide as or wider than the range of the type of the value.  In that case, the type of the value is automatically converted to the type of the variable.

(For example, types byte and short can be assigned to a variable of type int, because type int has a wider range than either type byte or type short.)

Conversion to narrower range

On the other hand, a primitive numeric value of a given type cannot be assigned to (is not assignment-compatible with) a variable of a type with a narrower range than the type of the value. 

However, it is possible to use a cast operator to force a type conversion for numeric primitive values.

(Oftentimes, such a conversion will result in the loss of data, and that loss is the responsibility of the programmer who performs the cast.)

Assignment compatibility for references

Assignment compatibility for references doesn't involve range issues, as is the case with primitives.  Rather, the reference to an object instantiated from a given class can be assigned to (is assignment-compatible with):

  • Any reference variable whose type is the same as the class from which the object was instantiated.
  • Any reference variable whose type is a superclass of the class from which the object was instantiated.
  • Any reference variable whose type is an interface that is implemented by the class from which the object was instantiated.
  • Any reference variable whose type is an interface that is implemented by a superclass of the class from which the object was instantiated.
  • A couple of other cases involving interfaces that extend other interfaces.

Such an assignment does not require the use of a cast operator.

Type Object is completely generic

A reference to any object can be assigned to a reference variable of the type Object, because the Object class is a superclass of every other class.

Converting reference types with a cast

Assignments of references, other than those listed above, require the use of a cast operator to purposely change the type of the reference.

Doesn't work in all cases

However, it is not possible to perform a successful cast to convert the type of a reference to another type in all cases.

Generally, a cast can only be performed among reference types that fall on the same ancestral line of the class hierarchy, or on an ancestral line of an interface hierarchy.  For example, a reference cannot be successfully cast to the type of a sibling or a cousin in the class hierarchy.

Downcasting

When we cast a reference along the class hierarchy in a direction away from the root class Object toward the leaves, we often refer to it as a downcast.

While it is also possible to cast in the direction from the leaves to the root, this conversion happens automatically, and the use of a cast operator is not required.

A sample program

The program named Poly02, shown in Listing 11 near the end of the lesson, illustrates the use of the cast operator with references.

When you examine that program, you will see that two classes named A and C each extend the class named Object.  Hence, we might say that they are siblings in the class hierarchy.

Another class named B extends the class named A.  Thus, we might say that A is a child of Object, and B is a child of A.

The class named A

The definition of the class named A is shown in Listing 1.  This class implicitly extends the class named Object by default.
 
/*File Poly02.cs
Copyright 2002, R.G.Baldwin
**************************************/
using System;

class A{
  //this class is empty
}//end class A

Listing 1

The class named A is empty.  It was included in this example for the sole purpose of adding a layer of inheritance to the class hierarchy.

The class named B

Listing 2 shows the definition of the class named B.  This class extends the class named A.
 
class B : A{
  public void m(){
    System.Console.WriteLine(
                       "m in class B");
  }//end method m()
}//end class B

Listing 2

The method named m()

The class named B defines a method named m().  The behavior of the method is simply to display a message each time it is invoked.

The class named C

Listing 3 contains the definition of the class named C, which also extends Object.
 
class C{
  //this class is empty
}//end class C

Listing 3

The class named C is also empty.  It was included in this example as a sibling class for the class named A.  Stated differently, it was included as a class that is not in the ancestral line of the class named B.

The driver class

Listing 4 shows the beginning of the driver class named Poly02.
 
public class Poly02{
  public static void Main(){
    Object var = new B();

Listing 4

An object of the class named B

This code instantiates an object of the class B and assigns the object's reference to a reference variable of type Object.

(It is important to note that the reference to the object of type B was not assigned to a reference variable of type B.  Rather, it was assigned to a reference variable of type Object.)

This assignment is allowable because Object is a superclass of B.  In other words, the reference to the object of the class B is assignment-compatible with a reference variable of the type Object.

Automatic type conversion

In this case, the reference of type B is automatically converted to type Object and assigned to the reference variable of type Object.

(Note that the use of a cast operator was not required in this assignment.)

Only part of the story

However, assignment compatibility is only part of the story.  The simple fact that a reference is assignment-compatible with a reference variable of a given type says nothing about what can be done with the reference after it is assigned to the reference variable.

An illegal operation

For example, in this case, the reference variable that was automatically converted to type Object cannot be used directly to invoke the method named m() on the object of type B.  This is indicated in Listing 5.
 
    //Following will not compile
    //var.m();


Listing 5

An attempt to invoke the method named m() on the reference variable of type Object in Listing 5 resulted in the following compiler error:

error CS0117:
'object' does not contain a definition for 'm'

(It was necessary to convert the statement to a comment in order to get the program to compile successfully.)

An important rule

In order to use a reference of a class type to invoke a method, the method must be defined at or above that class in the class hierarchy.  Stated differently, the method must either be defined in, or inherited into that class.

This case violates the rule

In this case, the method named m() is defined in the class named B, which is two levels down from the class named Object.

(The method named m() is neither defined in nor inherited into the class named Object.)

When the reference to the object of the class B was assigned to the reference variable of type Object, the type of the reference was automatically converted to type Object.

Therefore, because the reference is of type Object, it cannot be used directly to invoke the method named m().

The solution is a downcast

In this case, the solution to the problem is a downcast.

The code in Listing 6 shows an attempt to solve the problem by casting the reference down the hierarchy to type A.
 
    //Following will not compile
    //((A)var).m();    
    
Listing 6

Still doesn't solve the problem

However, this still doesn't solve the problem, and the result is another compiler error. 

(The method named m() is neither defined in nor inherited into the class named A.)

Again, it was necessary to convert the statement into a comment in order to get the program to compile.

What is the problem here?

The problem is that the downcast simply didn't go far enough down the inheritance hierarchy.

The class named A does not contain a definition of the method named m().  Neither does it inherit the method named m().  The method named m() is defined in class B, which is a subclass of A.

Therefore, a reference of type A is no more useful than a reference of type Object insofar as invoking the method named m() is concerned.

The real solution

The solution to the problem is shown in Listing 7.
 
    //Following will compile and run
    ((B)var).m();
    
Listing 7

The code in Listing 7 casts (converts) the reference value contained in the Object variable named var down to type B.

The method named m() is defined in the class named B.  Therefore, a reference of type B can be used to invoke the method.

The code in Listing 7 compiles and executes successfully.  This causes the method named m() to execute, producing the following output on the computer screen.

m in class B

A few odds and ends

Before leaving this topic, let's look at a few more issues.

The code in Listing 8 declares and populates a new variable of type B.
 
    //Following will compile and run
    B v1 = (B)var;


Listing 8

The code in Listing 8 uses a cast to:

  • Convert the contents of the Object variable to type B
  • Assign the converted reference to the new reference variable of type B.

A legal operation

This is a legal operation.  In this class hierarchy, the reference to the object of the class B can be assigned to a reference variable of the types B, A, or Object.

(While this operation is legal, it is often not a good idea to have two different reference variables that contain references to the same object.  In this case, the variables named var and v1 both contain a reference to the same object.)

Cannot be assigned to type C

However, the reference to the object of the class B cannot be assigned to a reference variable of any other type, including the type C.  An attempt to do so is shown in Listing 9.
 
    //Following will not execute.
    // Causes a runtime exception.
    //C v2 = (C)var;


Listing 9

The code in Listing 9 attempts to cast the reference to type C and assign it to a reference variable of type C.

A runtime exception

Although the program will compile, it won't execute.  An attempt to execute the statement in Listing 9 results in an exception at runtime.

As a result, it was necessary to convert the statement into a comment in order to execute the program.

Another failed attempt

Similarly, an attempt to cast the reference to type B and assign it to a reference variable of type C, as shown in Listing 10, won't compile.
 
    //Following will not compile
    //C v3 = (B)var;


Listing 10

The problem here is that the class C is not a superclass of the class named B.  Therefore, a reference of type B is not assignment-compatible with a reference variable of type C.

Again, it was necessary to convert the statement to a comment in order to compile the program.

Summary

This lesson discusses type conversion for primitive and reference types.

A value of a particular type may be assignment-compatible with variables of other types.

If the type of a value is not assignment-compatible with a variable of a given type, it may be possible to perform a cast on the value to change its type and assign it to the variable as the new type.  For primitive types, this will often result in the loss of information.

In general, numeric values of primitive types can be assigned to any variable whose type represents a numeric range that is as wide as or wider than the range of the value's type.  (Values of type bool can only be assigned to variables of type bool.)

With respect to reference types, the reference to an object instantiated from a given class can be assigned to any of the following without the use of a cast:

  • Any reference variable whose type is the same as the class from which the object was instantiated.
  • Any reference variable whose type is a superclass of the class from which the object was instantiated.
  • Any reference variable whose type is an interface that is implemented by the class from which the object was instantiated.
  • Any reference variable whose type is an interface that is implemented by a superclass of the class from which the object was instantiated.
  • A couple of other cases involving interfaces that extend other interfaces.

Assignments of references, other than those listed above, require the use of a cast to change the type of the reference.

It is not always possible to perform a successful cast to convert the type of a reference.  Whether or not a cast can be successfully performed depends on the relationship of the classes involved in the class hierarchy.

A reference to any object can be assigned to a reference variable of the type Object, because the Object class is a superclass of every other class.

When we cast a reference along the class hierarchy in a direction away from the root class Object toward the leaves, we often refer to it as a downcast.

Whether or not a method can be invoked on a reference to an object depends on the current type of the reference and the location in the class hierarchy where the method is defined.  In order to use a reference of a class type to invoke a method, the method must be defined in or inherited into that class.

A sample program is provided that illustrates much of the detail involved in type conversion, method invocation, and casting with respect to reference types.

What's Next?

I will begin the discussion of runtime polymorphism through method overriding and inheritance in the next lesson.

I will demonstrate that for runtime polymorphism, 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.

Complete Program Listing

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

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

This program illustrates downcasting

Program output is:
  
m in class B
**************************************/
using System;

class A{
  //this class is empty
}//end class A
//===================================//

class B : A{
  public void m(){
    System.Console.WriteLine(
                       "m in class B");
  }//end method m()
}//end class B
//===================================//

class C{
  //this class is empty
}//end class C
//===================================//

public class Poly02{
  public static void Main(){
    Object var = new B();
    //Following will not compile
    //var.m();
    //Following will not compile
    //((A)var).m();    
    //Following will compile and run
    ((B)var).m();
    
    //Following will compile and run
    B v1 = (B)var;
    //Following will not execute.
    // Causes a runtime exception.
    //C v2 = (C)var;
    //Following will not compile
    //C v3 = (B)var;
  }//end Main
}//end class Poly02
//===================================//

Listing 11


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