A class can inherit from another class to extend or customize the
original class. Inheriting from a class lets you reuse the functionality in that class
instead of building it from scratch. A class can inherit from only a single class, but can
itself be inherited by many classes, thus forming a class hierarchy. In this example, we
start by defining a class called Asset
:
public class Asset { public string Name; }
Next, we define classes called Stock
and House
, which will inherit from Asset.
Stock
and House
get everything an Asset
has, plus any additional members that they define:
public class Stock :Asset
// inherits from Asset { public long SharesOwned; } public class House :Asset
// inherits from Asset { public decimal Mortgage; }
Here’s how we can use these classes:
Stock msft = new Stock { Name="MSFT", SharesOwned=1000 }; Console.WriteLine (msft.Name); // MSFT Console.WriteLine (msft.SharesOwned); // 1000 House mansion = new House { Name="Mansion", Mortgage=250000 }; Console.WriteLine (mansion.Name); // Mansion Console.WriteLine (mansion.Mortgage); // 250000
The subclasses
Stock
and House
inherit the Name
property from the base
class
Asset
.
References are polymorphic, which means a reference to a base class can refer to an instance of a subclass. For instance, consider the following method:
public static void Display (Asset asset) { System.Console.WriteLine (asset.Name); }
This method can display both a Stock
and a House
because they are both Assets
:
Stock msft = new Stock ... ; House mansion = new House ... ; Display (msft); Display (mansion);
Polymorphism works on the basis that subclasses (Stock
and House
) have all the features of
their base class (Asset
). The converse, however, is not
true. If Display
was modified to accept a House
, you could not pass in an Asset
.
Implicitly upcast to a base class reference
Explicitly downcast to a subclass reference
Casting only affects references; the object itself is not converted or altered. An upcast always succeeds; a downcast succeeds only if the object is suitably typed.
An upcast operation creates a base class reference from a subclass reference. For example:
Stock msft = new Stock ...;
Asset a = msft; // upcast
After the upcast, variable a still references the same Stock
object as variable msft
. The
object being referenced is not itself altered or converted:
Console.WriteLine (a == msft); // True
Although a and msft
refer to the identical
object, a has a more restrictive view on that object:
Console.WriteLine (a.Name); // OK Console.WriteLine (a.SharesOwned); // Error
The last line generates a compile-time error because the reference a is of type
Asset
, even though it refers to an object of type
Stock
. To get to its SharedOwned
field, you must downcast the Asset
to a Stock
.
A downcast operation creates a subclass reference from a base class reference. For example:
Stock msft = new Stock( );
Asset a = msft; // upcast
Stock s = (Stock)a; // downcast
Console.WriteLine (s.SharesOwned); // <No error>
Console.WriteLine (s == a); // true
Console.WriteLine (s == msft); // true
As with an upcast, only references are affected—not the underlying object. A downcast requires an explicit cast because it can potentially fail at runtime:
House h = new House( );
Asset a = h; // Upcast always succeeds
Stock s = (Stock)a; // Downcast fails: a
is not a Stock
If a downcast fails, an InvalidCastException
is
thrown. This is an example of dynamic type checking (we will
elaborate on this concept in the upcoming “Static and Dynamic Type Checking”
section).
A function marked as virtual
can be
overridden by subclasses wanting to provide a specialized
implementation. Methods, properties, indexers, and events can all be declared virtual
:
public class Asset
{
public string Name;
public virtual
decimal Liability
{ get { return 0; } }
}
A subclass overrides a virtual method by applying the override
modifier:
public class Stock : Asset { public long SharesOwned; }
public class House : Asset
{
public decimal Mortgage;
public override
decimal Liability
{ get { return Mortgage; } }
}
By default, the Liability
of an Asset
is 0
. A Stock
does not need to specialize this behavior. However, the
Hous
specializes the Liability
property to return the value of the Mortgage
:
House mansion = new House { Name="Mansion", Mortgage=250000 }; Asset a = mansion; decimal d2 = mansion.Liability; // 250000
The signatures, return types, and accessibility of the virtual and overridden methods
must be identical. An overridden method can call its base class implementation via the
base
keyword (we will cover this shortly, in the
upcoming “The base Keyword” section).
A class declared as abstract can never be instantiated. Instead, only its concrete subclasses can be instantiated.
Abstract classes are able to define abstract members, which are like virtual members, except they don’t provide a default implementation. That implementation must be provided by the subclass, unless that subclass is also declared abstract:
publicabstract
class Asset { // Note empty implementation publicabstract
decimal NetValue { get; } } public class Stock : Asset { public long SharesOwned; public decimal CurrentPrice; // Override like a virtual method. publicoverride
decimal NetValue { get { return CurrentPrice * SharesOwned; } } }
A base class and a subclass may define identical members. For example:
public class A { public int Counter = 1; } public class B : A { public int Counter = 2; }
The Counter
field in class B
is said to hide the Counter
field in class A
. Usually, this
happens accidentally, when a member is added to the base type after
an identical member was added to the subtype. For this reason, the compiler generates a
warning, and then resolves the ambiguity as follows:
References to A
(at compile time) bind to
A.Counter
.
References to B
(at compile time) bind to
B.Counter
.
Occasionally, you want to hide a member deliberately, in which case, you can apply the
new
modifier to the member in the subclass. The
new
modifier does nothing more than
suppress the compiler warning that would otherwise result:
public class A { public int Counter = 1; }
public class B : A { public new
int Counter = 2; }
The new
modifier communicates your intent to the
compiler—and other programmers—that the duplicate member is not an accident.
An overridden function member may seal its implementation with
the sealed
keyword to prevent it from being overridden
by further subclasses. In our earlier virtual function member example, we could have
sealed House
’s implementation of Liability
, preventing a class that derives from House
from overriding Liability
, as follows:
public sealed
override decimal Liability
{ get { return Mortgage; } }
You can also seal the class itself, implicitly sealing all the virtual functions, by applying the sealed modifier to the class itself. Sealing a class is more common than sealing a function member.
The base
keyword is similar to the this
keyword. It serves two essential purposes:
Accessing an overridden function member from the subclass
Calling a base class constructor (see the next section)
In this example, House
uses the base
keyword to access Asset
’s implementation of Liability
:
public class House : Asset
{
...
public override decimal Liability
{
get { return base
.Liability + Mortgage; }
}
}
The same approach works if Liability
is
hidden rather than overridden. (You can also
access hidden members by casting to the base class before invoking the function.)
A subclass must declare its own constructors. For example, if we define Subclass
as follows:
public class Baseclass { public int X; public Baseclass () public Baseclass (int x) { this.X = x; } } public class Subclass : Baseclass { }
the following is illegal:
Subclass s = new Subclass (123);
Subclass
must hence redeclare any constructors it
wants to expose. In doing so, however, it can call any of the base class’s constructors
with the base
keyword:
public class Subclass : Baseclass
{
public Subclass (int x) : base (x)
{ }
}
The base
keyword works rather like the this
keyword, except that it calls a constructor in the base
class.
Base class constructors always execute first; this ensures that base initialization occurs before specialized initialization.
Inheritance has an interesting impact on method overloading. Consider the following two overloads:
static void Foo (Asset a) { } static void Foo (House h) { }
When an overload is called, the most specific type has precedence:
House h = new House (...); Foo (h); // calls Foo (House)
The particular overload to call is determined statically (at compile time) rather than
dynamically (at runtime). The following code calls Foo(Asset)
, even though the runtime type of a
is House
:
Asset a = new House (...); Foo (a); // calls Foo (Asset)