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.

Language Contrast: Java—Virtual Methods by Default

By default, methods in Java are virtual, and they must be explicitly sealed if nonvirtual behavior is preferred. In contrast, C# defaults to nonvirtual.

Listing 7.9 shows an example of property overriding.

Listing 7.9: Overriding a Property
1. public class PdaItem
2. {
3.     public virtual string Name { getset; }
4.     // ...
5. }
6.  
7. public class Contact : PdaItem
8. {
9.     public override string Name
10.     {
11.         get
12.         {
13.             return $"{ FirstName } { LastName }";
14.         }
15.  
16.         set
17.         {
18.             string[] names = value.Split(' ');
19.             // Error handling not shown
20.             FirstName = names[0];
21.             LastName = names[1];
22.         }
23.     }
24.  
25.     public string FirstName { getset; }
26.     public string LastName { getset; }
27.  
28.     // ...
29. }

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.

Language Contrast: Java and C++—Implicit Overriding

Unlike in Java and C++, the override keyword is required on the derived class in C#. C# does not allow implicit overriding. To override a method, both the base class and the derived class members must match and have corresponding virtual and override keywords. Furthermore, when the override keyword is specified, the derived implementation is assumed to replace the base class implementation.

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

Listing 7.10: Runtime Calling the Most Derived Implementation of a Virtual Method
1. public class Program
2. {
3.     public static void Main()
4.     {
5.         Contact contact;
6.         PdaItem item;
7.  
8.         contact = new Contact();
9.         item = contact;
10.  
11.         // Set the name via PdaItem variable
12.         item.Name = "Inigo Montoya";
13.  
14.         // Display that FirstName & LastName
15.         // properties were set
16.         Console.WriteLine(
17.             $"{ contact.FirstName } { contact.LastName}");
18.     }
19. }
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.

Language Contrast: C++—Dispatch Method Calls during Construction

In C++, methods called during construction will not dispatch the virtual method. Instead, during construction, the type is associated with the base type rather than the derived type, and virtual methods call the base implementation. In contrast, C# dispatches virtual method calls to the most derived type. This is consistent with the principle of calling the most derived virtual member, even if the derived constructor has not completely executed. Regardless, in C#, the situation should be avoided.

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.

Covariant Return Types

Generally, the signature of the overriding method must match the signature of the base method being overridden. However, starting in C# 9.0, an improvement was made that allowed the overriding method to specify a return type different from the base method if the return type was compatible with the base method’s return type. The feature is called covariant return types. Listing 7.11 provides an example.

Listing 7.11: Covariant Return Types
1. public class Base
2. {
3.     public virtual Base Create() => new();
4. }
5.  
6. public class Derived : Base
7. {
8.     public override Derived Create() => new();
9. }

Notice how the Base class’s Create() method returns Base while the Derived class’s returns Derived. Even though the latter’s signature overrides the former, the return types may be different if the latter is a derived type of the former’s return type. Covariance will be discussed more in Chapter 12.

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];

       }

   }

}

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.12 as an example. Its output appears in Output 7.4.

Listing 7.12: override versus new Modifier
1. public class Program
2. {
3.     public class BaseClass
4.     {
5.         public static void DisplayName()
6.         {
7.             Console.WriteLine("BaseClass");
8.         }
9.     }
10.  
11.     public class DerivedClass : BaseClass
12.     {
13.         // Compiler WARNING: DisplayName() hides inherited 
14.         // member. Use the new keyword if hiding was intended
15.         public virtual void DisplayName()
16.         {
17.             Console.WriteLine("DerivedClass");
18.         }
19.     }
20.  
21.     public class SubDerivedClass : DerivedClass
22.     {
23.         public override void DisplayName()
24.         {
25.             Console.WriteLine("SubDerivedClass");
26.         }
27.     }
28.  
29.     public class SuperSubDerivedClass : SubDerivedClass
30.     {
31.         public static new void DisplayName()
32.         {
33.             Console.WriteLine("SuperSubDerivedClass");
34.         }
35.     }
36.  
37.     public static void Main()
38.     {
39.         SuperSubDerivedClass superSubDerivedClass = new();
40.  
41.         SubDerivedClass subDerivedClass = superSubDerivedClass;
42.         DerivedClass derivedClass = superSubDerivedClass;
43.         BaseClass baseClass = superSubDerivedClass;
44.  
45.         SuperSubDerivedClass.DisplayName();
46.         subDerivedClass.DisplayName();
47.         derivedClass.DisplayName();
48.         BaseClass.DisplayName();
49.     }
50. }
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.13). 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.13: Sealing Members
1. class A
2. {
3.     public virtual void Method()
4.     {
5.     }
6. }
7.  
8. class B : A
9. {
10.     public sealed override void Method()
11.     {
12.     }
13. }
14.  
15. class C : B
16. {
17.     // ERROR:  Cannot override sealed members
18.     //public override void Method()
19.     //{
20.     //}
21. }

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

Listing 7.14: Accessing a Base Member
1. using static System.Environment;
2.  
3. public class Address
4. {
5.     public string StreetAddress;
6.     public string City;
7.     public string State;
8.     public string Zip;
9.  
10.     public override string ToString()
11.     {
12.         return $"{ StreetAddress + NewLine }"
13.             + $"{ City }{ State }  { Zip }";
14.     }
15. }
16.  
17. public class InternationalAddress : Address
18. {
19.     public string Country;
20.  
21.     public override string ToString()
22.     {
23.         return base.ToString() +
24.             NewLine + Country;
25.     }
26. }

In Listing 7.14, 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 (non-private) 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.15).

Listing 7.15: Specifying Which Base Constructor to Invoke
1. public class PdaItem
2. {
3.     public PdaItem(string name)
4.     {
5.         Name = name;
6.     }
7.     public virtual string Name { getset; }
8.     // ...
9. }
10. public class Contact : PdaItem
11. {
12.     // Disable warning since FirstName&LastName set via Name property
13.     // Non-nullable field is uninitialized.
14.     #pragma warning disable CS8618
15.     public Contact(string name) :
16.         base(name)
17.     {
18.     }
19.     #pragma warning restore CS8618
20.  
21.     public override string Name
22.     {
23.         get
24.         {
25.             return $"{ FirstName } { LastName }";
26.         }
27.  
28.         set
29.         {
30.             string[] names = value.Split(' ');
31.             // Error handling not shown
32.             FirstName = names[0];
33.             LastName = names[1];
34.         }
35.     }
36.  
37.     [NotNull] [DisallowNull]
38.     public string FirstName { getset; }
39.     [NotNull] [DisallowNull]
40.     public string LastName { getset; }
41.  
42.     // ...
43. }
44.  
45. public class Appointment : PdaItem
46. {
47.     public Appointment(string name, string location,
48.         DateTime startDateTime, DateTime endDateTime) :
49.         base(name)
50.     {
51.         Location = location;
52.         StartDateTime = startDateTime;
53.         EndDateTime = endDateTime;
54.     }
55.  
56.     public DateTime StartDateTime { getset; }
57.     public DateTime EndDateTime { getset; }
58.     public string Location { getset; }
59.  
60.     // ...
61. }

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

{{ snackbarMessage }}
;