developmentor - a developer services company				  
				      C# Tutorial
   Modules

Binding


Goals:

  • Explain type compatibility under inheritance.
  • Describe static and dynamic method binding.
  • Discuss the details of dynamic binding: virtual, abstract, and override.
  • Show how to use dynamic binding and polymorphism to write generic code.
  • Cover downcasting and type testing.

Overview

The most obvious advantages of inheritance are that it helps eliminate repeated code and allows program structure to mirror the organization of the real world. When inheritance is combined with reference compatibility and dynamic method binding things get even more exciting. Clients can then write extremely generic client code that works correctly for any type in the hierarchy. Dynamic binding ensures the correct code is executed for the type of object being processed.


Type compatibility

C# inheritance models the "is-a" relationship so a derived class inherits all the behavior of its base class. To explore the implications of this idea, we will return to the simple person/student/employee inheritance hierarchy which models people at a university.

Suppose the base class person contains a public method named Birthday and some supporting fields.

Now we derive a student class from the person class. The student class inherits the Birthday operation and adds a public SetId method and a supporting field.

Typical client code using the student class might look like that shown below. A Student object is created and a Student reference is used to refer to the object. Using the Student reference, the client code can invoke the inherited Birthday method and the student SetId method.

Things get more interesting when we consider the concept of type compatibility. Type compatibility is a natural consequence of the fact that inheritance models the is-a relationship. The formal way to express this idea is to say something like "a derived class object has all the behavior of its base class and so can be used in any situation where a base class object is expected." In practice this means that we are able to use a base class reference to refer to any derived class object. In our person hierarchy, we could use a Person reference to refer to a Student object. The assignment is legal because a Student is-a Person.

In fact, a Person reference can refer to any object in the hierarchy.

Let's put aside for the moment the major applications of this technique and simply examine the power and limitations of the code. The major advantage of using a base reference is that our code becomes a little bit more generic since only the type of the object is hardcoded. If we later decided to use a different type of object, say an Undergraduate, we would only need to modify the new statement where the object is created while the type of the reference used to manipulate the object would not have to change. The key limitation is that we are only allowed to access the Person part of any object when using a Person reference.

This is a key concept that shows up in many different situations so it's probably worth repeating. The type of the reference determines what access is allowed to the object. If the reference is of type Person, then only Person members can be accessed regardless of the actual type of the object. The object might be an Undergraduate that supports many other useful operations; however, they will not be available through a Person reference.

Despite this limitation, the use of a base class reference is an important and powerful technique. To begin to see the benefits, consider the following CheckAge method which has a parameter of type Person. Because the parameter is a base class reference, any derived class object can be passed as the argument. The CheckAge method is said to be generic because it works for all people.

Without the type compatibility provided through inheritance it would be painful to support the CheckAge operation for each type in the hierarchy. Individual methods would need to be written for each type. The ability to write a single method that works for all types in the hierarchy provides a nice savings in code, coding time, and maintenance effort.


Method binding

Classes from an inheritance hierarchy often contain methods which logically perform the same operation. For example, every class in a hierarchy of different types of employees would naturally offer a Promote method.

Each class would implement their Promote method as appropriate to their type. Ironically, the Employee version would likely be the trickiest to code since it is difficult to know how to promote a generic employee. We'll solve this problem by simply putting an empty body for the implementation.

The Faculty promote method would of course involve a salary increase. In addition, faculty members might have their level increased each time they are promoted. When they reach a certain level they are rewarded with tenure so they can pursue long term research without the need to continuously publish.

The Staff version of promote would probably be quite simple with only a small increase in salary.

Now things get quite interesting since Faculty and Staff objects have two Promote methods, the one they inherit from Employee and their own version. To see the issue, consider the following client code. The Evaluate method has a parameter of type Employee. Type compatibility means we can pass an Employee, a Faculty, or a Staff member. Inside the method, the Promote method is called. Which version should be invoked?

The process of deciding which method to invoke is called binding. There are two types of binding which are called different things in different programming languages. The first type is called static binding in the .NET world. Other names for this type of binding are early binding and compile time binding. Static binding decides which method to call based only on the type of the reference. In the Evaluate example static binding would always choose the Employee version of Promote. The other type of binding is called dynamic binding in .NET. Other names for this concept include runtime binding or late binding. Dynamic binding decides which method to call based on the actual type of the object involved. In essence, dynamic binding puts off the binding decision until runtime when the actual type of the object is known. At runtime, the dynamic binding mechanism determines the type of the object and invokes the appropriate version. In the Evaluate example, if the parameter happens to be a Faculty member then the Faculty version gets called, if it is a Staff member then the Staff version would be invoked.

The employee example has been carefully constructed so that dynamic binding is appropriate for the Promote operation. The key observation is that the Promote methods that appear throughout the hierarchy are all version of the same method. That is, the details of their implementations are of course different but conceptually they all perform the same operation.

To get static binding we need do nothing since it is the default. For dynamic binding we need to apply two new keywords: virtual and override. The keyword virtual goes on the method at the top of the hierarchy where the operation first appears. The derived class versions are labeled with override to indicate they are the more specific version of the inherited virtual method. The implementation for the employee hierarchy is shown below.

Calls to the Promote method will now be dynamically bound. The behavior is illustrated by the code below.

The thing that's so cool about all this is that the Evaluate method automatically adapts its behavior to whatever type of object is passed to it. We can even create new derived types of Employee and pass them to Evaluate and have their version of Promote called. This type of dynamic behavior is often called polymorphism (literally "many shapes" or "many forms"). Polymorphism lets us write generic code such as the Evaluate method that works correctly for all types in an inheritance hierarchy.


Abstract class

Some classes represent abstract concepts. Classic examples of this type of class include person, student, employee, shape, fruit, mammal, etc. These are abstract concepts because they represent only categories and not real concrete types. To model this in code, C# provides the abstract keyword. Any class that represents an abstract concept should be labeled abstract to make the intent of the class clear to the compiler and to other programmers. In the university people hierarchy, it is likely that the Person, Student, and Employee classes would all be abstract.

Marking a class abstract prevents objects of that class from being created. This makes sense because objects of the type do not exist in the real world anyway since the class is modeling an abstract concept.

References are allowed even if the class is abstract. The references will be used to refer to objects of concrete derived types.


Abstract method

Sometimes there is no sensible implementation for a method in a base class. The Promote method in the Employee class provides a good example since there is no way to promote a generic employee. A meaningful implementation is only possible in a concrete derived class such as Faculty or Staff. The abstract keyword is applied to a method to indicate it is an abstract operation. Abstract methods are declaration only and cannot contain an implementation. The signature of an abstract method is followed only by a semicolon where the method body would normally appear.

A class containing an abstract method is considered to be incompletely specified. For this reason, a class with an abstract method must be declared abstract or it is a compile time error.

An abstract method acts as a specification or contract that must be fulfilled by the derived classes. Derived classes need to provide an implementation for the inherited abstract method or they in turn become abstract classes. The derived class implementations are labeled with the keyword override.


Downcasting

The type of reference determines what members of an object are available. This is generally a good thing since it limits generic code to only the operations all types in the hierarchy share. This is a key reason why writing generic code makes sense. This limitation can be frustrating when the derived class offers many services beyond those of the base class. The additional members are, of course, not available through the base reference.

To access the additional members, we need to do a type conversion to obtain a more specific reference to the object. This type of conversion is often called a downcast since the conversion is moving down the hierarchy from base class to derived class. The compiler will not do these conversions automatically since there is the possibility that the conversion may fail. To see the issue consider the following code.

Sometimes the conversion will succeed (if the passed object is in fact a Faculty) and sometimes it will fail (if the passed object is not a Faculty). This possibility for failure is what motivates the compiler to refuse to do the conversion implicitly and to require the programmer to explicitly ask for it. An analogy might be the signing of a liability waiver. The compiler forces the programmer to acknowledge the possibility for failure and accept the risk and consequences of that failure.

The first way to do the conversion is with traditional cast syntax; that is, by placing the destination type name inside parentheses. If the object is of the specified type then the cast succeeds and a reference of the desired type is obtained. However, if the object is not of the right type the CLR will throw an InvalidCastException. The program can handle the exception using the try/catch syntax.

The other way to do the conversion is with the as operator. If the conversion succeeds then a valid reference results. If the conversion fails then the reference will be null.

There is one issue that is probably obvious by this time but which might be worth pointing out anyway. In this kind of type conversion, the thing that is being converted is the reference and not the object. We are simply obtaining a different reference to an existing object and the object itself is completely unchanged.


Type testing

It is possible to test the type of an object using operator is. The operator forms a Boolean expression that evaluates to true if the object is of the tested type and to false otherwise. Note that the type of the object is being tested and not the type of the reference. This should be obvious since the type of the reference is of course already known.

This material is excerpted from the Programming C# course offered by DevelopMentor.