7

Inheritance

Chapter 6 discussed how one class can reference other classes via fields and properties. This chapter discusses how to use the inheritance relationship between classes to build class hierarchies that form an “is a” relationship.

Beginner Topic
Inheritance Definitions

Chapter 6 provided an overview of inheritance. Here’s a review of the defined terms:

Derive/inherit: Specialize a base class to include additional members or customization of the base class members.
Derived/sub/child type: The specialized type that inherits the members of the more general type.
Base/super/parent type: The general type whose members a derived type inherits.

Inheritance forms an “is a kind of” relationship. The derived type is always implicitly also of the base type. Just as a hard drive is a kind of storage device, so any other type derived from the storage device type is a kind of storage device. Notice that the converse is not necessarily true: A storage device is not necessarily a hard drive.

note
Inheritance within code is used to define an “is a kind of” relationship between two classes where the derived class is a specialization of the base class.

Derivation

It is common to want to extend a given type to add features, such as behavior and data. The purpose of inheritance is to do exactly that. Given a Person class, you create an Employee class that additionally contains EmployeeId and Department properties. The reverse approach may also be applied. Given, for example, a Contact class within a personal digital assistant (PDA), you may decide to add calendaring support. Toward this effort, you create an Appointment class. However, instead of redefining the methods and properties that are common to both classes, you might choose to refactor the Contact class. Specifically, you could move the common methods and properties for Contact into a base class called PdaItem from which both Contact and Appointment derive, as shown in Figure 7.1.

Figure 7.1: Refactoring into a base class

The common items in this case are Created, LastUpdated, Name, ObjectKey, and the like. Through derivation, the methods defined on the base class, PdaItem, are accessible from all classes derived from PdaItem.

When declaring a derived class, follow the class identifier with a colon and then the base class, as Listing 7.1 demonstrates.

Listing 7.1: Deriving One Class from Another
public class PdaItem
{
    [DisallowNull]
    public string? Name { getset; }
    public DateTime LastUpdated { getset; }
}
// Define the Contact class as inheriting the PdaItem class
public class Contact : PdaItem
{
    public string? Address { getset; }
    public string? Phone { getset; }
 
    // ...
}

Listing 7.2 shows how to access the properties defined in Contact.

Listing 7.2: Using Inherited Methods
public class Program
{
    public static void Main()
    {
        Contact contact = new();
        contact.Name = "Inigo Montoya";
 
        // ...
    }
}

Even though Contact does not directly have a property called Name, all instances of Contact can still access the Name property from PdaItem and use it as though it was part of Contact. Furthermore, any additional classes that derive from Contact will also inherit the members of PdaItem or any class from which PdaItem was derived. The inheritance chain has no practical limit, and each derived class will have all the members of its base class inheritance chain combined (see Listing 7.3). In other words, although Customer doesn’t derive from PdaItem directly, it still inherits the members of PdaItem.

note
Via inheritance, each member of a base class will also appear within the chain of derived classes.
Listing 7.3: Classes Deriving from One Another to Form an Inheritance Chain
public class PdaItem : object
{
    // ...
}
public class Appointment : PdaItem
{
    // ...
}
public class Contact : PdaItem
{
    // ...
}
public class Customer : Contact
{
    // ...
}

In Listing 7.3, PdaItem is shown explicitly to derive from object. Although C# allows such syntax, it is unnecessary because all classes that don’t have some other derivation will derive from object, regardless of whether it is specified.

note
Unless an alternative base class is specified, all classes will derive from object by default.
Casting between Base and Derived Types

As Listing 7.4 shows, because derivation forms an “is a” relationship, a derived type value can always be directly assigned to a base type variable.

Listing 7.4: Implicit Base Type Casting
public class Program
{
    public static void Main()
    {
        // Derived types can be implicitly converted to
        // base types
        Contact contact = new();
        PdaItem item = contact;
        // ...
 
        // Base types must be cast explicitly to derived types
        contact = (Contact)item;
        // ...
    }
}

The derived type, Contact, is a PdaItem and can be assigned directly to a variable of type PdaItem. This is known as an implicit conversion because no cast operator is required and the conversion will, in principle, always succeed; that is, it will not throw an exception.

The reverse, however, is not true. A PdaItem is not necessarily a Contact; it could be an Appointment or some other derived type. Therefore, casting from the base type to the derived type requires an explicit cast, which could fail at runtime. To perform an explicit cast, you identify the target type within parentheses prior to the original reference, as Listing 7.4 demonstrates.

With the explicit cast, the programmer essentially communicates to the compiler to trust her—she knows what she is doing—and the C# compiler allows the conversion to proceed if the target type is derived from the originating type. Although the C# compiler allows an explicit conversion at compile time between potentially compatible types, the Common Language Runtime (CLR) will still verify the explicit cast at execution time, throwing an exception if the object instance is not actually of the targeted type.

The C# compiler allows use of the cast operator even when the type hierarchy allows an implicit conversion. For example, the assignment from contact to item could use a cast operator as follows:

item = (PdaItem)contact;

or even when no conversion is necessary:

contact = (Contact)contact;

note
A derived object can be implicitly converted to its base class. In contrast, converting from the base class to the derived class requires an explicit cast operator, as the conversion could fail. Although the compiler will allow an explicit cast if it is potentially valid, the runtime will still prevent an invalid cast at execution time by throwing an exception.
Beginner Topic
Casting within the Inheritance Chain

An implicit conversion to a base class does not instantiate a new instance. Instead, the same instance is simply referred to as the base type, and the capabilities (the accessible members) are those of the base type. It is just like referring to a CD-ROM drive as a “storage device.” Since not all storage devices support an eject operation, a CD-ROM drive that is viewed as a storage device cannot be ejected either, and a call to storageDevice.Eject() would not compile, even though the instantiated object may have been a CDROM object that supported the Eject() method.

Similarly, casting down from the base class to the derived class simply begins referring to the type more specifically, expanding the available operations. The restriction is that the actual instantiated type must be an instance of the targeted type (or something derived from it).

AdVanced Topic
Defining Custom Conversions

Conversion between types is not limited to types within a single inheritance chain. It is possible to convert between unrelated types as well, such as converting from an Address to a string and vice versa. The key is the provision of a conversion operator between the two types. C# allows types to include either explicit or implicit conversion operators. If the operation could possibly fail, such as in a cast from long to int, developers should choose to define an explicit conversion operator. This warns developers performing the conversion to do so only when they are certain the conversion will succeed, or else to be prepared to catch the exception if it doesn’t. They should also use an explicit conversion over an implicit conversion when the conversion is lossy. Converting from a float to an int, for example, truncates the decimal, which a return cast (from int back to float) would not recover.

Listing 7.5 shows an example of an implicit conversion operator signature.

Listing 7.5: Defining Cast Operators
class GpsCoordinates
{
    // ...
 
    public static implicit operator UtmCoordinates(
        GpsCoordinates coordinates)
    {
        // ...
    }
}

In this case, you have an implicit conversion from GpsCoordinates to UtmCoordinates. A similar conversion could be written to reverse the process. Note that an explicit conversion could also be written by replacing implicit with explicit.

private Access Modifier

All members of a base class, except for constructors and destructors, are inherited by the derived class. However, just because a member is inherited, that does not mean it is accessible. For example, in Listing 7.6, the private field, _Name, is not available in Contact because private members are accessible only inside the type that declares them.

Listing 7.6: Private Members Are Inherited but Not Accessible
public class PdaItem
{
    // ...
    private string _Name;
 
    public string Name
    {
        get { return _Name; }
        set { _Name = value; }
    }
    // ...
}
 
public class Contact : PdaItem
{
    // ...
}
 
public class Program
{
    public static void Main()
    {
        Contact contact = new();
        // ERROR:  'PdaItem._Name' is inaccessible
        // due to its protection level
        contact._Name = "Inigo Montoya";
    }
}

As part of respecting the principle of encapsulation, derived classes cannot access members declared as private.1 This forces the base class developer to make an explicit choice as to whether a derived class gains access to a member. In this case, the base class is defining an API in which _Name can be changed only via the Name property. That way, if validation is added, the derived class will gain the validation benefit automatically because it was unable to access _Name directly from the start. Regardless of the access modifier, all members can be accessed from the class that defines them.

note
Derived classes cannot access members declared as private in a base class.
protected Access Modifier

Encapsulation is finer grained than just public or private, however. It is possible to define members in base classes that only derived classes can access. (Any member can also always access other members within the same type.) As an example, consider the ObjectKey property shown in Listing 7.7.

Listing 7.7: protected Members Are Accessible Only from Derived Classes
using System.IO;
 
public class PdaItem
{
    public PdaItem(Guid objectKey) => ObjectKey = objectKey;
    protected Guid ObjectKey { get; }
}
 
public class Contact : PdaItem
{
    public Contact(Guid objectKey)
        : base(objectKey) { }
 
    public void Save()
    {
        // Instantiate a FileStream using <ObjectKey>.dat
        // for the filename
        using FileStream stream = File.OpenWrite(
            ObjectKey + ".dat");
        // ...
        stream.Dispose();
    }
    static public Contact Copy(Contact contact)
        => new(contact.ObjectKey);
 
    static public Contact Copy(PdaItem pdaItem) =>
    // ERROR: Cannot access protected member PdaItem.ObjectKey.
    // Use ((Contact)pdaItem).ObjectKey instead.
        new(pdaItem.ObjectKey);
}
 
public class Program
{
    public static void Main()
    {
        Contact contact = new(Guid.NewGuid());
 
        // ERROR:  'PdaItem.ObjectKey' is inaccessible
        Console.WriteLine(contact.ObjectKey);
    }
}

ObjectKey is defined using the protected access modifier. The result is that it is accessible only outside of PdaItem from members in classes that derive from PdaItem. Because Contact derives from PdaItem, all members of Contact (i.e., Save()) have access to ObjectKey. In contrast, Program does not derive from PdaItem, so using the ObjectKey property within Program results in a compile-time error.

note
Protected members in the base class are accessible only from the base class and other classes within the derivation chain.

An important subtlety shown in the static Contact.Copy(PdaItem pdaItem) method is worth noting. Developers are often surprised that it is not possible to access the protected ObjectKey of a PdaItem from code within Contact, even though Contact derives from PdaItem. The reason is that a PdaItem could potentially be an Address, and Contact should not be able to access protected members of Address. Therefore, encapsulation prevents Contact from potentially modifying the ObjectKey of an Address. A successful cast of PdaItem to Contact will bypass this restriction (i.e., ((Contact)pdaItem).ObjectKey), as does accessing contact.ObjectKey. The governing rule is that accessing a protected member from a derived class requires a compile-time determination that the protected member is an instance of the derived class.

Extension Methods

Extension methods are technically not members of the type they extend and, therefore, are not inherited. Nevertheless, because every derived class may be used as an instance of any of its base classes, an extension method for one type also extends every derived type. In other words, if we extend a base class such as PdaItem, all the extension methods will also be available in the derived classes. However, as with all extension methods, priority is given to instance methods. If a compatible signature appears anywhere within the inheritance chain, it will take precedence over an extension method.

Requiring extension methods for base types is rare. As with extension methods in general, if the base type’s code is available, it is preferable to modify the base type directly. Even in cases where the base type’s code is unavailable, programmers should consider whether to add extension methods to an interface that the base type or the individual derived types implement. We cover interfaces and their use with extension methods in Chapter 8.

Single Inheritance

In theory, you can place an unlimited number of classes in an inheritance tree. For example, Customer derives from Contact, which derives from PdaItem, which derives from object. However, C# is a single-inheritance programming language (as is the Common Intermediate Language [CIL] to which C# compiles). Consequently, a class cannot derive from two classes directly. It is not possible, for example, to have Contact derive from both PdaItem and Person.

Language Contrast: C++—Multiple Inheritance

C#’s single inheritance is one of its major object-oriented differences from C++.

For the rare cases that require a multiple-inheritance class structure, one solution is to use aggregation; instead of one class inheriting from another, one class contains an instance of the other. C# 8.0 provides additional constructs for achieving this, so we defer the details of implementing aggregation until Chapter 8.

Sealed Classes

Designing a class correctly so that others can extend it via derivation can be a tricky task that requires testing with examples to verify the derivation will work successfully. Listing 7.8 shows how to avoid unexpected derivation scenarios and problems by marking classes as sealed.

Listing 7.8: Preventing Derivation with Sealed Classes
public sealed class CommandLineParser
{
    // ...
}
 
// ERROR:  Sealed classes cannot be derived from
public sealed class DerivedCommandLineParser
    : CommandLineParser
{
    // ...
}

Sealed classes include the sealed modifier, so they cannot be derived from. The string type is an example of a type that uses the sealed modifier to prevent derivation.

________________________________________

1. Except for the corner case, when the derived class is also a nested class of the base class.
{{ snackbarMessage }}