
Polymorphism
Polymorphism
allows an entity (for example, variable, function or object) to take a variety
of representations. Therefore we have to distinguish different types of
polymorphism which will be outlined here.
The
first type is similar to the concept of dynamic binding. Here, the type of a
variable depends on its content. Thus, its type depends on the content at a
specific time:
v := 123 /* v is
integer */
... /* use v
as integer */
v := 'abc' /* v
"switches" to string */
... /* use v
as string */
|
Definition (Polymorphism
(1)) The concept of dynamic binding allows a variable to take different
types dependent on the content at a particular time. This ability of a
variable is called polymorphism. |
Another
type of polymorphism can be defined for functions. For example, suppose you
want to define a function isNull() which returns TRUE if its argument
is 0 (zero) and FALSE otherwise. For integer numbers this is easy:
boolean isNull(int i) {
if (i == 0) then
return TRUE
else
return FALSE
endif
}
However,
if we want to check this for real numbers, we should use another comparison due
to the precision problem:
boolean isNull(real r) {
if (r < 0.01 and r > -0.99) then
return TRUE
else
return FALSE
endif
}
In
both cases we want the function to have the name isNull. In
programming languages without polymorphism for functions we cannot declare
these two functions: The name isNull would be doubly defined. However,
if the language would take the parameters of the function into account
it would work. Thus, functions (or methods) are uniquely identified by:
·
the
name of the function (or method) and
·
the
types of its parameter list.
Since
the parameter list of both isNull functions differ, the compiler is
able to figure out the correct function call by using the actual types of the
arguments:
var i : integer
var r : real
i = 0
r = 0.0
...
if (isNull(i)) then ...
/* Use isNull(int) */
...
if (isNull(r)) then
... /* Use isNull(real) */
|
Definition (Polymorphism
(2)) If a function (or method) is defined by the combination of ·
its name and ·
the list of types of its parameters ·
we speak of polymorphism. This type of polymorphism
allows us to reuse the same name for functions (or methods) as long as the
parameter list differs. Sometimes this type of polymorphism is called overloading.
|
The
last type of polymorphism allows an object to choose correct methods. Consider
the function move() again, which takes an object of class Point
as its argument. We have used this function with any object of derived classes,
because the is-a relation holds.
Now
consider a function display() which should be used to display drawable
objects. The declaration of this function might look like this:
display(DrawableObject o) {
...
o.print()
...
}
We
would like to use this function with objects of classes derived from DrawableObject:
Circle acircle
Point apoint
Rectangle arectangle
display(apoint) /*
Should invoke apoint.print() */
display(acircle) /*
Should invoke acircle.print() */
display(arectangle) /*
Should invoke arectangle.print() */
The
actual method should be defined by the content of the object o
of function display(). Since this is somewhat complicated, here is a
more abstract example:
class Base {
attributes:
methods:
virtual foo()
bar()
}
class Derived inherits from Base {
attributes:
methods:
virtual foo()
bar()
}
demo(Base o) {
o.foo()
o.bar()
}
Base abase
Derived aderived
demo(abase)
demo(aderived)
In
this example we define two classes Base and Derive. Each
class defines two methods foo() and bar(). The first method
is defined as virtual. This means that if this
method is invoked its definition should be evaluated by the content of the
object.
We
then define a function demo() which takes a Base object as
its argument. Consequently, we can use this function with objects of class Derived
as the is-a relation holds. We call this function with a Base object
and a Derived object, respectively.
Suppose,
that foo() and bar() are defined to just print out their name
and the class in which they are defined. Then the output is as follows:
foo() of Base called.
bar() of Base called.
foo() of Derived called.
bar() of Base called.
Why
is this so? Let's see what happens. The first call to demo() uses a Base
object. Thus, the function's argument is ``filled'' with an object of class Base.
When it is time to invoke method foo() it's actual functionality is
chosen based on the current content of the corresponding object o.
This time, it is a Base object. Consequently, foo() as
defined in class Base is called.
The
call to bar() is not subject to this content resolution. It
is not marked as virtual. Consequently, bar()
is called in the scope of class Base.
The
second call to demo() takes a Derived object as its argument.
Thus, the argument o is filled with a Derived object.
However, o itself just represents the Base part of the
provided object aderived.
Now,
the call to foo() is evaluated by examining the content of o,
hence, it is called within the scope of Derived. On the other hand, bar()
is still evaluated within the scope of Base.
|
Definition (Polymorphism
(3)) Objects of superclasses can be filled with objects of their subclasses.
Operators and methods of subclasses can be defined to be evaluated in two
contextes:
|
Static and Dynamic Binding
In strongly typed programming languages you
typically have to declare
variables prior to their use. This also implies the variable's definition where the compiler
reserves space for the variable. For example, in Pascal an expression like
var i : integer;
declares variable i to be of type integer.
Additionally, it defines enough memory space to hold an integer value.
With the declaration we bind the name i
to the type integer. This binding is true within the scope in which i
is declared. This enables the compiler to check at compilation time for type
consistency. For example, the following assignment will result in a type
mismatch error when you try to compile it:
var i : integer;
...
i := 'string';
We call this particular type of binding ``static''
because it is fixed at compile time.
|
Definition
(Static Binding) If the type T of a variable is explicitly
associated with its name N by declaration we say that N is statically bound to T. The
association process is called static
binding. |
There exist programming languages which are not
using explicitly typed variables. For example, some languages allow to
introduce variables once they are needed:
... /* No appearance
of i */
i := 123 /* Creation of i
as an integer */
The type of i is known as soon as its value
is set. In this case, i is of type integer since we have assigned a whole
number to it. Thus, because the content of i is a whole
number, the type of i is integer.
|
Definition
(Dynamic Binding) If the type T of a variable with name N is
implicitly associated by its content we say that N is dynamically bound to T. The association process is called dynamic binding. |
Both bindings differ in the time when the type is
bound to the variable. Consider the following example which is only possible
with dynamic binding:
if somecondition() == TRUE then
n := 123
else
n := 'abc'
endif
The type of n after the if statement depends on the evaluation of somecondition(). If it is TRUE, n is of type integer whereas in
the other case it is of type string.
![]()
Exception Handling
( When the contract is broken )
Exceptions have two different
definitions:
·
an
event that causes suspension of normal application execution, and
·
a
set of information directly relating to the event that caused suspension of
normal application execution.
Exceptions can be contrasted with an older, less
reliable technology: “error codes.” The idea behind error codes was fairly
simple. You would request that an application, or part of an application,
accomplish some work. One of the pieces of information that would be returned
to the requester would be an error code. If all had gone well, the error code
would typically have a value of zero. If any problems had occurred, the error
code would have a non-zero value. It was also quite common to associate
different non-zero values of an error code with specific errors.
Error codes suffered from two major problems:
·
No
one was forced to actually check the value of returned error codes.
·
Changes
(additions, deletions, and modifications) in the meanings of the special values
assigned to error codes were not automatically passed on to interested parties.
Tracking the effects of a changed error code value often consumed a significant
amount of resources.
To understand how exceptions directly address both
of these issues, we first need to understand how exceptions typically work:
Ø Exceptions may be defined by
the environment or by the user.
Ø When an exceptional (but not
unforeseen) condition occurs, an appropriate exception is activated. (People
use different terms to express the activation of an exception. The most common
is “raise.” Less commonly, people use the terms “throw” or “activate.”) This activation
may be automatic (controlled by the environment) or may be expressly requested
by the designer of the object or application.
Examples of exceptional
conditions include trying to remove something from an empty container,
directing an elevator on the top floor to "go up," and attempting to
cause a date to take on an invalid value like "February 31, 1993."
Ø Once the exception is
activated, normal application execution stops and control is transferred to a
locally defined exception handler, if one is present. If no locally defined exception
handler is present or if the exception handler is not equipped to handle the
exception, the exception is propagated to the next higher level of the
application. Exceptions cannot be ignored.
An exception will continue to be sent to higher levels of the application until
it is either turned off or the application ceases to function.
Ø An exception handler checks
to see what type of exception has been activated. If the exception is one that
the handler recognizes, a specific set of actions is taken. Executing a set of
actions in response to an exception is known as “handling the exception.”
Handling an exception deactivates the exception; the exception will not be
propagated any further.
Unlike error codes, exceptions cannot be ignored.
Once an exception has been activated, it demands attention. In object-oriented
systems, exceptions are placed in the public interfaces of objects. Changes in
the public interfaces of objects very often require an automatic rechecking of
all other objects that invoke operations in the changed objects. Thus, changes
in exceptions result in at least a partially automated propagation of change
information.