DekGenius.com
[ Team LiB ] Previous Section Next Section

3.2 Inheritance

A class can inherit from another class to extend or customize the original class. Inheriting from a class allows you to 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. A well-designed class hierarchy is one that reasonably generalizes the nouns in a problem space. For example, there is a class called Image in the System.Drawing namespace, which the Bitmap, Icon, and Metafile classes inherit from. All classes are ultimately part of a single giant class hierarchy, of which the root is the Object class. All classes implicitly inherit from it.

In this example, we start by defining a class called Location. This class is very basic, and provides a location with a name property and a way to display itself to the console window:

class Location { // Implicitly inherits from object
  string name;
  
  // The constructor that initializes Location
  public Location(string n) {
    name = n;
  }
  public string Name {get {return name;}}
  public void Display( ) {
    System.Console.WriteLine(Name);
  }
}

Next, we define a class called URL, which will inherit from Location. The URL class has all the same members as Location, as well as a new member, Navigate. Inheriting from a class requires specifying the class to inherit from the class declaration, using the C++ colon notation:

class URL : Location { // Inherit from Location
  public void Navigate( ) {
    System.Console.WriteLine("Navigating to "+Name);
  }
  // The constructor for URL, which calls Location's constructor
  public URL(string name) : base(name) {  }
}

Now, we instantiate a URL, then invoke both the Display method (which is defined in Location) and the navigate method (which is defined in URL):

class Test {
  static void Main( ) {
    URL u = new URL("http://microsoft.com");
    u.Display( );
    u.Navigate( );
  }
}

The specialized class/general class is referred to as the derived class/base class or the subclass/superclass.

3.2.1 Class Conversions

A class D may be implicitly upcast to the class B that it derives from, and a class B may be explicitly downcast to the class D that derives from it. For instance:

URL u = new URL( );
Location l = u; // upcast
u = (URL)l; // downcast

If the downcast fails, an InvalidCastException is thrown.

3.2.1.1 The as operator

The as operator makes a downcast that evaluates to null if the downcast fails:

u = l as URL;
3.2.1.2 The is operator

The is operator tests whether an object is or derives from a specified class (or implements an interface). It is often used to perform a test before a downcast:

if (l is URL)
  ((URL)l).Navigate( );

3.2.2 Polymorphism

Polymorphism is the ability to perform the same operations on many types, as long as each type shares a common subset of characteristics. C# custom types exhibit polymorphism by inheriting classes and implementing interfaces (see Section 3.5 later in this chapter).

To continue with our inheritance example, the Show method can perform the operation Display on both a URL and a LocalFile, because both types inherit a Location's set of characteristics:

class LocalFile : Location {
  public void Execute( ) {
    System.Console.WriteLine("Executing "+Name);
  }
  // The constructor for LocalFile, which calls URL's constructor
  public LocalFile(string name) : base(name) {  }
}
class Test {
  static void Main( ) {
    URL u = new URL("http://microsoft.com");
    LocalFile l = new LocalFile("c:\\readme.txt");
    Show(u);
    Show(l);
  }
  public static void Show(Location loc) {
    System.Console.Write("Location is: ");
    loc.Display( );
  }
}

3.2.3 Virtual Function Members

A key aspect of polymorphism is the ability for each type to exhibit a shared characteristic in its own way. A base class may have virtual function members, which enable a derived class to provide its own implementation for that function member (also see Section 3.5 later in this chapter):

class Location {
  public virtual void Display( ) {
    Console.WriteLine(Name);
  }
    ...
}
class URL : Location {
  // chop off the http:// at the start
  public override void Display( ) {
    Console.WriteLine(Name.Substring(6));
  }
  ...
}

URL now has a custom way of displaying itself. The Show method in the Test class in the previous section now calls the new implementation of Display. The signatures of the overridden method and the virtual method must be identical, but unlike Java and C++, the override keyword is also required.

3.2.4 Abstract Classes and Abstract Members

A class may be declared abstract. An abstract class may have abstract members. These are function members without implementation that are implicitly virtual. In our earlier examples, we had a Navigate method for the URL and an Execute method for the LocalFile. Instead, Location could be an abstract class with an abstract method called Launch:

abstract class Location {
  public abstract void Launch( );
}
class URL : Location {
  public override void Launch( ) {
    Console.WriteLine("Run Internet Explorer...");
  }
}
class LocalFile : Location {
  public override void Launch( ) {
    Console.WriteLine("Run Win32 Program...");
  }
}

A derived class must override all its inherited abstract members, or must itself be declared abstract. An abstract class cannot be instantiated. For instance, if LocalFile does not override Launch, then LocalFile itself must be declared abstract, perhaps so that Shortcut and PhysicalFile can derive from it.

3.2.5 Sealed Classes

A class may prevent other classes from inheriting from it by specifying the sealed modifier in the class declaration:

sealed class Math {
  ...
}

The most common scenario for sealing a class is when a class is composed of only static members, which is the case with the Math class. Another effect of sealing a class is that it enables the compiler to turn all virtual method invocations made on that class into faster nonvirtual method invocations.

3.2.6 Hiding Inherited Members

Aside from calling a constructor, the new keyword can also be used to hide the base class data members, function members, and type members of a class. Overriding a virtual method with the new keyword hides, rather than overrides, the base class implementation of the method:

class B {
  public virtual void Foo( ) {  }
}
class D : B {
  public override void Foo( ) {  }
}
class N : D {
  public new void Foo( ) {  } // hides D's Foo
}
N n = new N( );
n.Foo( ); // calls N's Foo
((D)n).Foo( ); // calls D's Foo
((B)n).Foo( ); // calls D's Foo

A method declaration with the same signature as its base class must explicitly state whether it overrides or hides the inherited member.

3.2.7 Advanced Features of Virtual Function Members

This section deals with the subtleties of the virtual function member calling mechanism.

3.2.7.1 Versionioning virtual function members

In C#, a method is compiled with a flag that is true if it overrides a virtual method. This flag is important for versioning. Suppose you write a class that derives from a base class in the .NET Framework, and deploy your application to a client computer. Later, the client upgrades the .NET Framework, and that base class now has a virtual method that matches the signature of one of your methods in the derived class:

class B { // written by the library people
  virtual void Foo( ) {...} // added in latest update
}
class D : B { // written by you
  void Foo( ) {...}
}

In most object-oriented languages such as Java, methods are not compiled with this flag, so a derived class's method with the same signature is assumed to override the base class's virtual method. This means a virtual call is made to D's Foo, even though D's Foo was unlikely to have been made according to the specification intended by the author of B. This could easily break your application. In C#, the flag for D's Foo is false, so the runtime knows to treat D's Foo as new, which ensures that your application functions as it was originally intended. When you get the chance to recompile with the latest framework, you can add the new modifier to Foo, or perhaps rename Foo to something else.

3.2.7.2 Sealed virtual function members

An overridden function member may seal its implementation, so that it cannot be overridden. In our earlier virtual function member example, we could have sealed the URL's implementation of Display. This prevents a class that derives from URL from overriding Display, which provides a guarantee on the behavior of a URL.

class Location {
  public virtual void Display( ) {
    Console.WriteLine(Name);
    }
    ...
}
class URL : Location {
  public sealed override void Display( ) {
    Console.WriteLine(Name.Substring(6));
  }
  ...
}
3.2.7.3 Overriding a virtual function member

Very occasionally, it is useful to override a virtual function member with an abstract one. In this example, the abstract Editor class overrides the Text property to ensure that concrete classes deriving from Editor provide an implementation for Text.

class Control {
  public virtual string Text {
    get {
       return null;
    }
  }
}
abstract class Editor : Control {
  abstract override string Text {get;}
}

3.2.8 The base Keyword

The base keyword is similar to the this keyword, except that it accesses an overridden or hidden base class function member. The base keyword is also used to call a base class constructor (see the next section) or access a base class indexer (by using base instead of this). Calling base accesses the next most derived class that defines that member. The following builds upon our example from the section on the this keyword:

class Hermit : Dude {
  public void new Introduce(Dude a) {
    base.Introduce(a);
    Console.WriteLine("Nice Talking To You");
  }
}

There is no way to access an instance member of a specific base class, as with the C++ scope resolution :: operator.

3.2.9 Constructor and Field Initialization in the Inheritance Chain

Initialization code can execute for each class in an inheritance chain, in constructors as well as field initializers. There are two rules. The first is field initialization occurs before any constructor in the inheritance chain is called. The second is the base class executes its initialization code before the derived class does.

A constructor must call its base class constructors first. In a case in which the base class has a parameterless constructor, the constructor is implicitly called. In a case in which the base class provides only constructors that require parameters, the derived class constructor must explicitly call one of the base class constructors with the base keyword. A constructor may also call an overloaded constructor (which calls base for it):

class B {
  public int x ;
  public B(int a) {
    x = a;
  }
  public B(int a, int b) {
    x = a * b;
  }
  // Notice how all of B's constructors need parameters
}
class D : B {
  public D( ) : this(7) {  } // call an overloaded constructor
  public D(int a) : base(a) {  } // call a base class constructor
}

Consistent with instance constructors, static constructors respect the inheritance chain, so each static constructor from the least derived to the most derived is called.

    [ Team LiB ] Previous Section Next Section