Learn about assignment compatibility, type conversion, and casting for both primitive and reference types. Also learn about the relationships among reference types, method invocations, and the location in the class hierarchy where a method is defined.
Published: January 25, 2010
Validated with Amaya
By Richard G. Baldwin
XNA Programming Notes # 0112
|
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, and compile-time polymorphism in earlier lessons. Before I can explain runtime polymorphism, however, I need to step back and explain some concepts involving type conversion, casting, and assignment compatibility.
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 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.
Type conversion
This lesson explains type conversion for both primitive and reference 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, an object instantiated from any class is assignment-compatible with the type Object.)
Calling a method on an object
Whether or not a method can be called 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 call a method, the method must be defined at or above that class in the class hierarchy.
Assignment compatibility and type conversion
As a background for understanding runtime polymorphism, you need to understand about assignment compatibility and type conversion.
As I mentioned earlier, 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 sbyte and short can be assigned to a variable of type int, because type int has a wider range than either type sbyte 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):
In this lesson, we are interested only in cases 1 and 2 above. We will be interested in the other cases in future lessons involving 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 is provided that illustrates much of the detail involved in type conversion, method invocation, and casting with respect to reference types.
A sample program
The program named Polymorph02, 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.
Listing 1. Source code for class A. using System; class A { //this class is empty }//end class A |
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.
Listing 2. Source code for class B. class B : A {
public void m() {
Console.WriteLine("m in class B");
}//end method m()
}//end class B
|
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 called.
The class named C
Listing 3 shows the definition of the class named C, which also extends Object.
Listing 3. Source code for class C. class C { //this class is empty }//end class C |
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 Polymorph02.
Listing 4. Beginning of the class named Polymorph02. public class Polymorph02 {
public static void Main() {
Object var = new B();
|
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.
(Let me repeat what I just said for emphasis. The reference to the object of type B was not assigned to a reference variable of type B. Instead, 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 call the method named m on the object of type B. This is indicated in Listing 5.
Listing 5. Try to call method m on variable var. //Following will not compile //var.m(); |
A compiler error
An attempt to call 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 call 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 type Object, it cannot be used directly to call 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.
Listing 6. Try a downcast to type A. //Following will not compile //((A)var).m(); |
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 calling the method named m is concerned.
The real solution
The solution to the problem is shown in Listing 7.
Listing 7. Try a downcast to type B. //Following will compile and run ((B)var).m(); |
The code in Listing 7 casts (converts) the reference value contained in the Object variable named var to type B.
The method named m is defined in the class named B. Therefore, a reference of type B can be used to call 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.
Listing 8. Assign var to v1. //Following will compile and run B v1 = (B)var; |
The code in Listing 8 uses a cast to:
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 type C. An attempt to do so is shown in Listing 9.
Listing 9. Cannot be assigned to C. //Following will not execute. // Causes a runtime exception. //C v2 = (C)var; |
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.
Listing 10. Another failed attempt. //Following will not compile //C v3 = (B)var; //Pause until user presses any key. Console.ReadKey(); }//end Main }//end class Polymorph02 |
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.
The end of the program
Listing 10 signals the end of the Main method, the end of the class, and the end of the program.
I encourage you to copy the code from Listing 11. 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.
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:
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 called 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 call a method, the method must be defined in or inherited into that class.
A complete listing of the C# program discussed in this lesson is provided in Listing 11.Listing 11. Project Polymorph02. /*Project Polymorph02 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() { Console.WriteLine("m in class B"); }//end method m() }//end class B //======================================================// class C { //this class is empty }//end class C //======================================================// public class Polymorph02 { 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; //Pause until user presses any key. Console.ReadKey(); }//end Main }//end class Polymorph02 //======================================================// |
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-