Inheritance

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.

Subclasses are also called derived classes.

Polymorphism

References are polymorphic. This means a variable of type x can refer to an object that subclasses x. 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, since they are both Assets. 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 rewritten to accept a House, you could not pass in an Asset.

Casting and Reference Conversions

An object reference can be:

  • Implicitly upcast to a base class reference

  • Explicitly downcast to a subclass reference

Upcasting and downcasting between compatible reference types performs reference conversions: a new reference is created that points to the same object. An upcast always succeeds; a downcast succeeds only if the object is suitably typed.

Upcasting

An upcast operation creates a base class reference from a subclass reference. For example:

Stock msft = new Stock();    // From previous example
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 same 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 variable a is of type Asset, even though it refers to an object of type Stock. To get to its SharesOwned field, you must downcast the Asset to a Stock.

Downcasting

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 runtime type checking (see Static and Runtime Type Checking).

The as operator

The as operator performs a downcast that evaluates to null (rather than throwing an exception) if the downcast fails:

Asset a = new Asset();
Stock s = a as Stock;   // s is null; no exception thrown

This is useful when you’re going to subsequently test whether the result is null:

if (s != null) Console.WriteLine (s.SharesOwned);

The as operator cannot perform custom conversions (see the section Operator Overloading) and it cannot do numeric conversions.

The is operator

The is operator tests whether a reference conversion would succeed; in other words, whether an object derives from a specified class (or implements an interface). It is often used to test before downcasting:

if (a is Stock) Console.Write (((Stock)a).SharesOwned);

The is operator does not consider custom or numeric conversions, but it does consider unboxing conversions (see The object Type).

Virtual Function Members

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 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 House specializes the Liability property to return the value of the Mortgage:

House mansion = new House { Name="Mansion",
                            Mortgage=250000 };
Asset a = mansion;
Console.WriteLine (mansion.Liability);  // 250000
Console.WriteLine (a.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 (see The base Keyword).

Abstract Classes and Abstract Members

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:

public abstract class Asset
{
  // Note empty implementation
  public abstract decimal NetValue { get; }
}

Subclasses override abstract members just as though they were virtual.

Hiding Inherited Members

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 by accident, 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.

Sealing Functions and Classes

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 { ... } }

You can also seal the class itself, implicitly sealing all the virtual functions, by applying the sealed modifier to the class itself.

The base Keyword

The base keyword is similar to the this keyword. It serves two essential purposes: accessing an overridden function member from the subclass, and 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; }
  }
}

With the base keyword, we access Asset’s Liability property nonvirtually. This means we will always access Asset’s version of this property—regardless of the instance’s actual runtime type.

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.)

Constructors and Inheritance

A subclass must declare its own constructors. For example, if we define Baseclass and 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 “redefine” any constructors it wants to expose. In doing so, 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.

If a constructor in a subclass omits the base keyword, the base type’s parameterless constructor is implicitly called (if the base class has no accessible parameterless constructor, the compiler generates an error).

Constructor and field initialization order

When an object is instantiated, initialization takes place in the following order:

  1. From subclass to base class:

    1. Fields are initialized.

    2. Arguments to base-class constructor calls are evaluated.

  2. From base class to subclass:

    1. Constructor bodies execute.

Overloading and Resolution

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 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)

Note

If you cast Asset to dynamic (see Dynamic Binding), the decision as to which overload to call is deferred until runtime and is based on the object’s actual type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.12.186