Understanding the Basic Mechanics of Inheritance
Inheritance is an aspect of OOP that facilitates code reuse. Code reuse comes in two forms: inheritance and the containment/delegation model. Inheritance can be described as an "is-a" relationship and the containment/delegation model can be described as a "has-a" relationship.
Specifying the Parent Class of an Existing Class
Classical inheritance allows you to build new class definitions that extend the functionality of an existing class. The existing class that serves as the basis for new classes is termed the base class, superclass, or parent class. The extending classes are termed derived or child classes. In C#, the colon operator is used to establish the "is-a" relationship.
class MiniVan : Car { }
Although constructors are typically defined as public, a derived class never inherits the constructors of a parent class. Constructors are used to construct only the class that they are defined within. They can be called by a derived class through constructor chaining.
Always remember that inheritance preserves encapsulation. Private members can be accessed only by the class that defines it.
Regarding Multiple Base Classes
C# demands that a given class have exactly one direct base class. Multiple inheritance is not supported in C#. It is supported in unmanaged C++.
Using the sealed Keyword
The sealed keyword prevents inheritance from occurring. When a class is marked as sealed, the compiler will not allow classes to derive from it. Sealing a class often makes sense when you are designing a utility class. C# structures are always implicitly sealed. Therefore a structure or class cannot be derived from a structure. A structure also cannot be derived from a class.
Revisiting Visual Studio Class Diagrams
Understanding the Second Pillar of OOP: The Details of Inheritance
Calling Base Class Constructors with the base Keyword
The default constructor of a base class is called automatically before the logic of the derived constructor is executed. It is a good idea to implement the subclass constructors to explicitly call an appropriate custom base class constructor.
public Manager(string fullName, int age, int empId, float currPay, string ssn, int numbOfOpts) : base(fullName, age, empId, currPay, ssn, EmployeePayTypeEnum.Salaried) { // This property is defined by the Manager class. StockOptions = numbOfOpts; }
This indicates a derived constructor is passing data to the immediate parent constructor.
The base keyword can be used whenever a subclass wants to access a public or protected member defined by a parent class. Use of this keyword is not limited to constructor logic.
Recall that once you add a custom constructor to a class definition, the default constructor is silently removed.
Keeping Family Secrets: The protected Keyword
Public items are directly accessible from anywhere, while private items can be accessed only by the class that has defined them. C# like many other languages provides an additional keyword, protected.
Protected members can be accessed directly by any descendent. Convention is that protected members are named PascalCased and not underscore-camelCase.
The benefit of defining protected members is that derived types no longer have to access the data indirectly through the public interface, which is also a potential downside as business rules could you bypassed. Protected members involve a level of trust between the parent and child classes. Outside the family, protected data is regarded as private.
Adding a sealed Class
A sealed class cannot be extended by other classes. You might find, when building a class hierarchy, that a certain branch in the inheritance chain should be "capped off."
Understanding Inheritance with Record Types
The new C# 9.0 record types support inheritance.
Inheritance for Record Types with Standard Properties
Inheritance for Record Types with Positional Parameters
Nondestructive Mutation with Inherited Record Types
Equality with Inherited Record Types
Deconstructor Behavior with Inherited Record Types
Programming for Containment/Delegation
The "has-a" relationship is known as containment/delegation or aggregation.
Delegation is simply the act of adding public members to the containing class that use the contained object's functionality.
partial class Employee { // Contain a BenefitPackage object. protected BenefitPackage EmpBenefits = new BenefitPackage(); // Expose certain benefit behaviors of object. public double GetBenefitCost() => EmpBenefits.ComputePayDeduction(); // Expose object through a custom property public BenefitPackage Benefits { get { return EmpBenefits; } set { EmpBenefits = value; } } }
Understanding Nested Type Definitions
In C# it is possible to define a type directly within the scope of a class or structure. The nested (or inner) type is considered a member of the nesting (or outer) class and in the eyes of the runtime can be manipulated like any other member.
public class OuterClass { // A public nested type can be used by anybody. public class PublicInnerClass {} // A private nested type can only be used by members // of the containing class private class PrivateInnerClass {} }
Nested types allow you to gain complete control over the access level of the inner type because they may be declared privately. Recall that non-nested classes cannot be declared using the private keyword. Because a nested type is a member of the containing class, it can access private members of the containing class. Often a nested type is useful as a helper for the outer class and is not intended to be used by the outside world.
Understanding the Third Pillar of OOP: C#'s Polymorphic Support
Using the virtual and override Keywords
Polymorphism provides for a way for a subclass to define its own version of a method defined by its base class, using the process termed method overriding. If a base class wants to define a method that may be, but does not have to be, overridden by a subclass, it must mark the method with the virtual keyword. Methods that have been marked with the virtual keyword are called virtual methods.
When a subclass wants to change the implementation details of a virtual method, it does so by using the override keyword, and the default behavior can be used using the base keyword.
public override void GiveBonus(float amount) { ... public override void GiveBonus(float amount) { base.GiveBonus(amount); Random r = new Random(); StockOptions += r.Next(500); } }
Overriding Virtual Members with Visual Studio/Visual Studio Code
If you type the word override within the scope of a class type and then hit the spacebar, IntelliSense will display a list of all the overridable members defined in the parent classes and excluding methods that are already overridden.
Sealing Virtual Members
You can use the sealed keyword to seal certain methods in a class that is not sealed.
Understanding Abstract Classes
If you have a class that is meant to be used as a parent class but not actually have class instances, you should enforce that instances of it cannot be created by using the abstract keyword. This creates an abstract base class.
Understanding the Polymorphic Interface
When a class has been defined as an abstract base class via the abstract keyword, it may define any number of abstract members. Abstract members can be used whenever you want to define a member that does not supply a default implementation but must be accounted for by each derived class. This enforces a polymorphic interface on each descendent.
An abstract base class's polymorphic interface refers to its set of virtual and abstract methods.Â
Understanding Member Shadowing
C# provides a facility that is the logical opposite of method overriding, termed shadowing. Formally, if a derived class defines a member that is identical to a member defined in a base class, the derived class has shadowed the parent's version. In the real world, the possibility of this occurring is greatest when subclassing a class that you did not create yourself, such as from a third-party package.
This can be default with by using the override keyword if the parent class method is abstract. If it isn't and you do not have access to change it, you can use the new keyword.
Understanding Base Class/Derived Class Casting Rules
A class instance can be stored in a variable of a type where the type is the parent type of the object's class. It is always safe to store a derived object within a base class reference. This is called an implicit cast.
The compiler will not allow storing a derived class as a System.Object variable though because object is higher up the inheritance chain than the parent class. The compiler will not allow an implicit cast in order to keep the code as type-safe as possible. If you know that the object is compatible, you can perform an explicit cast with the casting operator.
Using the C# as Keyword
Explicit casting is evaluated at runtime. Therefore the compiler can allow explicit casts that will result in runtime exceptions. C# provides the as keyword to quickly determine at runtime whether a given type is compatible with another. When you use the as keyword, you are able to determine compatibility by checking against a null return value:
foreach (object item in things) { Hexagon h = item as Hexagon; if (h == null) { Console.WriteLine("Item is not a hexagon."); } else { h.Draw(); } }
Using the C# is Keyword
The is keyword can be used to determine whether two items are compatible. is will return false, rather than null, if the types are incompatible.
static void GivePromotion(Employee emp) { Console.WriteLine("{0} was promoted.", emp.Name); if (emp is SalesPerson) { Console.WriteLine("{0} made {1} sales.", emp.Name, ((SalesPerson)emp).SalesNumber); } else if (emp is Manager) { ... } }
Here you are performing a runtime check to determine what the incoming base class reference is actually pointing to in memory. The is keyword can also assign the converted type to a variable if the cast works. This cleans up the preceding method by preventing the "double-cast" problem.
static void GivePromotion(Employee emp) { Console.WriteLine("{0} was promoted!", emp.Name); // Check if is SalesPerson, assign to variable s if (emp is SalesPerson s) { Console.WriteLine("{0} made {1} sale(s)., s.Name, s.SalesNumber); } ... }
C# 9.0 introduced additional pattern matching capabilities:
if (emp is not Manager and not SalesPerson) { ... }
Discards with the is Keyword
The is keyword can be used with the discard variable placeholder. To create a catchall in your if or switch statement:
if (obj is var _) { // do something }
This will match everything, so be careful about the order in which you use the comparer with the discard.
Revisiting Pattern Matching
static void GivePromotion(Employee emp) { switch (emp) { case SalesPerson s: Console.WriteLine(...); break; case Manager m: ... } }
Discards with switch Statements
Discards can also be used in switch statements:
case Employee _: ... break;
Understanding the Super Parent Class: System.Object
You may have noticed that the base classes in the example hierarchies do not explicitly specify their parent classes. In .NET, every type ultimately derives from a base class named System.Object, represented in C# with the object keyword. System.Object defines a set of common members for every type in the framework. When you build a class that does not explicitly define its parent, the compiler automatically derives your type from System.Object.
The System.Object defines a set of methods, some marked virtual, and others marked static.
Overriding System.Object.ToString()
public override string ToString() => $"[First Name: {FirstName}; Last Name: {LastName}; Age: {Age}]";
Remember that you may also need to account for data up the chain of inheritance. To do so, obtain the ToString() value from the parent using the base keyword, and then append the derived class's custom information.
Overriding System.Object.Equals()
By default, Equals() returns true if two objects being compared reference the same object instance in memory. It could be useful to re-implement this method to instead compare if state were the same between objects.
Overriding System.Object.GetHashCode()
When a class overrides the Equals() method, it should also override the default GetHashCode(). A hash code is a numerical value that represents an object as a particular state. If you create two strings with the same value, they will have the same hash code.
By default, the System.Object.GetHashCode() uses the object's current location in memory to yield the hash value. If you are building a custom type that you intend to store in a Hashtable type (within the System.Collections namespace), you should override this member. Internally, the Hashtable will be invoking Equals() and GetHashCode() to retrieve the correct object. More specifically, the System.Collections.Hashtable class calls GetHashCode() internally to gain a general idea of where the object is located, and then calls Equals() to determine the exact match.
If you can identify a piece of data that should be unique for all instances and immutable, you can override GetHashCode() by having it call the default GetHashCode() on that unique data value.
Using the Static Members of System.Object
System.Object defines two static members that test for value-based or reference-based equality: Equals() and ReferenceEquals().
Summary
This chapter explored the role and details of inheritance and polymorphism. Over these pages you were introduced to numerous new keywords and tokens to support each of these techniques. For example, recall that the colon token is used to establish the parent class of a given type. Parent types are able to define any number of virtual and/or abstract members to establish a polymorphic interface. Derived types override such members using the override keyword.Â
In addition to building numerous class hierarchies, this chapter also examined how to explicitly cast between base and derived types and wrapped up by diving into the details of the cosmic parent class in the .NET base class libraries: System.Object.Â
Article notes
According to the Pro C# book, what are the two forms of code reuse?
Inheritance and containment/delegation
Inheritance can be described as what kind of relationship?
"is-a"
Containment/delegation can be described as what kind of relationship?
"has-a"
In .NET, is it safe to store an object of a derived class type within a base class reference?
Yes
What type of cast is it called in .NET when a derived class object is stored within a base class reference?
An implicit cast
What type of casting in .NET is why you can pass an object argument as a method parameter where the parameter is a parent class of the object's class?
Implicit casting
Is it possible in .NET to cast an object from a base class type to a derived type that is lower in the inheritance chain?
No
Hexagon h = item as Hexagon
What value will h have if item is not a type that can be explicitly cast to a Hexagon?
null
What instance method of System.Object compares items and, by default, only returns true if the items being compared refer to the same item in memory?
Equals()
If you override the System.Object Equals() method in C#, then what other method should you also override (because both of these methods are used internally by Hashtable types to retrieve subobjects from the container)?
GetHashCode()
When you build a class in C# that does not explicitly define its parent class, what is the parent class?
System.Object
What method of System.Object does ValueType override so that structures can do value-based comparisons?
Equals()
"For the time being, you can understand this method (when overridden) is called
to free any allocated resources before the object is destroyed."
What method of System.Object does this quote describe?
Finalize()
What method of System.Object returns an int that identifies a specific object instance?
GetHashCode()
What method of System.Object returns a string representation of the object using the fully qualified name (<namespace>.<type name>)?
ToString()
What keyword in C# is similar to as but it returns false instead of null when an explicit cast is not possible?
is
What method of System.Object returns a Type object that fully describes the object you are currently referencing (this is a runtime type identification method available to all objects)?
GetType()
What method of System.Object exists to return a member-by-member copy of the current object, which is often used when cloning an object?
MemberwiseClone()
What keyword does C# provides to quickly determine at runtime whether a given type is compatible with another (by checking against a null return value)?
as
What two keywords can you use in C# to trap the possibility of an invalid explicit cast resulting in a runtime exception?
try catch
When is explicit casting evaluated in .NET (at compile time or runtime)?
Runtime
What type of cast can you use in .NET when you know the object reference is pointing to a compatible class in memory but the compiler cannot?
An explicit cast
What is the ultimate base class in the .NET system?
System.Object
What are three terms for the existing class that serves as the basis for new classes in classical inheritance?
Base class, superclass, parent class
What are two terms for the extending classes in classical inheritance?
Derived class, child class
What is the operator that establishes an "is-a" relationship (inheritance) in C#?
:
Does a derived class inherit the constructor of a parent class in C#?
Never
What can be used/what is it called in C# to allow a constructor to be called by a derived class?
Constructor chaining
Does C# support multiple inheritance?
No
What is it called when a class in a programming language can have more than one direct parent class?
Multiple inheritance
What is the keyword in C# that prevents inheritance from occurring?
sealed
What category of types in C# is always sealed?
structure
What C# keyword can be used whenever a subclass wants to access (not override) a public or protected member defined by a parent class?
base
What happens to the default constructor in a C# class when a custom constructor is added to the class definition?
The default constructor is silently removed
What kind of C# members are directly accessible by any descendent of the class but outside the family, regarded as private?
Protected
What C# keyword can be used to specify that a class cannot be extended by other classes?
sealed
What is the act of adding public members to a class containing an object that use the contained object's functionality?
Delegation
Can non-nested classes be declared using the private keyword in C#?
No
What C# keyword is used on a method that may be, but does not have to be, overridden by a subclass?
virtual
What keyword does a child class in C# use when it wants to change the implementation details of a virtual method?
override
What keyword can be used to enforce that a parent class cannot have instances of it created in C#?
abstract
What keyword should be used in C# to define a method that does not provide a default implementation and must be accounted for in each derived class?
abstract
An abstract base class's polymorphic interface in C# refers to its what?
Set of virtual and abstract methods
If you were using a third-party package, and your subclass of a class from the package defined an identical member as the parent class, this is an example of what happening?
Shadowing