Specialization and Substitutability
In the article on inheritance, you saw how one class can derive from another. The
derived class inherits the capabilities of the base and polymorphism enables us
to change the behaviours of that class by adding capabilities.
Generally, a class should have well defined purpose and will provide a specific
set of functionality. The first class in a hierarchy will be as generalised as
possible and classes that derive from these base classes will be more and more
specialized. An analogy for this idea is to imagine trees defined as a hierarchy
of classes.
The most general type of tree is a trunk with branches having leaves. In the
family of trees, we can specialize into two subtypes, Deciduous and Evergreen
trees. Each of these types can specialize further into, for example, Oak or Ash
that specialize from Deciduous and Holly or Pine that derive from the Evergreen
type.
When we program, we will use a variable to store a type or a parameter in which
a type may be passed. The actual data that can be passed in the parameter or
stored in the variable depends upon the relationship between the declared type
and the actual type.
Barbera Liskov, a computer scientist at M.I.T in the 1980's and now Institute
Professor at the same university published a paper that showed how a variable
could contain a type or a more specialized subtype. In short, given a storage
location for type T we can subtitute any subtype
S that derives in some way from the base type
T.
To illustrate this, the slide show below explains the substitutions that would
work for our Tree based heirarchy.
Covariance and contravariance
These are two terms that are related to the substitution principle and refer to
the way types may be used when passed to parameters or when returned from
methods.
Covariance rules apply when passing a parameter and when a type is passed which
is different to but which may be converted to the type specified in the
parameter declaration. Covariance allows that a type may undergo an implicit
conversion if the type is a subtype of the declared type or if that conversion
can be made with no loss of precision. For example, when we declare a
method like so:
static void
DoSomething(float f)
{
}
static void
Main()
{
int n = 10;
float f = 10.5f;
double d = 10.5;
DoSomething(n);
DoSomething(f);
DoSomething(d); <-- Causes a compiler error because doubles are not
covariant with floats
}
The compiler will refuse to compile the last line because the double cannot be
used without an explicit conversion that acknowledges that loss of precision is
acceptable.
Similarly, when we return a value from a method, covariance rules apply like so:
static float
DoSomething1()
{
return (int)10;
}
static float
DoSomething2()
{
return 10.5f;
}
static float
DoSomething3()
{
return (double)10.5;
<--Refuses to compile because a double cannot be returned where a float is
expected
}
Contravariance allows for a type to be used where a more specialized type is
specified. For example the code which passes a double in a parameter designated
for a float would not present a problem.
In the C# programming language support for contravariant operations has recently
been added in C# 4.0. The specific cases are an advanced level subject that are
not relavent to this discussion
|