Abstract Classes

Many of the inheritance examples so far have defined a class called PdaItem that defines the methods and properties common to Contact, Appointment, and so on, which are type objects that derive from PdaItem. PdaItem is not intended to be instantiated itself, however. A PdaItem instance has no meaning by itself; instead, it has meaning only when it is used as a base class—to share default method implementations across the set of data types that derive from it. These characteristics are indicative of the need for PdaItem to be an abstract class rather than a concrete class. Abstract classes are designed for derivation only. It is not possible to instantiate an abstract class, except in the context of instantiating a class that derives from it. Classes that are not abstract and can instead be instantiated directly are concrete classes.

Abstract classes are a fundamental object-oriented principle, so we describe them here accordingly. However, starting with C# 8.0, interfaces almost (specifically, no instance fields can be declared) support a superset of the functionality previously limited to abstract classes. While the details of the new interface capabilities are available in Chapter 8, understanding the concepts regarding abstract members is a prerequisite, so we will provide the details of abstract classes here.

Beginner Topic
Abstract Classes

Abstract classes represent abstract entities. Their abstract members define what an object derived from an abstract entity should contain, but they don’t include the implementation. Often, much of the functionality within an abstract class is unimplemented. Before a class can successfully derive from an abstract class, however, it needs to provide the implementation for the abstract methods in its abstract base class.

To define an abstract class, C# requires the abstract modifier to the class definition, as shown in Listing 7.16.

Listing 7.16: Defining an Abstract Class
// Define an abstract class
public abstract class PdaItem
{
    public PdaItem(string name)
    {
        Name = name;
    }
 
    public virtual string Name { getset; }
}
 
public class Program
{
    public static void Main()
    {
        
        // ERROR:  Cannot create an instance of the abstract class
        PdaItem item = new("Inigo Montoya");
    }
}

Although abstract classes cannot be instantiated, this restriction is a minor characteristic of an abstract class. Their primary significance is achieved when abstract classes include abstract members. An abstract member is a method or property that has no implementation. Its purpose is to force all derived classes to provide the implementation.

Consider Listing 7.17 as an example.

Listing 7.17: Defining Abstract Members
using static System.Environment;
 
// Define an abstract class
public abstract class PdaItem
{
    public PdaItem(string name)
    {
        Name = name;
    }
 
    public virtual string Name { getset; }
    public abstract string GetSummary();
}
 
public class Contact : PdaItem
{
    // ...
    public override string Name
    {
        get
        {
            return $"{ FirstName } { LastName }";
        }
        set
        {
            string[] names = value.Split(' ');
            // Error handling not shown
            FirstName = names[0];
            LastName = names[1];
        }
    }
 
    public string FirstName
    {
        get
        {
            return _FirstName!;
        }
        set
        {
            _FirstName = value ??
                throw new ArgumentNullException(nameof(value)); ;
        }
    }
    private string? _FirstName;
 
    public string LastName
    {
        get
        {
            return _LastName!;
        }
        set
        {
            _LastName = value ?? throw new ArgumentNullException(nameof(value));
        }
    }
    private string? _LastName;
    public string? Address { getset; }
 
    public override string GetSummary()
    {
        return $"FirstName: { FirstName + NewLine }"
        + $"LastName: { LastName + NewLine }"
        + $"Address: { Address + NewLine }";
    }
 
    // ...
}
 
public class Appointment : PdaItem
{
    public Appointment(string name, string location,
        DateTime startDateTime, DateTime endDateTime) :
        base(name)
    {
        Location = location;
        StartDateTime = startDateTime;
        EndDateTime = endDateTime;
    }
 
    public DateTime StartDateTime { getset; }
    public DateTime EndDateTime { getset; }
    public string Location { getset; }
 
    // ...
    public override string GetSummary()
    {
        return $"Subject: { Name + NewLine }"
            + $"Start: { StartDateTime + NewLine }"
            + $"End: { EndDateTime + NewLine }"
            + $"Location: { Location }";
    }
}

Listing 7.17 defines the GetSummary() member as abstract, so it doesn’t include any implementation. The code then overrides this member within Contact and provides the implementation. Because abstract members are supposed to be overridden, such members are automatically virtual and cannot be declared so explicitly. In addition, abstract members cannot be private because derived classes would not be able to see them.

It is surprisingly difficult to develop a well-designed object hierarchy. For this reason, when programming abstract types, you should be sure to implement at least one (and preferably more) concrete type that derives from the abstract type to validate the design.

If you don’t provide a GetSummary() implementation in Contact, the compiler will report an error.

note
Abstract members must be overridden, so they are automatically virtual and cannot be declared so explicitly.
Language Contrast: C++—Pure Virtual Functions

C++ allows for the definition of abstract functions using the cryptic notation =0. These functions are called pure virtual functions in C++. In contrast to C#, however, C++ does not require the class itself to have any special declaration. Unlike C#’s abstract class modifier, C++ has no class declaration change when the class includes pure virtual functions.

note
By declaring an abstract member, the abstract class programmer states that to form an “is a” relationship between a concrete class and an abstract base class (that is, a PdaItem), it is necessary to implement the abstract members, the members for which the abstract class could not provide an appropriate default implementation.
Beginner Topic
Polymorphism

When the implementation for the same member signature varies between two or more classes, the scenario demonstrates a key object-oriented principle: polymorphism. Poly means “many” and morph means “form,” so polymorphism refers to the existence of multiple implementations of the same signature. Also, because the same signature cannot be used multiple times within a single class, each implementation of the member signature occurs on a different class.

The idea behind polymorphism is that the object itself knows best how to perform a particular operation. Moreover, by enforcing common ways to invoke those operations, polymorphism encourages code reuse when taking advantage of the commonalities. Given multiple types of documents, each document type class knows best how to perform a Print() method for its corresponding document type. Therefore, instead of defining a single print method that includes a switch statement with the special logic to print each document type, with polymorphism you call the Print() method corresponding to the specific type of document you wish to print. For example, calling Print() on a word processing document class behaves according to word processing specifics, whereas calling the same method on a graphics document class will result in print behavior specific to the graphic. Given the document types, however, all you have to do to print a document is call Print(), regardless of the type.

Moving the custom print implementation out of a switch statement offers a number of maintenance advantages. First, the implementation appears in the context of each document type’s class rather than in a location far removed from it; this is in keeping with encapsulation. Second, adding a new document type doesn’t require a change to the switch statement. Instead, all that is necessary is for the new document type class to implement the Print() signature.

Abstract members are intended to be a way to enable polymorphism. The base class specifies the signature of the method, and the derived class provides the implementation (see Listing 7.18 with Output 7.5).

Listing 7.18: Using Polymorphism to List the PdaItems
public class Program
{
    public static void Main()
    {
        PdaItem[] pda = new PdaItem[3];
 
        Contact contact = new("Sherlock Holmes")
        {
            Address = "221B Baker Street, London, England"
        };
        pda[0] = contact;
 
        Appointment appointment = new(
               "Soccer tournament""Estádio da Machava"
               new DateTime(2008, 7, 18), new DateTime(2008, 7, 19));
        pda[1] = appointment;
 
        contact = new Contact("Anne Frank")
        {
            Address = "Apt 56B, Whitehaven Mansions, Sandhurst Sq, London"
        };
        pda[2] = contact;
 
        List(pda);
    }
 
    public static void List(PdaItem[] items)
    {
        // Implemented using polymorphism. The derived
        // type knows the specifics of implementing 
        // GetSummary().
        foreach(PdaItem item in items)
        {
            Console.WriteLine("________");
            Console.WriteLine(item.GetSummary());
        }
    }
}
Output 7.5
________
FirstName: Sherlock
LastName: Holmes
Address: 221B Baker Street, London, England
________
Subject: Soccer tournament
Start: 7/18/2008 12:00:00 AM
End: 7/19/2008 12:00:00 AM
Location: Estádio da Machava
________
FirstName: Hercule
LastName: Poirot
Address: Apt 56B, Whitehaven Mansions, Sandhurst Sq, London

In this way, you can call the method on the base class, but the implementation is specific to the derived class. Output 7.5 shows that the List() method from Listing 7.18 is able to successfully display both Contacts and Addresses, and display them in a way tailored to each. The invocation of the abstract GetSummary() method actually invokes the overriding method specific to the instance.

{{ snackbarMessage }}