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.

Inheritance provides five various properties namely: derivation; overriding; abstract classes; system.object; and pattern matching. Derivation includes casting, protected, single inheritance, and sealed classes. The overriding consists of virtual, new, and sealed.

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.

A figure shows the process of refactoring into a base class.

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 { get; set; }

  public DateTime LastUpdated { get; set; }
}
// Define the Contact class as inheriting the PdaItem class
public class Contact : PdaItem                                            
{
  public string Address { get; set; }
  public string Phone { get; set; }
}

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();
      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 Contact();
      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.

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 at code locations 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 Contact();

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

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

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. 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(contact.ObjectKey);                                                

  static public Contact Copy(PdaItem pdaItem) =>
      // Error: Cannot access protected member PdaItem.ObjectKey.                    
      new Contact(pdaItem.ObjectKey);                                                
}

public class Program
{
    public static void Main()
    {
        Contact contact = new Contact(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 outside of PdaItem, but only 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(PdaItempdaItem) 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.

Begin 3.0

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.

End 3.0

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.

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.

Overriding the Base Class

All members of a base class are inherited in the derived class, except for constructors and destructors. However, sometimes the base class does not have the optimal implementation of a particular member. Consider the Name property on PdaItem, for example. The implementation is probably acceptable when inherited by the Appointment class. For the Contact class, however, the Name property should return the FirstName and LastName properties combined. Similarly, when Name is assigned, it should be split across FirstName and LastName. In other words, the base class property declaration is appropriate for the derived class, but the implementation is not always valid. A mechanism is needed for overriding the base class implementation with a custom implementation in the derived class.

virtual Modifier

C# supports overriding on instance methods and properties but not on fields or on any static members. It requires an explicit action within both the base class and the derived class. The base class must mark each member for which it allows overriding as virtual. If public or protected members do not include the virtual modifier, subclasses will not be able to override those members.

Listing 7.9 shows an example of property overriding.

Listing 7.9: Overriding a Property

public class PdaItem
{
    public virtual string Name { get; set; }                            
    // ...
}

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; set; }
    public string LastName { get; set; }

    // ...
}

Not only does PdaItem include the virtual modifier on the Name property, but Contact’s Name property is also decorated with the keyword override. Eliminating virtual would result in an error and omitting override would cause a warning to be generated, as you will see shortly. C# requires the overriding methods to use the override keyword explicitly. In other words, virtual identifies a method or property as available for replacement (overriding) in the derived type.

Overriding a member causes the runtime to call the most derived implementation (see Listing 7.10).

Listing 7.10: Runtime Calling the Most Derived Implementation of a Virtual Method

public class Program
{
  public static void Main()
  {
        Contact contact;
        PdaItem item;

        contact = new Contact();
        item = contact;

        // Set the name via PdaItem variable
        item.Name = "Inigo Montoya";

        // Display that FirstName & LastName
        // properties were set
        Console.WriteLine(
            $"{ contact.FirstName } { contact.LastName }");
}

Output 7.1 shows the results of Listing 7.10.

Output 7.1

Inigo Montoya

In Listing 7.10, when item.Name, which is declared on the PdaItem, is assigned, the contact’s FirstName and LastName are still set. The rule is that whenever the runtime encounters a virtual method, it calls the most derived and overriding implementation of the virtual member. In this case, the code instantiates a Contact and calls Contact.Name because Contact contains the most derived implementation of Name.

Virtual methods provide default implementations only—that is, implementations that derived classes could override entirely. However, because of the complexities of inheritance design, it is important to consider (and preferably to implement) a specific scenario that requires the virtual method definition rather than to declare members as virtual by default.

This step is also important because converting a method from a virtual method to a nonvirtual method could break derived classes that override the method. Once a virtual member is published, it should remain virtual if you want to avoid introducing a breaking change. So be careful when introducing a virtual member—perhaps making it private protected, for example.

Finally, only instance members can be virtual. The CLR uses the concrete type, specified at instantiation time, to determine where to dispatch a virtual method call; thus static virtual methods are meaningless and the compiler prohibits them.

new Modifier

When an overriding method does not use override, the compiler issues a warning similar to that shown in Output 7.2 or Output 7.3.

Output 7.2

warning CS0114: '<derived method name>' hides inherited member
'<base method name>'. To make the current member override that
implementation, add the override keyword. Otherwise add the new
keyword.

Output 7.3

warning CS0108: The keyword new is required on '<derived property
name>' because it hides inherited member '<base property name>'

The obvious solution is to add the override modifier (assuming the base member is virtual). However, as the warnings point out, the new modifier is also an option. Consider the scenario shown in Table 7.1—a specific example of the more general case known as the brittle or fragile base class problem.

Table 7.1: Why the New Modifier?

Activity

Code

Programmer A defines class Person that includes properties FirstName and LastName.

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Programmer B derives from Person and defines Contact with the additional property Name. In addition, he defines the Program class whose Main() method instantiates Contact, assigns Name, and then prints out the name.

public class Contact : Person
{
  public string Name
  {
      get
      {
          return FirstName + " " + LastName;
      }
      set
      {
          string[] names = value.Split(' ');
          // Error handling not shown
          FirstName = names[0];
          LastName = names[1];
      }
  }
}

Later, Programmer A adds the Name property, but instead of implementing the getter as FirstName + " " + LastName, she implements it as LastName + ", " + FirstName. Furthermore, she doesn’t define the property as virtual, and she uses the property in a DisplayName() method.

// ...
public class Person
{
  public string Name
  {
      get
      {
          return LastName + ", " + FirstName;
      }
      set
      {
          string[] names = value.Split(", ");
          // Error handling not shown
          LastName = names[0];
          FirstName = names[1];
      }
  }
  public static void Display(Person person)
  {
      // Display <LastName>, <FirstName>
      Console.WriteLine( person.Name );
  }
}

 

Because Person.Name is not virtual, Programmer A expects Display() to use the Person implementation, even if a Person-derived data type, Contact, is passed in. However, Programmer B expects Contact.Name to be used in all cases where the variable data type is a Contact. (Programmer B has no code where Person.Name was used, since no Person.Name property existed initially.) To allow the addition of Person.Name without breaking either programmer’s expected behavior, you cannot assume virtual was intended. Furthermore, because C# requires an override member to explicitly use the override modifier, some other semantic must be assumed instead of allowing the addition of a member in the base class to cause the derived class to no longer compile.

This semantic is the new modifier, which hides a redeclared member of the derived class from the base class. Instead of calling the most derived member, a member of the base class calls the most derived member in the inheritance chain prior to the member with the new modifier. If the inheritance chain contains only two classes, a member in the base class will behave as though no method was declared on the derived class (if the derived implementation overrides the base class member). Although the compiler will report the warning shown in either Output 7.2 or Output 7.3, if neither override nor new is specified, new will be assumed, thereby maintaining the desired version safety.

Consider Listing 7.11 as an example. Its output appears in Output 7.4.

Listing 7.11: override versus new Modifier

public class Program
{
  public class BaseClass
  {
      public void DisplayName()
      {
          Console.WriteLine("BaseClass");
      }
  }

    public class DerivedClass : BaseClass
    {
        // Compiler WARNING: DisplayName() hides inherited
        // member. Use the new keyword if hiding was intended.
        public virtual void DisplayName()
        {
            Console.WriteLine("DerivedClass");
        }
    }
  public class SubDerivedClass : DerivedClass
  {
      public override void DisplayName()
      {
          Console.WriteLine("SubDerivedClass");
      }
  }

  public class SuperSubDerivedClass : SubDerivedClass
  {
      public new void DisplayName()
      {
          Console.WriteLine("SuperSubDerivedClass");
      }
  }
  public static void Main()
  {
      SuperSubDerivedClass superSubDerivedClass
          = new SuperSubDerivedClass();

      SubDerivedClass subDerivedClass = superSubDerivedClass;
      DerivedClass derivedClass = superSubDerivedClass;
      BaseClass baseClass = superSubDerivedClass;

      superSubDerivedClass.DisplayName();
      subDerivedClass.DisplayName();
      derivedClass.DisplayName();
      baseClass.DisplayName();
  }
}

Output 7.4

SuperSubDerivedClass
SubDerivedClass
SubDerivedClass
BaseClass

These results occur for the following reasons:

  • SuperSubDerivedClass: SuperSubDerivedClass.DisplayName() displays SuperSubDerivedClass because there is no derived class and therefore no override.

  • SubDerivedClass: SubDerivedClass.DisplayName() is the most derived member to override a base class’s virtual member. SuperSubDerivedClass.DisplayName() is hidden because of its new modifier.

  • SubDerivedClass: DerivedClass.DisplayName() is virtual and SubDerivedClass.DisplayName() is the most derived member to override it. As before, SuperSubDerivedClass.DisplayName() is hidden because of the new modifier.

  • BaseClass: BaseClass.DisplayName() does not redeclare any base class member and it is not virtual; therefore, it is called directly.

When it comes to the CIL, the new modifier has no effect on which statements the compiler generates. However, a “new” method results in the generation of the newslot metadata attribute on the method. From the C# perspective, its only effect is to remove the compiler warning that would appear otherwise.

sealed Modifier

Just as you can prevent inheritance using the sealed modifier on a class, so virtual members may be sealed as well (see Listing 7.12). This approach prevents a subclass from overriding a base class member that was originally declared as virtual higher in the inheritance chain. Such a situation arises when a subclass B overrides a base class A’s member and then needs to prevent any further overriding below subclass B.

Listing 7.12: Sealing Members

class A
{
  public virtual void Method()
  {
  }
}
class B : A
{
  public override sealed void Method()
  {
  }
}

class C : B
{
  // ERROR:  Cannot override sealed members
  // public override void Method()
  // {
  // }
}

In this example, the use of the sealed modifier on class B’s Method() declaration prevents class C from overriding Method().

In general, marking a class as sealed is rarely done and should be reserved only for those situations in which there are strong reasons favoring such a restriction. In fact, leaving types unsealed has become increasingly desirable as unit testing has assumed greater prominence, because of the need to support mock (test double) object creation in place of real implementations. One possible scenario in which sealing a class might be warranted is when the cost of sealing individual virtual members outweighs the benefits of leaving the class unsealed. However, a more targeted sealing of individual members—perhaps because of dependencies in the base implementation that are necessary for correct behavior—is likely to be preferable.

base Member

In choosing to override a member, developers often want to invoke the member on the base class (see Listing 7.13).

Listing 7.13: Accessing a Base Member

using static System.Environment;

public class Address
{
    public string StreetAddress;
    public string City;
    public string State;
    public string Zip;

    public override string ToString()
    {
        return $"{ StreetAddress + NewLine }"
            + $"{ City }, { State }  { Zip }";
    }
}

public class InternationalAddress : Address
{

    public string Country;

    public override string ToString()

    {
        return base.ToString() +
            NewLine + Country;
    }
}

In Listing 7.13, InternationalAddress inherits from Address and implements ToString(). To call the parent class’s implementation, you use the base keyword. The syntax is virtually identical to the use of the this keyword, including support for using base as part of the constructor (discussed shortly).

Parenthetically, in the Address.ToString() implementation, you are required to override because ToString() is also a member of object. Any members that are decorated with override are automatically designated as virtual, so additional child classes may further specialize the implementation.

Note

Any methods decorated with override are automatically virtual. A base class method can be overridden only if it is virtual, and the overriding method is therefore virtual as well.

Invoking Base Class Constructors

When instantiating a derived class, the runtime first invokes the base class’s constructor so that the base class initialization is not circumvented. However, if there is no accessible (nonprivate) default constructor on the base class, it is not clear how to construct the base class; in turn, the C# compiler reports an error.

To avoid the error caused by the lack of an accessible default constructor, programmers need to designate explicitly, in the derived class constructor header, which base constructor to run (see Listing 7.14).

Listing 7.14: Specifying Which Base Constructor to Invoke

public class PdaItem
{
  public PdaItem(string name)
  {
      Name = name;
  }
  public virtual string Name { get; set; }
  // ...
}
public class Contact : PdaItem
{
  // Disable warning since FirstName&LastName set via Name property
  #pragma warning disable CS8618 // Non-nullable field is uninitialized.
  public Contact(string name) :                                   
      base(name)                                                  
  {
  }
  #pragma warning restore CS8618

  public override string Name
  {
      get
      {
          return $"{ FirstName } { LastName }";
      }
      set
      {
          string[] names = value.Split(' ');
          // Error handling not shown
          FirstName = names[0];
          LastName = names[1];
      }
  }

  [NotNull][DisallowNull]
  public string FirstName { get; set; }
  [NotNull][DisallowNull]
  public string LastName { get; set; }
  // ...
}

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 { get; set; }
  public DateTime EndDateTime { get; set; }
  public string Location { get; set; }

  // ...
}

By identifying the base constructor in the code, you let the runtime know which base constructor to invoke before invoking the derived class constructor.

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.

Begin 8.0

Abstract classes are a fundamental object-oriented principle, so we describe them here accordingly. However, starting with C# 8.0 and .NET Core 3.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.

End 8.0

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

Listing 7.15: Defining an Abstract Class

// Define an abstract class
public abstract class PdaItem                                                 
{
  public PdaItem(string name)
  {
      Name = name;
  }

  public virtual string Name { get; set; }
}
public class Program
{
  public static void Main()
  {
      PdaItem item;
      // ERROR:  Cannot create an instance of the abstract class
      // item = new PdaItem("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.16 as an example.

Listing 7.16: Defining Abstract Members

// Define an abstract class
public abstract class PdaItem
{
  public PdaItem(string name)
  {
      Name = name;
  }

  public virtual string Name { get; set; }
  public abstract string GetSummary();                                  
}
using static System.Environment;

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 { get; set; }

  public override string GetSummary()                                             
  {                                                                               
      return @"FirstName: { FirstName + NewLine }"                                
          + $"LastName: { LastName + NewLine }"                                   
          + $"Address: { Address + NewLine }";                                    
  }                                                                               

  // ...
)

public class Appointment : PdaItem
{
  public Appointment(string name) :
}
    base(name)
  {
      Location = location;
      StartDateTime = startDateTime;
      EndDateTime = endDateTime;
  }
  public DateTime StartDateTime  { get; set; }
  public DateTime EndDateTime  { get; set; }
  public string Location  { get; set; }

  // ...

  public override string GetSummary()
  {
        return $"Subject: { Name + NewLine }"
            + $"Start: { StartDateTime + NewLine }"
            + $"End: { EndDateTime + NewLine }"
            + $"Location: { Location }";
  }
}

Listing 7.16 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 provide no 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.

Note

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.

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

Listing 7.17: Using Polymorphism to List the PdaItems

public class Program
{
  public static void Main()
  {
      PdaItem[] pda = new PdaItem[3];

      Contact contact = new Contact("Sherlock Holmes");
      {
          Address = "221B Baker Street, London, England";
      }
      pda[0] = contact;

      new Appointment(
          "Soccer tournament", "Estádio da Machava",
          new DateTime(2008, 7, 19), new DateTime(2008, 7, 18));
      pda[1] = appointment;

      contact = new Contact("Hercule Poirot");
      contact.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());
      }
  }
}

The results of Listing 7.17 appear in Output 7.5.

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

All Classes Derive from System.Object

Given any class, whether a custom class or one built into the system, the methods shown in Table 7.2 will be defined.

Table 7.2: Members of System.Object

Method Name

Description

public virtual bool Equals(object o)

Returns true if the object supplied as a parameter is equal in value, not necessarily in reference, to the instance.

public virtual int GetHashCode()

Returns an integer corresponding to an evenly spread hash code. This is useful for collections such as HashTable collections.

public Type GetType()

Returns an object of type System.Type corresponding to the type of the object instance.

public static bool ReferenceEquals(
    object a, object b)

Returns true if the two supplied parameters refer to the same object.

public virtual string ToString()

Returns a string representation of the object instance.

public virtual void Finalize()

An alias for the destructor; informs the object to prepare for termination. C# prevents you from calling this method directly.

protected object MemberwiseClone()

Clones the object in question by performing a shallow copy; references are copied, but not the data within a referenced type.

 

 

All of the methods listed in Table 7.2 appear on all objects through inheritance; all classes derive (either directly or via an inheritance chain) from object. Even literals include these methods, enabling somewhat peculiar-looking code such as this:

Console.WriteLine( 42.ToString() );

Even class definitions that don’t have any explicit derivation from object derive from object anyway. The two declarations for PdaItem in Listing 7.18, therefore, result in identical CIL.

Listing 7.18: System.Object Derivation Implied When No Derivation Is Specified Explicitly

public class PdaItem
{
  // ...
}
public class PdaItem : object
{
  // ...
}

When the object’s default implementation isn’t sufficient, programmers can override one or more of the three virtual methods. Chapter 10 describes the details involved in doing so.

Pattern Matching with the is Operator

While C# has had an is operator since C# 1.0, both C# 7.0 and C# 8.0 provide significant improvements in the support for pattern matching. Note, however, that many of these features are relatively minor until used in the context of a switch statement or expression, as we discuss in the next section.

Verifying the Underlying Type with the is Operator

Since C# allows casting down the inheritance chain, it is sometimes desirable to determine what the underlying type is before attempting a conversion. Also, checking the type may be necessary for type-specific actions where polymorphism was not implemented. To determine the underlying type, C# has included an is operator since C# 1.0 (see Listing 7.19).

Listing 7.19: is Operator Determining the Underlying Type

public static void Save(object data)
{
  if (data is string)
  {
      string text = (string)data;
      if (text.Length > 0)
      {
          data = Encrypt(text);
          // ...
      }
  }
  else if (data is null)
  {
      // ...
  }
  // ...
}

Listing 7.19 encrypts the data if the underlying type is a string. This is significantly different from encrypting any data type that successfully converts to a string, since some types support conversion to a string, yet their underlying type is not a string.

While it might be clearer to check for null at the start of the method, in this case, we check later. Our intention is to demonstrate that even if the target is null, the is operator will return false, so the null check will still execute.

Begin 7.0

With an explicit cast, it is the programmer’s responsibility to understand the code logic sufficiently to avoid an invalid cast exception. If an invalid cast might potentially occur, it would be preferable to leverage an is operator and avoid the exception entirely. The advantage is that the is operator enables a code path for when the explicit cast might fail without the expense of exception handling. Furthermore, in C# 7.0 and later, the is operator will make the assignment in addition to checking for data type.

Type, Var, and Constant Pattern Matching

Starting with C# 7.0, the is operator has been improved to support pattern matching. The problem with the is operator, as described previously in the chapter, is that after checking that data is indeed a string, we still must cast the data to a string (assuming we want to access it as a string). A preferable approach would be to both check and, if the check is true, assign the result to a new variable. With C# 7.0’s introduction of pattern matching, this becomes possible using type, var, and const pattern matching. C# 8.0 then builds on this with the addition of tuple, positional, property, and recursive pattern matching. All of these options can replace the more basic is operator in most scenarios.

Table 7.3 provides an example of each of the C# 7.0 pattern matching capabilities.

Table 7.3: Type, var, and const Pattern Matching with the is Operator

Description

Method Name

Type Pattern Matching

The result from GetObjectById(id) is checked against the Employee type and assigned to the variable employee in the same expression. If the result of GetObjectById(id) is null or not an Employee, then false is produced and the else clause executes.

The employee variable is available within and after the if statement; however, it would need to be assigned a value before accessing it inside or after the else clause.

    // ...
string id =
  "92e80a67-d453-4998-8d85-f430fa02d6c7";
if(GetObjectById(id) is Employee employee)
{
  Display(employee);
}
else
{
  ReportError($"Employee id, {id} is invalid.")
}

Constant Pattern Matching

In Chapter 4, we demonstrated constant pattern matching when using the is operator to check for null (i.e., data is null). The same check is supported against any constant. You could, for example, compare data against an empty string with data is "". However, the comparison must be against a constant. data is string.Empty, a property rather than a constant, is not valid.

public static void Save(object data)
{
  // ...
  else if (data is "")
  {
      return;
      // ...
  }
}

Pattern Matching with var

Unlike in type pattern matching, you can use a var for the data type to capture any value, including null. Of course, the benefit of this approach is questionable over simply assigning var result = GetObjectById(id) because it always succeeds.

Where is becomes more useful, however, is in switch statements, where it serves as a catch-all case with declaration.

    // ...
else (GetObjectById(id) is var result)
{
    // ...
}

 

End 7.0
Begin 8.0

Pattern matching gets slightly more complex in C# 8.0, where the language includes support for tuple, positional, property, and recursive pattern matching.

Tuple Pattern Matching

With tuple pattern matching, you can check for constant values within the tuple or assign tuple items to a variable (see Listing 7.20).

Listing 7.20: Tuple Pattern Matching with the is Operator

public class Program
{
    const int Action = 0;
    const int FileName = 1;
    public const string DataFile = "data.dat";
    static public void Main(params string[] args)
    {
        // ...

        if ((args.Length, args[Action]) is (1, "show"))                                    
        {
            Console.WriteLine(File.ReadAllText(DataFile));
        }
        else if ((args.Length, args[Action].ToLower(), args[FileName]) is                  
            (2, "encrypt", string fileName))                                               
        {
            string data = File.ReadAllText(DataFile);
            File.WriteAllText(fileName, Encrypt(data).ToString());
        }
        // ...
    }
}

In this example, we pattern match against a tuple that is populated with the length and the elements of args. In the first if condition, we check for one argument and the action "show". In the second if condition, we evaluate whether the first item in the array is equal to "encrypt", and if it is, then we assign the third element in the tuple to the variable fileName. Each element match can be a constant or a variable. Since the tuple is instantiated before the is operator executes, we can’t use the "encrypt" scenario first because args[FileName] would not be a valid index if the "show" action was requested.

Positional Pattern Matching

Building on the deconstructor construct introduced in C# 7.0 (see Chapter 6), C# 8.0 enables positional pattern matching with a syntax that closely matches tuple pattern matching (see Listing 7.21).

Listing 7.21: : Positional Pattern Matching with the is Operator

public class Person
{
  // ...

  public void Deconstruct(out string firstName, out string lastName) =>                               
      (firstName, lastName) = (FirstName, LastName);                                                  
}

public class Program
{
  static public void Main(string[] args)
  {
      Person person = new Person("Inigo", "Montoya");
      // Positional Pattern Matching
      if(person is (string firstName, string lastName))                                                
      {
          Console.WriteLine($"{firstName} {lastName}");
      }
  }
}

In this example, there are no const elements. Instead, all items from the deconstructor are assigned to variables within the newly constructed tuple. Checking for constant values within the tuple is allowed.

Property Pattern Matching

With property patterns, you can switch match expressions based on property names and values of the data type identified in the switch expression, as shown in Listing 7.22.

Listing 7.22: Property Pattern Matching with the is Operator

// ...
Person person = new Person("", "");

// Positional pattern matching
if(person is {FirstName: string firstName, LastName: string lastName })          
{
    Console.WriteLine($"{firstName} {lastName}");
}
// ...

At a glance, Listing 7.22 looks almost identical to Listing 7.21 and appears similar to positional pattern matching. However, it has two important differences. First, property pattern matching uses curly braces rather than the parentheses used to identify what to match against. Second, the positions of the arguments (which are important to both positional and tuple matching) are irrelevant with property pattern matching because the property names are used to map the match expression. Also note that property pattern matching is used when checking for not null with is { }.

End 8.0

Recursive Pattern Matching

As mentioned earlier, much of the power of property matching doesn’t really emerge until it is leveraged within a switch statement or expression. The one exception, however, might be when pattern matching is used recursively. Admittedly, Listing 7.23 is nonsensical; it provides an example of the potential complexity when applying patterns recursively.

Listing 7.23: Recursive Pattern Matching with the is Operator

// ...
Person inigo = new Person("Inigo", "Montoya");
var buttercup =
    (FirstName: "Princess", LastName: "Buttercup");

(Person inigo, (string FirstName, string LastName) buttercup) couple =
  (inigo, buttercup);

if (couple is
    ( // Tuple
        ( // Positional
            { // Property
                Length: int inigoLength1 },
            _ // Discard
        ),
    { // Property
        FirstName: string buttercupFirstName }))
{
    Console.WriteLine($"({inigoFirstNameLength}, {buttercupFirstName})");
}
else
{
  // ...
}
// ...

In this example, couple is of the following type:

(Person, (string FirstName, string LastName))

As such, the first match occurs on the outer tuple, (inigo, buttercup). Next, positional pattern matching is used against inigo, leveraging the Person deconstructor. This selects a (FirstName, LastName) tuple, from which property pattern matching is used to extract the Length of the inigo.FirstName value. The LastName portion of the positional pattern matching is discarded with an underscore. Finally, property pattern matching is used to select buttercup.LastName.

The property matching construct of C# 8.0 is a powerful means to select data, but is not without limitations. Unlike with the switch statement (described in Chapter 4) and the inclusion of the when clause, you cannot, for example, pattern match on a predicate expression—such as checking that FirstName and LastName lengths are greater than zero. Also, be mindful of readability. Even with the comments in Listing 7.23, it is likely challenging to understand the code. Without them, it would be even harder, as shown here:

if (couple is ( ( { Length: int inigoFirstNameLength }, _ ),
   { FirstName: string buttercupFirstName })) { ...}

Even so, where pattern matching really proves useful is in switch statements and expressions.

Begin 7.0

Pattern Matching within a switch Expression

Listing 7.23 is an if-else statement, but you can imagine a similar example in which we check for more than just a string. And, while an if statement would work, a switch statement with a match expression (or statement) in which the match expression works with a base type can provide better readability. Listing 7.24 provides an example that formats a date in various forms.

Listing 7.24: Pattern Matching within a switch Expression

public static string? CompositeFormatDate(
        object input, string compositFormatString) =>
    input switch
    {
        DateTime { Year: int year, Month: int month, Day: int day }
            => (year, month, day),
        DateTimeOffset
        { Year: int year, Month: int month, Day: int day }
                => (year, month, day),
        string dateText => DateTime.TryParse(
            dateText, out DateTime dateTime) ?
                (dateTime.Year, dateTime.Month, dateTime.Day) :
                default((int Year, int Month, int Day)?),
        _ => null
    } is { } date ? string.Format(
        compositFormatString, date.Year, date.Month, date.Day) : null;

The first case of the switch expression uses type pattern matching (C# 7.0) to check whether the input is of type DateTime. If the result is true, it passes the result to the property pattern matching to declare and assign the values year, month, and day; it then uses those variables in a tuple expression that returns the tuple (year, month, day). The DateTimeOffset case works the same way.

The case string does not use recursive pattern matching, nor does the default (_). Note that with string, if TryParse() is unsuccessful, we return a default((int Year, int Month, int Day)?),2 which evaluates to null. It is not possible to simply return null because there is no implicit conversion from (int Year, int Month, int Day) (the type returned by the other cases) and null. Rather, a nullable tuple needs to be specified to accurately determine the switch expression’s type. (The alternative to using the default operator would be to cast: ((int Year, int Month, int Day)?) null.) Additionally, nullability is important so that input switch {} is { } date doesn’t return true when parsing is unsuccessful.

2. See Chapter 12 for more information.

Note that Listing 7.24 does not include any when clauses with predicate expressions that further limit a match. Clauses, such as in

DateTime
  { Year: int year, Month: int month, Day: int day } tempDate
      when tempDate < DateTime.Now => (year, month, day)

(which restricts dates to be in the future), are fully supported.

Avoid Pattern Matching When Polymorphism Is Possible

Although the pattern matching capability is important, you should consider issues related to polymorphism prior to using the is operator. Polymorphism supports the expansion of a behavior to other data types without requiring any modification of the implementation that defines the behavior. For example, placing a member like Name in the base class PdaItem and then working with values derived from PdaItem is preferable to using pattern matching with case statements for each type. The former allows adding an additional type that derives from PdaItem (potentially even in a different assembly) without recompiling. In contrast, the latter requires additionally modifying the pattern matching code to address a newly introduced type. Regardless, polymorphism is not always possible.

End 7.0
End 8.0

One scenario where polymorphism fails is when no object hierarchy matches your goals—if you are working with classes that are part of unrelated systems, for example. Furthermore, it is assumed the code requiring polymorphism is out of your control and can’t be modified. Working with the dates in Listing 7.24 is one such example. A second scenario is when functionality you’re adding isn’t part of the core abstraction for these classes. For example, the toll paid by a vehicle changes for different types of vehicles traveling on a toll road, but the toll isn’t a core function of the vehicle.

Summary

This chapter discussed how to specialize a class by deriving from it and adding additional methods and properties. This coverage included a discussion of the private and protected access modifiers that control the level of encapsulation.

The chapter also investigated the details of overriding the base class implementation and, alternatively, hiding it using the new modifier. To control overriding, C# provides the virtual modifier, which identifies to the deriving class developer which members she intends for derivation. To prevent any derivation, the sealed modifier may be used on the class. Similarly, placing the sealed modifier on a member prevents further overriding from subclasses.

This chapter briefly discussed how all types derive from object. Chapter 10 discusses this derivation in more depth, looking at how object includes three virtual methods with specific rules and guidelines that govern overloading. Before you get there, however, you need to consider another programming paradigm that builds on object-oriented programming: interfaces. This is the subject of Chapter 8.

The chapter ended with an examination of pattern matching with both the is operator and a switch expression/statement. Both C# 7.0 and C# 8.0 considerably expanded these capabilities, though they are not necessarily commonly used—especially since leveraging polymorphism is a preferable solution to pattern matching, when possible.

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

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