It is reasonably common to want to extend a given type to add functionality, 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 occur. Given, for example, a Contact class within a Personal Digital Assistant (PDA), you decide you also can 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 refactor the Contact class. Specifically, you move the common methods and properties on Contact into a base class called PdaItem from which both Contact and Appointment derive, as shown in Figure 6.1. Figure 6.1. Refactoring into a Base ClassThe common items in this case are DateTimeCreated, DateTimeLastUpdated, Name, ObjectKey, and the like. Through derivation, the methods defined on the base class, PdaItem, are accessible from all subclasses of PdaItem. When defining a derived class, follow the class identifier with a colon and then the base class, as Listing 6.1 demonstrates. Listing 6.1. Deriving One Class from Another
Listing 6.2 shows how to access the properties defined in Contact. Listing 6.2. Using Inherited Methods
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 exposed members of its base class inheritance chain combined (see Listing 6.3). Listing 6.3. Classes Deriving from Each Other to Form an Inheritance Chain
In other words, although Customer doesn't derive from PdaItem directly, it still inherits the members of PdaItem. In Listing 6.3, PdaItem is shown explicitly to derive from object. Although C# allows such syntax, it is unnecessary because all objects that don't have some other derivation will derive from object, regardless of whether it is specified. Casting between Base and Derived TypesAs Listing 6.4 shows, because derivation forms an "is a" relationship, a derived type can always be directly assigned to a base type. Listing 6.4. Implicit Base Type Casting
The derived type, Contact, is a PdaItem and can be assigned directly to a PdaItem. This is known as an implicit cast because no specific operator is required and the conversion will, on principal, always succeed; 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 undefined, classderived type. Therefore, casting from the base type to the derived type requires an explicit cast, which at runtime could fail. To perform an explicit cast, identify the target type within parentheses prior to the original reference, as Listing 6.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 as long as 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 CLR will still verify the explicit cast at runtime, throwing an exception if in fact the base type is not of the targeted type. The C# compiler allows the cast operator even when the type hierarchy allows an implicit cast. For example, the assignment from contact to item could use a cast operator as follows: item = (Contact)contact; or even when no cast is necessary: contact = (Contact)contact; Although casting between Contact and PdaItem is fully supported, casting in either direction between arrays of these types is not supported. In other words, the following, known as covariance, will not compile: PdaItem[] pdaItems = (PdaItem[])new Contact[42]; The reverse cast, contravariance, is not supported either. Support for Parameter Covariance and ContravarianceC#'s inheritance implementation doesn't support either covariance or contravariance. Covariance allows overriding a member by supplying a method in the derived class to be a more specialized type. For example, covariance would allow the method on a derived class, void Draw(Widget part), to override the base class, perhaps an abstract member, void Draw(Part part), where Widget derives from Part. The problem with that covariance is that the base class allowed for any Part to be drawn, but the derived type allows for only the drawing of type Widget. Therefore, does the derived type handle the drawing of Part's? Presumably it should, but not via the implementation of void Draw(Widget part), because it handles only Widget. Effectively, therefore, void Draw(Widget part) does not override void Draw(Part part), but instead, it overloads it, providing a second method with the same name. Covariance is especially questionable when overriding abstract members. An abstract member requires that the derived class implements a specific signature, and if the derived class specializes the signature, narrowing what is actually supported, then the full functionality required by the abstract method will be lacking. Contravariance occurs when the derived type's members have broader signatures than the base type's signature. For example, the base class could have a method, void Add(Widget widget), and the derived class would override the method with void Add(Part part). This would allow addition of a Part to the class via the derived method, but since the base class always expects a Widget, it may call into the added item as though it was a Widget and fail. Keeping with the strong type semantics of C#, neither covariance nor contravariance is supported.
private Access ModifierAll public members of a base class are available to the derived class. However, private members are not. For example, in Listing 6.6, the private field, _Name, is not available on Contact. Listing 6.6. Private Members Are Not Inherited
As part of keeping with the principal of encapsulation, derived classes cannot access members declared as private. 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. protected Access ModifierEncapsulation is finer grained than just public or private, however. It is possible to define members in base classes that only derived classes can access. Consider the ObjectKey property shown in Listing 6.7, for example. Listing 6.7. protected Members Are Accessible Only from Derived Classes
ObjectKey is defined using the protected access modifier. The result is that it is accessible outside of PdaItem only from classes that derive from PdaItem. Contact derives from PdaItem and, therefore, all members of Contact have access to ObjectKey. Since Program does not derive from PdaItem, using the ObjectKey property within Program results in a compile error. Single InheritanceIn 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 singleinheritance programming language (as is the CIL language to which C# compiles). This means that 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 multipleinheritance class structure, the general solution is to use aggregation. Figure 6.2 shows an example of this class structure. Aggregation occurs when the association relationship defines a core part of the containing object. For multiple inheritance, this involves picking one class as the primary base class (PdaItem) and deriving a new class (Contact) from that. The second desired base class (Person) is added as a field in the derived class (Contact). Next, all the nonprivate members on the field (Person) are redefined on the derived class (Contact) which then delegates the calls out to the field (Person). Some code duplication occurs because methods are redeclared; however, this is minimal, since the real method body is implemented only within the aggregated class (Person). Figure 6.2. Working around Multiple Inheritance Using AggregationIn Figure 6.2, Contact contains a private property called InternalPerson that is drawn as an association to the Person class. Contact also contains the FirstName and LastName properties but with no corresponding fields. Instead, the FirstName and LastName properties simply delegate their calls out to InternalPerson.FirstName and InternalPerson.LastName, respectively. Listing 6.8 shows the resulting code. Listing 6.8. Working around Single Inheritance Using Aggregation
Another drawback is that any methods added to the field class (Person) will require manual addition to the derived class (Contact); otherwise, Contact will not expose the added functionality. Sealed ClassesTo design a class correctly that others can extend via derivation can be a tricky task that requires testing with examples to verify the derivation will work successfully. Furthermore, a slight performance overhead is associated with derived classes. To avoid some of these concerns, classes can be marked as sealed (see Listing 6.9). Listing 6.9. Preventing Derivation with Sealed Classes
Sealed classes include the sealed modifier, and the result is that they cannot be derived. The string type is another example of a type that uses the sealed modifier to prevent derivation. |