Implementation Inheritance
If you want to declare that a class derives
from another class, use the following syntax:
|
|
Tip |
This syntax is very similar to C++ and Java
syntax. However, C++ programmers, who will be used to the concepts
of public and private inheritance, should note that C# does not
support private inheritance, hence the absence of a public or
private qualifier on the base class name. Supporting private
inheritance would have complicated the language for very little
gain. In practice, private inheritance is used extremely rarely in
C++ anyway.
|
If a class (or a struct) also derives from
interfaces, the list of base class and interfaces is separated by
commas:
For a struct, the syntax is as follows:
If you do not specify a base class in a class
definition, the C# compiler will assume that System.Object is the base class. Hence, the
following two pieces of code yield the same result:
and
For the sake of simplicity, the second form is more
common.
Because C# supports the object keyword, which serves as a pseudonym for the
System.Object class, you can also
write:
If you want to reference the Object class, use the object keyword, which is recognized by intelligent
editors such as Visual Studio .NET and thus facilitates editing
your code.
Virtual Methods
By declaring a base class function as
virtual, you allow the function to be
overridden in any derived classes:
It is also permitted to declare a property as
virtual. For a virtual or overridden
property, the syntax is the same as for a nonvirtual property, with
the exception of the keyword virtual,
which is added to the definition. The syntax looks like this:
For simplicity, the following discussion focuses
mainly on methods, but it applies equally well to properties.
The concepts behind virtual functions in C# are
identical to standard OOP concepts. You can override a virtual
function in a derived class, and when the method is called, the
appropriate method for the type of object is invoked. In C#,
functions are not virtual by default but (aside from constructors)
can be explicitly declared as virtual.
This follows the C++ methodology: for performance reasons,
functions are not virtual unless indicated. In Java, by contrast,
all functions are virtual. C# differs from C++ syntax, however,
because it requires you to
declare when a derived class’s function overrides another function,
using the override keyword:
This syntax for method overriding removes potential
runtime bugs that can easily occur in C++, when a method signature
in a derived class unintentionally differs slightly from the base
version, resulting in the method failing to override the base
version. In C# this is picked up as a compile-time error, because
the compiler would see a function marked as override but no base method for it to override.
Neither member fields nor static functions can
be declared as virtual. The concept simply wouldn’t make sense for
any class member other than an instance function member.
Hiding Methods
If a method with the same signature is
declared in both base and derived classes, but the methods are not
declared as virtual and override, respectively, then the derived class
version is said to hide the base class
version.
In most cases, you would want to override methods
rather than hide them; by hiding them you risk calling the “wrong”
method for a given class instance. However, as shown in the
following example, C# syntax is designed to ensure that the
developer is warned at compile time about this potential problem,
thus making it safer to hide methods if that is your intention.
This also has versioning benefits for developers of class
libraries.
Suppose that you have a class called HisBaseClass:
At some point in the future you write a derived
class that adds some functionality to HisBaseClass. In particular, you add a method called
MyGroovyMethod(), which is not present
in the base class:
One year later, you decide to extend the
functionality of the base class. By coincidence, you add a method
that is also called MyGroovyMethod() and
has the same name and signature as yours, but probably doesn’t do
the same thing. When you compile your code using the new version of
the base class, you have a potential clash because your program
won’t know which method to call. It’s all perfectly legal C#, but
because your MyGroovyMethod() is not
intended to be related in any way to the base class MyGroovyMethod(), the result is that running this
code does not yield the result you want. Fortunately, C# has been
designed in such a way that it copes very well when conflicts of
this type arise.
In these situations, C# generates a compilation
warning. That reminds you to use the new
keyword to declare that you intend to hide a method, like this:
However, because your version of MyGroovyMethod() is not declared as new, the compiler will pick up on the fact that it’s
hiding a base class method without being instructed to do so and
generate a warning (this applies whether or not you declared
MyGroovyMethod() as virtual). If you want, you can rename your version
of the method. This is the recommended course of action, because it
will eliminate future confusion. However, if you decide not to
rename your method for whatever reason (for example, if you’ve
published your software as a library for other companies, so you
can’t change the names of methods), all your existing client code
will still run correctly, picking up your version of MyGroovyMethod(). That’s because any existing code
that accesses this method must be doing so through a reference to
MyDerivedClass (or a further derived
class).
Your existing code cannot access this method
through a reference to HisBaseClass; it
would generate a compilation error when compiled against the
earlier version of HisBaseClass. The
problem can only happen in client code you have yet to write. C#
arranges things so that you get a warning that a potential problem
might occur in future code - and you will need to pay attention to
this warning, and take care not to attempt to call your version of
MyGroovyMethod() through any reference
to HisBaseClass in any future code you
add. However, all your existing code will still work fine. It may
be a subtle point, but it’s quite an impressive example of how C#
is able to cope with different versions of classes.
Calling Base Versions of Functions
C# has a special syntax for calling base
versions of a method from a derived class: base.<MethodName>(). For example, if you want
a method in a derived class to return 90 percent of the value
returned by the base class method, you can use the following
syntax:
Java uses a similar syntax, with the exception that
Java uses the keyword super rather than
base. C++ has no similar keyword but
instead requires specification of the class name (CustomerAccount:: CalculatePrice()). Any equivalent to base in C++ would have been ambiguous because C++
supports multiple inheritance.
Note that you can use the base.<MethodName>() syntax to call any method
in the base class - you don’t have to call it from inside an
override of the same method.
Abstract Classes and Functions
C# allows both classes and functions to be
declared as abstract. An abstract class cannot be instantiated,
whereas an abstract function does not have an implementation, and
must be overridden in any non-abstract derived class. Obviously, an
abstract function is automatically virtual (although you don’t need
to supply the virtual keyword; doing so
results in a syntax error). If any class contains any abstract
functions, that class is also abstract and must be declared as
such:
C++ developers will notice some syntactical
differences in C# here. C# does not support the =0 syntax to declare abstract functions. In C#, this
syntax would be misleading because =<value> is allowed in member fields in class
declarations to supply initial values:
|
|
Tip |
C++ developers should also note the slightly
different terminology: In C++, abstract functions are often
described as pure virtual; in the C# world, the only correct term
to use is abstract.
|
Sealed Classes and Methods
C# allows classes and methods to be declared
as sealed. In the case of a class, this
means that you can’t inherit from that class. In the case of a
method, this means that you can’t override that method.
|
|
Tip |
Java developers will recognize sealed as the C# equivalent of Java’s final.
|
The most likely situation in which you’ll mark a
class or method as sealed will be if the
class or method is internal to the operation of the library, class,
or other classes that you are writing, so you are sure that any
attempt to override some of its functionality will cause problems.
You might also mark a class or method as sealed for commercial reasons, in order to prevent a
third party from extending your classes in a manner that is
contrary to the licensing agreements. In general, however, you
should be careful about marking a class or member as sealed, because by doing so you are severely
restricting how it can be used. Even if you don’t think it would be
useful to inherit from a class or override a particular member of
it, it’s still possible that at some point in the future someone
will encounter a situation you hadn’t anticipated in which it is
useful to do so. The .NET base class library frequently uses sealed
classes in order to make these classes inaccessible to third-party
developers who might want to derive their own classes from them.
For example, string is a sealed
class.
Declaring a method as sealed serves a similar purpose as for a class,
although you rarely will want to declare a method as sealed.
It does not make sense to use the sealed keyword on a method unless that method is
itself an override of another method in some base class. If you are
defining a new method and you don’t want anyone else to override
it, you would not declare it as virtual
in the first place. If, however, you have overridden a base class
method, the sealed keyword provides a
way of ensuring that the override you supply to a method is a
“final” override in the sense that no one else can override it
again.
Constructors of Derived Classes
Chapter 3 discusses how constructors can be
applied to individual classes. An interesting question arises as to
what happens when you start defining your own constructors for
classes that are part of a hierarchy, inherited from other classes
that may also have custom constructors.
Assume you have not defined any explicit
constructors for any of your classes. This means that the compiler
supplies default zeroing-out constructors for all your classes.
There is actually quite a lot going on under the hood when that
happens, but the compiler is able to arrange it so that things work
out nicely throughout the class hierarchy and every field in every
class gets initialized to whatever its default value is. When you
add a constructor of your own, however, you are effectively taking
control of construction. This has implications right down through
the hierarchy of derived classes, and you have to make sure that
you don’t inadvertently do anything to prevent construction through
the hierarchy from taking place smoothly.
You might be wondering why there is any special
problem with derived classes. The reason is that when you create an
instance of a derived class, there is actually more than one
constructor at work. The constructor of the class you instantiate
isn’t by itself sufficient to initialize the class - the
constructors of the base classes must also be called. That’s why
we’ve been talking about construction through the hierarchy.
To see why base class constructors must be called,
you’re going to develop an example based on a cell phone company
called MortimerPhones. The example contains an abstract base class,
GenericCustomer, which represents any
customer. There is also a (non-abstract) class, Nevermore60Customer, that represents any customer on
a particular rate called the Nevermore60
rate. All customers have a name, represented by a private field.
Under the Nevermore60 rate, the first
few minutes of the customer’s call time are charged at a higher
rate, necessitating the need for the field highCostMinutesUsed, which details how many of these
higher-cost minutes each customer has used up. The class
definitions look like this:
We won’t worry about what other methods might be
implemented in these classes, because we are concentrating solely
on the construction process here. And if you download the sample
code for this chapter, you’ll find that the class definitions
include only the constructors.
Take a look at what happens when you use the
new operator to instantiate a
Nevermore60Customer:
Clearly, both of the member fields name and highCostMinutesUsed must be initialized when
customer is instantiated. If you don’t
supply constructors of your own, but rely simply on the default
constructors, then you’d expect name to
be initialized to the null reference,
and highCostMinutesUsed to zero. Let’s
look in a bit more detail at how this actually happens.
The highCostMinutesUsed
field presents no problem: the default Nevermore60Customer constructor supplied by the
compiler will initialize this field to zero.
What about name? Looking
at the class definitions, it’s clear that the Nevermore60Customer constructor can’t initialize
this value. This field is declared as private, which means that
derived classes don’t have access to it. So, the default
Nevermore60Customer constructor simply
won’t know that this field exists. The only code items that have
that knowledge are other members of GenericCustomer. This means that if name is going to be initialized, that’ll have to be
done by some constructor in GenericCustomer. No matter how big your class
hierarchy is, this same reasoning applies right down to the
ultimate base class, System.Object.
Now that you have an understanding of the issues
involved, you can look at what actually happens whenever a derived
class is instantiated. Assuming that default constructors are used
throughout, the compiler first grabs the constructor of the class
it is trying to instantiate, in this case Nevermore60Customer. The first thing that the
default Nevermore60Customer constructor
does is attempt to run the default constructor for the immediate
base class, GenericCustomer. Then the
GenericCustomer constructor attempts to
run the constructor for its immediate base class; System.Object. System.Object doesn’t have any base classes, so its
constructor just executes and returns control to the GenericCustomer constructor. That constructor now
executes, initializing name to
null, before returning control to the
Nevermore60Customer constructor. That
constructor in turn executes, initializing highCostMinutesUsed to zero, and exits. At this
point, the Nevermore60Customer instance
has been successfully constructed and initialized.
The net result of all this is that the constructors
are called in order of System.Object
first, then progressing down the hierarchy until the compiler
reaches the class being instantiated. Notice also that in this
process, each constructor handles initialization of the fields in
its own class. That’s how it should normally work, and when you
start adding your own constructors you should try to stick to that
principle.
Notice the order in which this happens. It’s always
the base class constructors that get called first. This means that
there are no problems with a constructor for a derived class
invoking any base class methods, properties, and any other members
that it has access to, because it can be confident that the base
class has already been constructed and its fields initialized. It
also means that if the derived class doesn’t like the way that the
base class has been initialized, it can change the initial values
of the data, provided that it has access to do so. However, good
programming practice almost invariably means you’ll try to prevent
that situation from occurring if you can, and you will trust the
base class constructor to deal with its own fields.
Now that you know how the process of construction
works, you can start fiddling with it by adding your own
constructors.
Adding a Constructor in a Hierarchy
We’ll take the simplest case first and see
what happens if you simply replace the default constructor
somewhere in the hierarchy with another constructor that takes no
parameters. Suppose that you decide that you want everyone’s name
to be initially set to the string “<no
name>” instead of to the null
reference. You’d modify the code in GenericCustomer like this:
Adding this code will work fine. Nevermore60Customer still has its default
constructor, so the sequence of events described earlier will
proceed as before, except that the compiler will use the custom
GenericCustomer constructor instead of
generating a default one, so the name
field will always be initialized to “<no
name>” as required.
Notice that in your constructor that you’ve added a
call to the base class constructor before the GenericCustomer constructor is executed, using a
syntax similar to that used earlier when we discussed how to get
different overloads of constructors to call each other. The only
difference is that this time you use the base keyword instead of this, to indicate that it’s a constructor to the
base class rather than a constructor to
the current class you want to call. There are no parameters in the
brackets after the base keyword - that’s
important because it means you are not passing any parameters to
the base constructor, so the compiler will have to look for a
parameterless constructor to call. The result of all this is that
the compiler will inject code to call the System.Object constructor, just as would happen by
default anyway.
In fact, you can omit that line of code and write
the following (as was done for most of the constructors so far in
the chapter):
If the compiler doesn’t see any reference to
another constructor before the opening curly brace, it assumes that
you intended to call the base class constructor; this fits in with
the way that default constructors work.
The base and
this keywords are the only keywords
allowed in the line that calls another constructor. Anything else
causes a compilation error. Also note that only one other
constructor can be specified.
So far, this code works fine. One good way to mess
up the progression through the hierarchy of constructors, however,
is to declare a constructor as private:
If you try this, you’ll find you get an interesting
compilation error, which could really throw you if you don’t
understand how construction down a hierarchy works:
The interesting thing is that the error occurs not
in the GenericCustomer class, but in the
derived class, Nevermore60Customer.
What’s happened is that the compiler has tried to generate a
default constructor for Nevermore60Customer but has not been able to because
the default constructor is supposed to invoke the no-parameter
GenericCustomer constructor. By
declaring that constructor as private,
you’ve made it inaccessible
to the derived class. A similar error occurs if you supply a
constructor to GenericCustomer, which
takes parameters, but at the same time you fail to supply a
no-parameter constructor. In this case, the compiler will not
generate a default constructor for GenericCustomer, so when it tries to generate the
default constructors for any derived class, it’ll again find that
it can’t because a no-parameter base class constructor is not
available. A workaround would be to add your own constructors to
the derived classes, even if you don’t actually need to do anything
in these constructors, so that the compiler doesn’t try to generate
any default constructor for them.
Now that you have all the theoretical
background you need, you’re ready to move on to an example of how
you can neatly add constructors to a hierarchy of classes. In the
next section, you start adding constructors that take parameters to
the MortimerPhones example.
Adding constructors with parameters to a
hierarchy
You’re going to start with a one-parameter
constructor for GenericCustomer, which
controls that customers can be instantiated only when they supply
their names:
So far, so good. However, as mentioned previously,
this will cause a compilation error when the compiler tries to
create a default constructor for any derived classes, because the
default compiler-generated constructors for Nevermore60Customer will try to call a no-parameter
GenericCustomer constructor and
GenericCustomer does not possess such a
constructor. Therefore, you’ll need to supply your own constructors
to the derived classes to avoid a compilation error:
Now instantiation of Nevermore60Customer objects can only take place when
a string containing the customer’s name is supplied, which is what
you want anyway. The interesting thing is what the Nevermore60Customer constructor does with this
string. Remember that it can’t initialize the name field itself, because it has no access to
private fields in its base class. Instead, it passes the name
through to the base class for the GenericCustomer constructor to handle. It does this
by specifying that the base class constructor to be executed first
is the one that takes the name as a parameter. Other than that, it
doesn’t take any action of its own.
Next, you’re going to investigate what happens if
you have different overloads of the constructor as well as a class
hierarchy to deal with. To this end, assume that Nevermore60
customers may have been referred to MortimerPhones by a friend as
part of one of those sign-up-a-friend-and-get-a-discount offers.
This means that when you
construct a Nevermore60Customer, you may
need to pass in the referrer’s name as well. In real life, the
constructor would have to do something complicated with the name,
such as process the discount, but here you’ll just store the
referrer’s name in another field.
The Nevermore60Customer
definition will now look like this:
The constructor takes the name and passes it to the
GenericCustomer constructor for
processing. referrerName is the variable
that is your responsibility here, so the constructor deals with
that parameter in its main body.
However, not all Nevermore60Customers will have a referrer, so you
still need a constructor that doesn’t require this parameter (or a
constructor that gives you a default value for it). In fact, you
will specify that if there is no referrer, then the referrerName field should be set to “<None>”, using the following one-parameter
constructor:
You’ve now got all your constructors set up
correctly. It’s instructive to examine the chain of events that now
occurs when you execute a line like this:
The compiler sees that it needs a one-parameter
constructor that takes one string, so the constructor it will
identify is the last one that you’ve defined:
When you instantiate customer, this constructor will be called. It
immediately transfers control to the corresponding Nevermore60Customer two-parameter constructor,
passing it the values “Arabel Jones”,
and “<None>”. Looking at the code
for this constructor, you see that it in turn immediately passes
control to the one-parameter GenericCustomer constructor, giving it the string
“Arabel Jones”, and in turn that
constructor passes control to the System.Object default constructor. Only now do the
constructors execute. First, the System.Object constructor executes. Next comes the
GenericCustomer constructor, which
initializes the name field. Then the
Nevermore60Customer two-parameter
constructor gets control back, and sorts out initializing the
referrerName to “<None>”. Finally, the Nevermore60Customer one-parameter constructor gets
to execute; this constructor doesn’t do anything else.
As you can see, this is a very neat and
well-designed process. Each constructor handles initialization of
the variables that are obviously its responsibility, and in the
process your class has been correctly instantiated and prepared for
use. If you follow the same principles when you write your own
constructors for your classes, you should find that even the most
complex classes get initialized smoothly and without any
problems.
|