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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
These results occur for the following reasons:
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.
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.
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.
In choosing to override a member, developers often want to invoke the member on the base class (see Listing 7.14).
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.
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).
By identifying the base constructor in the code, you let the runtime know which base constructor to invoke before invoking the derived class constructor.