In this chapter you’ll learn:
• How polymorphism enables you to “program in the general” and make systems extensible.
• To use overridden methods to effect polymorphism.
• To create abstract classes and methods.
• To determine an object’s type at execution time.
• To create sealed
methods and classes.
• To declare and implement interfaces.
• To overload operators to enable them to manipulate objects.
One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them.
—John Ronald Reuel Tolkien
General propositions do not decide concrete cases.
—Oliver Wendell Holmes
A philosopher of imposing stature doesn’t think in a vacuum. Even his most abstract ideas are, to some extent, conditioned by what is or is not known in the time when he lives.
—Alfred North Whitehead
12.1 Introduction
12.2 Polymorphism Examples
12.3 Demonstrating Polymorphic Behavior
12.4 Abstract Classes and Methods
12.5 Case Study: Payroll System Using Polymorphism
12.5.1 Creating Abstract Base Class Employee
12.5.2 Creating Concrete Derived Class SalariedEmployee
12.5.3 Creating Concrete Derived Class HourlyEmployee
12.5.4 Creating Concrete Derived Class CommissionEmployee
12.5.5 Creating Indirect Concrete Derived Class BasePlusCommission-Employee
12.5.6 Polymorphic Processing, Operator is
and Downcasting
12.5.7 Summary of the Allowed Assignments Between Base-Class and Derived-Class Variables
12.6 sealed
Methods and Classes
12.7 Case Study: Creating and Using Interfaces
12.7.1 Developing an IPayable
Hierarchy
12.7.2 Declaring Interface IPayable
12.7.3 Creating Class Invoice
12.7.4 Modifying Class Employee
to Implement Interface IPayable
12.7.5 Modifying Class SalariedEmployee
for Use with IPayable
12.7.6 Using Interface IPayable
to Process Invoice
s and Employee
s Polymorphically
12.7.7 Common Interfaces of the .NET Framework Class Library
12.8 Operator Overloading
12.9 Wrap-Up
We now continue our study of object-oriented programming by explaining and demonstrating polymorphism with inheritance hierarchies. Polymorphism enables us to “program in the general” rather than “program in the specific.” In particular, polymorphism enables us to write applications that process objects that share the same base class in a class hierarchy as if they were all objects of the base class.
Let’s consider a polymorphism example. Suppose we create an application that simulates moving several types of animals for a biological study. Classes Fish
, Frog
and Bird
represent the types of animals under investigation. Imagine that each class extends base class Animal
, which contains a method Move
and maintains an animal’s current location as x–y–z coordinates. Each derived class implements method Move
. Our application maintains an array of references to objects of the various Animal-
derived classes. To simulate an animal’s movements, the application sends each object the same message once per second—namely, Move
. Each specific type of Animal
responds to a Move
message in a unique way—a Fish
might swim three feet, a Frog
might jump five feet and a Bird
might fly 10 feet. The application issues the Move
message to each animal object generically, but each object modifies its x–y–z coordinates appropriately for its specific type of movement. Relying on each object to know how to “do the right thing” in response to the same method call is the key concept of polymorphism. The same message (in this case, Move
) sent to a variety of objects has “many forms” of results—hence the term polymorphism.
With polymorphism, we can design and implement systems that are easily extensible—new classes can be added with little or no modification to the general portions of the application, as long as the new classes are part of the inheritance hierarchy that the application processes generically. The only parts of an application that must be altered to accommodate new classes are those that require direct knowledge of the new classes that you add to the hierarchy. For example, if we extend class Animal
to create class Tortoise
(which might respond to a Move
message by crawling one inch), we need to write only the Tortoise
class and the part of the simulation that instantiates a Tortoise
object. The portions of the simulation that process each Animal
generically can remain the same.
This chapter has several parts. First, we discuss common examples of polymorphism. We then provide a live-code example demonstrating polymorphic behavior. As you’ll soon see, you’ll use base-class references to manipulate both base-class objects and derived-class objects polymorphically.
Employee
Inheritance HierarchyWe then present a case study that revisits the employee hierarchy of Section 11.4.5. We develop a simple payroll application that polymorphically calculates the weekly pay of several different types of employees using each employee’s Earnings
method. Though the earnings of each type of employee are calculated in a specific way, polymorphism allows us to process the employees “in the general.” In the case study, we enlarge the hierarchy to include two new classes—SalariedEmployee
(for people paid a fixed weekly salary) and HourlyEmployee
(for people paid an hourly salary and “time-and-a-half” for overtime). We declare a common set of functionality for all the classes in the updated hierarchy in an “abstract” class, Employee
, from which classes SalariedEmployee
, HourlyEmployee
and CommissionEmployee
inherit directly and class BasePlusCommissionEmployee
inherits indirectly. As you’ll soon see, when we invoke each employee’s Earnings
method off a base-class Employee
reference, the correct earnings calculation is performed due to C#’s polymorphic capabilities.
Occasionally, when performing polymorphic processing, we need to program “in the specific.” Our Employee
case study demonstrates that an application can determine the type of an object at execution time and act on that object accordingly. In the case study, we use these capabilities to determine whether a particular employee object is a BasePlusCommissionEmployee
. If so, we increase that employee’s base salary by 10%.
The chapter continues with an introduction to C# interfaces. An interface describes a set of methods and properties that can be called on an object, but does not provide concrete implementations for them. You can declare classes that implement (i.e., provide concrete implementations for the methods and properties of) one or more interfaces. Each interface member must be defined for all the classes that implement the interface. Once a class implements an interface, all objects of that class have an is-a relationship with the interface type, and all objects of the class are guaranteed to provide the functionality described by the interface. This is true of all derived classes of that class as well.
Interfaces are particularly useful for assigning common functionality to possibly unrelated classes. This allows objects of unrelated classes to be processed polymorphically—objects of classes that implement the same interface can respond to the same method calls. To demonstrate creating and using interfaces, we modify our payroll application to create a general accounts-payable application that can calculate payments due for the earnings of company employees and for invoice amounts to be billed for purchased goods. As you’ll see, interfaces enable polymorphic capabilities similar to those enabled by inheritance.
This chapter ends with an introduction to operator overloading. In previous chapters, we declared our own classes and used methods to perform tasks on objects of those classes. Operator overloading allows us to define the behavior of the built-in operators, such as +
, -
and <
, when used on objects of our own classes. This provides a much more convenient notation than calling methods for performing tasks on objects.
We now consider several additional examples of polymorphism.
If class Rectangle
is derived from class Quadrilateral
(a four-sided shape), then a Rectangle
is a more specific version of a Quadrilateral
. Any operation (e.g., calculating the perimeter or the area) that can be performed on a Quadrilateral
object can also be performed on a Rectangle
object. These operations also can be performed on other Quadrilateral
s, such as Square
s, Parallelogram
s and Trapezoid
s. The polymorphism occurs when an application invokes a method through a base-class variable—at execution time, the correct derived-class version of the method is called, based on the type of the referenced object. You’ll see a simple code example that illustrates this process in Section 12.3.
SpaceObject
Inheritance HierarchyAs another example, suppose we design a video game that manipulates objects of many different types, including objects of classes Martian
, Venusian
, Plutonian
, SpaceShip
and LaserBeam
. Imagine that each class inherits from the common base class SpaceObject
, which contains method Draw
. Each derived class implements this method. A screen-manager application maintains a collection (e.g., a SpaceObject
array) of references to objects of the various classes. To refresh the screen, the screen manager periodically sends each object the same message—namely, Draw
. However, each object responds in a unique way. For example, a Martian
object might draw itself in red with the appropriate number of antennae. A SpaceShip
object might draw itself as a bright silver flying saucer. A LaserBeam
object might draw itself as a bright red beam across the screen. Again, the same message (in this case, Draw
) sent to a variety of objects has many forms of results.
A polymorphic screen manager might use polymorphism to facilitate adding new classes to a system with minimal modifications to the system’s code. Suppose we want to add Mercurian
objects to our video game. To do so, we must build a Mercurian
class that extends SpaceObject
and provides its own Draw
method implementation. When objects of class Mercurian
appear in the SpaceObject
collection, the screen-manager code invokes method Draw
, exactly as it does for every other object in the collection, regardless of its type, so the new Mercurian
objects simply “plug right in” without any modification of the screen-manager code by the programmer. Thus, without modifying the system (other than to build new classes and modify the code that creates new objects), you can use polymorphism to include additional types that might not have been envisioned when the system was created.
Polymorphism promotes extensibility: Software that invokes polymorphic behavior is independent of the object types to which messages are sent. New object types that can respond to existing method calls can be incorporated into a system without requiring modification of the base system. Only client code that instantiates new objects must be modified to accommodate new types.
Section 11.4 created a commission-employee class hierarchy, in which class BasePlusCommissionEmployee
inherited from class CommissionEmployee
. The examples in that section manipulated CommissionEmployee
and BasePlusCommissionEmployee
objects by using references to them to invoke their methods. We aimed base-class references at base-class objects and derived-class references at derived-class objects. These assignments are natural and straightforward—base-class references are intended to refer to base-class objects, and derived-class references are intended to refer to derived-class objects. However, other assignments are possible.
In the next example, we aim a base-class reference at a derived-class object. We then show how invoking a method on a derived-class object via a base-class reference can invoke the derived-class functionality—the type of the actual referenced object, not the type of the reference, determines which method is called. This example demonstrates the key concept that an object of a derived class can be treated as an object of its base class. This enables various interesting manipulations. An application can create an array of base-class references that refer to objects of many derived-class types. This is allowed because each derived-class object is an object of its base class. For instance, we can assign the reference of a BasePlusCommissionEmployee
object to a base-class CommissionEmployee
variable because a BasePlusCommissionEmployee
is a CommissionEmployee
—so we can treat a BasePlusCommissionEmployee
as a CommissionEmployee
.
A base-class object is not an object of any of its derived classes. For example, we cannot directly assign the reference of a CommissionEmployee
object to a derived-class BasePlusCommissionEmployee
variable, because a CommissionEmployee
is not a BasePlusCommissionEmployee
—a CommissionEmployee
does not, for example, have a baseSalary
instance variable and does not have a BaseSalary
property. The is-a relationship applies from a derived class to its direct and indirect base classes, but not vice versa.
The compiler allows the assignment of a base-class reference to a derived-class variable if we explicitly cast the base-class reference to the derived-class type—a technique we discuss in greater detail in Section 12.5.6. Why would we ever want to perform such an assignment? A base-class reference can be used to invoke only the methods declared in the base class—attempting to invoke derived-class-only methods through a base-class reference results in compilation errors. If an application needs to perform a derived-class-specific operation on a derived-class object referenced by a base-class variable, the application must first cast the base-class reference to a derived-class reference through a technique known as downcasting. This enables the application to invoke derived-class methods that are not in the base class. We present an example of downcasting in Section 12.5.6.
Figure 12.1 demonstrates three ways to use base-class and derived-class variables to store references to base-class and derived-class objects. The first two are straightforward—as in Section 11.4, we assign a base-class reference to a base-class variable, and we assign a derived class reference to a derived class variable. Then we demonstrate the relationship between derived classes and base classes (i.e., the is-a relationship) by assigning a derived-class reference to a base-class variable. [Note: This application uses classes CommissionEmployee
and BasePlusCommissionEmployee
from Fig. 11.12 and Fig. 11.13, respectively.]
Fig. 12.1. Assigning base-class and derived-class references to base-class and derived-class variables.
In Fig. 12.1, lines 11–12 create a new CommissionEmployee
object and assign its reference to a CommissionEmployee
variable. Lines 15–17 create a new BasePlusCommissionEmployee
object and assign its reference to a BasePlusCommissionEmployee
variable. These assignments are natural—for example, a CommissionEmployee
variable’s primary purpose is to hold a reference to a CommissionEmployee
object. Lines 21–25 use the reference commissionEmployee
to invoke methods ToString
and Earnings
. Because commissionEmployee
refers to a CommissionEmployee
object, base class Commission-Employee
’s version of the methods are called. Similarly, lines 29–33 use the reference basePlusCommissionEmployee
to invoke the methods ToString
and Earnings
on the BasePlusCommissionEmployee
object. This invokes derived class BasePlusCommissionEmployee
’s version of the methods.
Lines 37–38 then assign the reference to derived-class object basePlusCommissionEmployee
to a base-class CommissionEmployee
variable, which lines 39–43 use to invoke methods ToString
and Earnings
. A base-class variable that contains a reference to a derived-class object and is used to call a virtual
method actually calls the overriding derived-class version of the method. Hence, commissionEmployee2.ToString()
in line 42 actually calls derived class BasePlusCommissionEmployee
’s ToString
method. The compiler allows this “crossover” because an object of a derived class is an object of its base class (but not vice versa). When the compiler encounters a method call made through a variable, the compiler determines if the method can be called by checking the variable’s class type. If that class contains the proper method declaration (or inherits one), the compiler allows the call to be compiled. At execution time, the type of the object to which the variable refers determines the actual method to use.
When we think of a class type, we assume that applications will create objects of that type. In some cases, however, it’s useful to declare classes for which you never intend to instantiate objects. Such classes are called abstract classes. Because they’re used only as base classes in inheritance hierarchies, we refer to them as abstract base classes. These classes cannot be used to instantiate objects, because, as you’ll soon see, abstract classes are incomplete—derived classes must define the “missing pieces.” We demonstrate abstract classes in Section 12.5.1.
The purpose of an abstract class is primarily to provide an appropriate base class from which other classes can inherit, and thus share a common design. In the Shape
hierarchy of Fig. 11.3, for example, derived classes inherit the notion of what it means to be a Shape
—common attributes such as location
, color
and borderThickness
, and behaviors such as Draw
, Move
, Resize
and ChangeColor
. Classes that can be used to instantiate objects are called concrete classes. Such classes provide implementations of every method they declare (some of the implementations can be inherited). For example, we could derive concrete classes Circle
, Square
and Triangle
from abstract base class TwoDimensionalShape
. Similarly, we could derive concrete classes Sphere
, Cube
and Tetrahedron
from abstract base class ThreeDimensionalShape
. Abstract base classes are too general to create real objects—they specify only what is common among derived classes. We need to be more specific before we can create objects. For example, if you send the Draw
message to abstract class TwoDimensionalShape
, the class knows that two-dimensional shapes should be drawable, but it does not know what specific shape to draw, so it cannot implement a real Draw
method. Concrete classes provide the specifics that make it reasonable to instantiate objects.
Not all inheritance hierarchies contain abstract classes. However, you’ll often write client code that uses only abstract base-class types to reduce client code’s dependencies on a range of specific derived-class types. For example, you can write a method with a parameter of an abstract base-class type. When called, such a method can be passed an object of any concrete class that directly or indirectly extends the base class specified as the parameter’s type.
Abstract classes sometimes constitute several levels of the hierarchy. For example, the Shape
hierarchy of Fig. 11.3 begins with abstract class Shape
. On the next level of the hierarchy are two more abstract classes, TwoDimensionalShape
and ThreeDimensionalShape
. The next level of the hierarchy declares concrete classes for TwoDimensionalShape
s (Circle
, Square
and Triangle
) and for ThreeDimensionalShape
s (Sphere
, Cube
and Tetrahedron
).
You make a class abstract by declaring it with the keyword abstract
. An abstract class normally contains one or more abstract methods. An abstract method is one with keyword abstract
in its declaration, as in
public abstract void Draw(); // abstract method
Abstract methods are implicitly virtual and do not provide implementations. A class that contains abstract methods must be declared as an abstract class even if it contains some concrete (nonabstract) methods. Each concrete derived class of an abstract base class also must provide concrete implementations of the base class’s abstract methods. We show an example of an abstract class with an abstract method in Fig. 12.4.
Properties can also be declared abstract
or virtual
, then overridden in derived classes with the override
keyword, just like methods. This allows an abstract base class to specify common properties of its derived classes. Abstract property declarations have the form:
public abstract PropertyType MyProperty
{
get;
set;
} // end abstract property
The semicolons after the get
and set
keywords indicate that we provide no implementation for these accessors. An abstract property may omit implementations for the get
accessor or the set
accessor. Concrete derived classes must provide implementations for every accessor declared in the abstract property. When both get
and set
accessors are specified, every concrete derived class must implement both. If one accessor is omitted, the derived class is not allowed to implement that accessor. Doing so causes a compilation error.
Constructors and static
methods cannot be declared abstract
. Constructors are not inherited, so an abstract
constructor could never be implemented. Similarly, derived classes cannot override static
methods, so an abstract static
method could never be implemented.
An abstract class declares common attributes and behaviors of the various classes that inherit from it, either directly or indirectly, in a class hierarchy. An abstract class typically contains one or more abstract methods or properties that concrete derived classes must override. The instance variables, concrete methods and concrete properties of an abstract class are subject to the normal rules of inheritance.
Attempting to instantiate an object of an abstract class is a compilation error.
Failure to implement a base class’s abstract methods and properties in a derived class is a compilation error unless the derived class is also declared abstract
Although we cannot instantiate objects of abstract base classes, you’ll soon see that we can use abstract base classes to declare variables that can hold references to objects of any concrete classes derived from those abstract classes. Applications typically use such variables to manipulate derived-class objects polymorphically. Also, you can use abstract base-class names to invoke static
methods declared in those abstract base classes.
Polymorphism is particularly effective for implementing so-called layered software systems. In operating systems, for example, each type of physical device could operate quite differently from the others. Even so, common commands can read or write data from and to the devices. For each device, the operating system uses a piece of software called a device driver to control all communication between the system and the device. The write message sent to a device driver object needs to be interpreted specifically in the context of that driver and how it manipulates a specific device. However, the write call itself really is no different from the write to any other device in the system: Place some number of bytes from memory onto that device. An object-oriented operating system might use an abstract base class to provide an “interface” appropriate for all device drivers. Then, through inheritance from that abstract base class, derived classes are formed that all behave similarly. The device-driver methods are declared as abstract methods in the abstract base class. The implementations of these abstract methods are provided in the derived classes that correspond to the specific types of device drivers. New devices are always being developed, often long after the operating system has been released. When you buy a new device, it comes with a device driver provided by the device vendor. The device is immediately operational after you connect it to your computer and install the device driver. This is another elegant example of how polymorphism makes systems extensible.
It’s common in object-oriented programming to declare an iterator class that can traverse all the objects in a collection, such as an array (Chapter 8) or a List
(Chapter 9). For example, an application can print a List
of objects by creating an iterator object and using it to obtain the next list element each time the iterator is called. Iterators often are used in polymorphic programming to traverse a collection that contains references to objects of various classes in an inheritance hierarchy. (Chapters 22–23 present a thorough treatment of C#’s “generics” capabilities and iterators.) A List
of references to objects of class TwoDimensionalShape
, for example, could contain references to objects from derived classes Square
, Circle
, Triangle
and so on. Calling method Draw
for each TwoDimensionalShape
object off a TwoDimensionalShape
variable would polymorphically draw each object correctly on the screen.
This section reexamines the CommissionEmployee
-BasePlusCommissionEmployee
hierarchy that we explored throughout Section 11.4. Now we use an abstract method and polymorphism to perform payroll calculations based on the type of employee. We create an enhanced employee hierarchy to solve the following problem:
A company pays its employees on a weekly basis. The employees are of four types: Salaried employees are paid a fixed weekly salary regardless of the number of hours worked, hourly employees are paid by the hour and receive “time-and-a-half” overtime pay for all hours worked in excess of 40 hours, commission employees are paid a percentage of their sales, and salaried-commission employees receive a base salary plus a percentage of their sales. For the current pay period, the company has decided to reward salaried-commission employees by adding 10% to their base salaries. The company wants to implement a C# application that performs its payroll calculations polymorphically.
We use abstract
class Employee
to represent the general concept of an employee. The classes that extend Employee
are SalariedEmployee
, CommissionEmployee
and HourlyEmployee
. Class BasePlusCommissionEmployee
—which extends CommissionEmployee
—represents the last employee type. The UML class diagram in Fig. 12.2 shows the inheritance hierarchy for our polymorphic employee payroll application. Abstract class Employee
is italicized, as per the convention of the UML.
Fig. 12.2. Employee
hierarchy UML class diagram.
Abstract base class Employee
declares the “interface” to the hierarchy—that is, the set of methods that an application can invoke on all Employee
objects. We use the term “interface” here in a general sense to refer to the various ways applications can communicate with objects of any Employee
derived class. Be careful not to confuse the general notion of an “interface” with the formal notion of a C# interface, the subject of Section 12.7. Each employee, regardless of the way his or her earnings are calculated, has a first name, a last name and a social security number, so those pieces of data appear in abstract base class Employee
.
A derived class can inherit “interface” or “implementation” from a base class. Hierarchies designed for implementation inheritance tend to have their functionality high in the hierarchy—each new derived class inherits one or more methods that were implemented in a base class, and the derived class uses the base-class implementations. Hierarchies designed for interface inheritance tend to have their functionality lower in the hierarchy—a base class specifies one or more abstract methods that must be declared for each concrete class in the hierarchy, and the individual derived classes override these methods to provide derived-class-specific implementations.
The following sections implement the Employee
class hierarchy. The first section implements abstract
base class Employee
. The next four sections each implement one of the concrete classes. The sixth section implements a test application that builds objects of all these classes and processes those objects polymorphically.
Employee
Class Employee
(Fig. 12.4) provides methods Earnings
and ToString
, in addition to the auto-implemented properties that manipulate Employee
’s data. An Earnings
method certainly applies generically to all employees. But each earnings calculation depends on the employee’s class. So we declare Earnings
as abstract
in base class Employee
, because a default implementation does not make sense for that method—there’s not enough information to determine what amount Earnings
should return. Each derived class overrides Earnings
with an appropriate implementation. To calculate an employee’s earnings, the application assigns a reference to the employee’s object to a base class Employee
variable, then invokes the Earnings
method on that variable. We maintain an array of Employee
variables, each of which holds a reference to an Employee
object (of course, there cannot be Employee
objects because Employee
is an abstract class—because of inheritance, however, all objects of all derived classes of Employee
may nevertheless be thought of as Employee
objects). The application iterates through the array and calls method Earnings
for each Employee
object. C# processes these method calls polymorphically. Including Earnings
as an abstract method in Employee
forces every directly derived concrete class of Employee
to override Earnings
with a method that performs an appropriate pay calculation.
Method ToString
in class Employee
returns a string
containing the employee’s first name, last name and social security number. Each derived class of Employee
overrides method ToString
to create a string representation of an object of that class containing the employee’s type (e.g., "salaried employee:"
), followed by the rest of the employee’s information.
The diagram in Fig. 12.3 shows each of the five classes in the hierarchy down the left side and methods Earnings
and ToString
across the top. For each class, the diagram shows the desired results of each method. [Note: We do not list base class Employee
’s properties because they’re not overridden in any of the derived classes—each of these properties is inherited and used “as is” by each of the derived classes.]
Fig. 12.3. Polymorphic interface for the Employee
hierarchy classes.
Let’s consider class Employee
’s declaration (Fig. 12.4). The class includes a constructor that takes the first name, last name and social security number as arguments (lines 15–20); read-only properties for obtaining the first name, last name and social security number (lines 6, 9 and 12, respectively); method ToString
(lines 23–27), which uses properties to return the string representation of the Employee
; and abstract
method Earnings
(line 30), which must be implemented by concrete derived classes. The Employee
constructor does not validate the social security number in this example. Normally, such validation should be provided.
Fig. 12.4. Employee
abstract base class.
Why did we declare Earnings
as an abstract
method? As explained earlier, it simply does not make sense to provide an implementation of this method in class Employee
. We cannot calculate the earnings for a general Employee
—we first must know the specific Employee
type to determine the appropriate earnings calculation. By declaring this method abstract
, we indicate that each concrete derived class must provide an appropriate Earnings
implementation and that an application will be able to use base-class Employee
variables to invoke method Earnings
polymorphically for any type of Employee
.
SalariedEmployee
Class SalariedEmployee
(Fig. 12.5) extends class Employee
(line 5) and overrides Earnings
(lines 34–37), which makes SalariedEmployee
a concrete class. The class includes a constructor (lines 10–14) that takes a first name, a last name, a social security number and a weekly salary as arguments; property WeeklySalary
(lines 17–31) to manipulate instance variable weeklySalary
, including a set
accessor that ensures we assign only nonnegative values to weeklySalary
; method Earnings
(lines 34–37) to calculate a SalariedEmployee
’s earnings; and method ToString
(lines 40–44), which returns a string
including the employee’s type, namely, "salaried employee: "
, followed by employee-specific information produced by base class Employee
’s ToString
method and SalariedEmployee
’s WeeklySalary
property. Class SalariedEmployee
’s constructor passes the first name, last name and social security number to the Employee
constructor (line 11) via a constructor initializer to initialize the base class’s data. Method Earnings
overrides Employee
’s abstract method Earnings
to provide a concrete implementation that returns the SalariedEmployee
’s weekly salary. If we do not implement Earnings
, class SalariedEmployee
must be declared abstract
—otherwise, a compilation error occurs (and, of course, we want SalariedEmployee
to be a concrete class).
Fig. 12.5. SalariedEmployee
class that extends Employee
.
SalariedEmployee
method ToString
(lines 40–44) overrides Employee
’s version. If class SalariedEmployee
did not override ToString
, SalariedEmployee
would have inherited the Employee
version. In that case, SalariedEmployee
’s ToString
method would simply return the employee’s full name and social security number, which does not adequately represent a SalariedEmployee
. To produce a complete string representation of a SalariedEmployee
, the derived class’s ToString
method returns "salaried employee: "
, followed by the base-class Employee
-specific information (i.e., first name, last name and social security number) obtained by invoking the base class’s ToString
(line 43)—this is a nice example of code reuse. The string
representation of a SalariedEmployee
also contains the employee’s weekly salary, obtained by using the class’s WeeklySalary
property.
HourlyEmployee
Class HourlyEmployee
(Fig. 12.6) also extends class Employee
(line 5). The class includes a constructor (lines 11–17) that takes as arguments a first name, a last name, a social security number, an hourly wage and the number of hours worked. Lines 20–34 and 37–51 declare properties Wage
and Hours
for instance variables wage
and hours
, respectively. The set
accessor in property Wage
ensures that wage
is nonnegative, and the set
accessor in property Hours
ensures that hours
is in the range 0
–168
(the total number of hours in a week) inclusive. The class overrides method Earnings
(lines 54–60) to calculate an HourlyEmployee
’s earnings and method ToString
(lines 63–68) to return the employee’s string representation. The HourlyEmployee
constructor, similarly to the SalariedEmployee
constructor, passes the first name, last name and social security number to the base-class Employee
constructor (line 13) to initialize the base class’s data
. Also, method ToString
calls base-class method ToString
(line 67) to obtain the Employee
-specific information (i.e., first name, last name and social security number.
Fig. 12.6. HourlyEmployee
class that extends Employee
.
CommissionEmployee
Class CommissionEmployee
(Fig. 12.7) extends class Employee
(line 5). The class includes a constructor (lines 11–16) that takes a first name, a last name, a social security number, a sales amount and a commission rate; properties (lines 19–33 and 36–50) for instance variables grossSales
and commissionRate
, respectively; method Earnings
(lines 53–56) to calculate a CommissionEmployee
’s earnings; and method ToString
(lines 59–64), which returns the employee’s string representation. The CommissionEmployee
’s constructor also passes the first name, last name and social security number to the Employee
constructor (line 12) to initialize Employee
’s data
. Method ToString
calls base-class method ToString
(line 62) to obtain the Employee
-specific information (i.e., first name, last name and social security number).
Fig. 12.7. CommissionEmployee
class that extends Employee
.
BasePlusCommissionEmployee
Class BasePlusCommissionEmployee
(Fig. 12.8) extends class CommissionEmployee
(line 5) and therefore is an indirect derived class of class Employee
. Class BasePlusCommissionEmployee
has a constructor (lines 10–15) that takes as arguments a first name, a last name, a social security number, a sales amount, a commission rate and a base salary. It then passes the first name, last name, social security number, sales amount and commission rate to the CommissionEmployee
constructor (line 12) to initialize the base class’s data. BasePlusCommissionEmployee
also contains property BaseSalary
(lines 19–33) to manipulate instance variable baseSalary
. Method Earnings
(lines 36–39) calculates a BasePlusCommissionEmployee
’s earnings. Line 38 in method Earnings
calls base class CommissionEmployee
’s Earnings
method to calculate the commission-based portion of the employee’s earnings. Again, this shows the benefits of code reuse. BasePlusCommissionEmployee
’s ToString
method (lines 42–46) creates a string representation of a BasePlusCommissionEmployee
that contains "base-salaried"
, followed by the string
obtained by invoking base class CommissionEmployee
’s ToString
method (another example of code reuse), then the base salary. The result is a string
beginning with "base-salaried commission employee"
, followed by the rest of the BasePlusCommissionEmployee
’s information. Recall that CommissionEmployee
’s ToString
method obtains the employee’s first name, last name and social security number by invoking the ToString
method of its base class (i.e., Employee
)—a further demonstration of code reuse. BasePlusCommissionEmployee
’s ToString
initiates a chain of method calls that spans all three levels of the Employee
hierarchy.
Fig. 12.8. BasePlusCommissionEmployee
class that extends CommissionEmployee
.
is
and DowncastingTo test our Employee
hierarchy, the application in Fig. 12.9 creates an object of each of the four concrete classes SalariedEmployee
, HourlyEmployee
, CommissionEmployee
and BasePlusCommissionEmployee
. The application manipulates these objects, first via variables of each object’s own type, then polymorphically, using an array of Employee
variables. While processing the objects polymorphically, the application increases the base salary of each BasePlusCommissionEmployee
by 10% (this, of course, requires determining the object’s type at execution time). Finally, the application polymorphically determines and outputs the type of each object in the Employee
array. Lines 10–20 create objects of each of the four concrete Employee
derived classes. Lines 24–32 output the string representation and earnings of each of these objects. Each object’s ToString
method is called implicitly by WriteLine
when the object is output as a string
with format items.
Fig. 12.9. Employee
hierarchy test application.
Line 35 declares employees
and assigns it an array of four Employee
variables. Lines 38–41 assign a SalariedEmployee
object, an HourlyEmployee
object, a CommissionEmployee
object and a BasePlusCommissionEmployee
object to employees[0]
, employees[1]
, employees[2] and employees[3]
, respectively. Each assignment is allowed, because a SalariedEmployee
is an Employee
, an HourlyEmployee
is an Employee
, a CommissionEmployee
is an Employee
and a BasePlusCommissionEmployee
is an Employee
. Therefore, we can assign the references of SalariedEmployee
, HourlyEmployee
, CommissionEmployee
and BasePlusCommissionEmployee
objects to base-class Employee
variables, even though Employee
is an abstract class.
Employee
sLines 46–66 iterate through array employees
and invoke methods ToString
and Earnings
with Employee
variable currentEmployee
, which is assigned the reference to a different Employee
during each iteration. The output illustrates that the appropriate methods for each class are indeed invoked. All calls to virtual
methods ToString
and Earnings
are resolved at execution time, based on the type of the object to which currentEmployee
refers. This process is known as dynamic binding or late binding. For example, line 48 implicitly invokes method ToString
of the object to which currentEmployee
refers. Only the methods of class Employee
can be called via an Employee
variable—and Employee
includes class object
’s methods, such as ToString
. (Section 11.7 discussed the methods that all classes inherit from class object
.) A base-class reference can be used to invoke only methods of the base class.
BasePlusCommissionEmployee
s 10% RaisesWe perform special processing on BasePlusCommissionEmployee
objects—as we encounter them, we increase their base salary by 10%. When processing objects polymorphically, we typically do not need to worry about the “specifics,” but to adjust the base salary, we do have to determine the specific type of each Employee
object at execution time. Line 51 uses the is
operator to determine whether a particular Employee
object’s type is BasePlusCommissionEmployee
. The condition in line 51 is true if the object referenced by currentEmployee
is a BasePlusCommissionEmployee
. This would also be true for any object of a BasePlusCommissionEmployee
derived class (if there were any), because of the isa relationship a derived class has with its base class. Lines 55–56 downcast current-Employee
from type Employee
to type BasePlusCommissionEmployee
—this cast is allowed only if the object has an is-a relationship with BasePlusCommissionEmployee
. The condition at line 51 ensures that this is the case. This cast is required if we are to use derived class BasePlusCommissionEmployee
’s BaseSalary
property on the current Employee
object—attempting to invoke a derived-class-only method directly on a base class reference is a compilation error.
Assigning a base-class variable to a derived-class variable (without an explicit downcast) is a compilation error.
If at execution time the reference to a derived-class object has been assigned to a variable of one of its direct or indirect base classes, it’s acceptable to cast the reference stored in that base-class variable back to a reference of the derived-class type. Before performing such a cast, use the is
operator to ensure that the object is indeed an object of an appropriate derived-class type.
When downcasting an object, an InvalidCastException
(of namespace System
) occurs if at execution time the object does not have an is a relationship with the type specified in the cast operator. An object can be cast only to its own type or to the type of one of its base classes. You can avoid a potential InvalidCastException
by using the as
operator to perform a downcast rather than a cast operator. For example, in the statement
BasePlusCommissionEmployee employee =
currentEmployee as BasePlusCommissionEmployee;
employee
is assigned a reference to an object that is a BasePlusCommissionEmployee
, or the value null
if currentEmployee
is not a BasePlusCommissionEmployee
. You can then compare employee with null
to determine whether the cast succeeded.
If the is
expression in line 51 is true
, the if
statement (lines 51–62) performs the special processing required for the BasePlusCommissionEmployee
object. Using BasePlusCommissionEmployee
variable employee
, line 58 accesses the derived-class-only property BaseSalary
to retrieve and update the employee’s base salary with the 10% raise.
Lines 64–65 invoke method Earnings
on currentEmployee
, which calls the appropriate derived-class object’s Earnings
method polymorphically. Obtaining the earnings of the SalariedEmployee
, HourlyEmployee
and CommissionEmployee
polymorphically in lines 64–65 produces the same result as obtaining these employees’ earnings individually in lines 24–29. However, the earnings amount obtained for the BasePlusCommissionEmployee
in lines 64–65 is higher than that obtained in lines 30–32, due to the 10% increase in its base salary.
Lines 69–71 display each employee’s type as a string. Every object in C# knows its own type and can access this information through method GetType
, which all classes inherit from class object
. Method GetType
returns an object of class Type
(of namespace System
), which contains information about the object’s type, including its class name, the names of its methods, and the name of its base class. Line 71 invokes method GetType
on the object to get its runtime class (i.e., a Type
object that represents the object’s type). Then method ToString
is implicitly invoked on the object returned by GetType
. The Type
class’s ToString
method returns the class name.
In the previous example, we avoid several compilation errors by downcasting an Employee
variable to a BasePlusCommissionEmployee
variable in lines 55–56. If we remove the cast operator (BasePlusCommissionEmployee)
from line 56 and attempt to assign Employee
variable currentEmployee
directly to BasePlusCommissionEmployee
variable employee
, we receive a “Cannot implicitly convert type
” compilation error. This error indicates that the attempt to assign the reference of base-class object commissionEmployee
to derived-class variable basePlusCommissionEmployee
is not allowed without an appropriate cast operator. The compiler prevents this assignment, because a CommissionEmployee
is not a BasePlusCommissionEmployee
—again, the is-a relationship applies only between the derived class and its base classes, not vice versa.
Similarly, if lines 58 and 61 use base-class variable currentEmployee
, rather than derived-class variable employee
, to use derived-class-only property BaseSalary
, we receive an “'Employee' does not contain a definition for 'BaseSalary'
” compilation error on each of these lines. Attempting to invoke derived-class-only methods on a base-class reference is not allowed. While lines 58 and 61 execute only if is
in line 51 returns true
to indicate that currentEmployee
has been assigned a reference to a BasePlusCommissionEmployee
object, we cannot attempt to use derived-class BasePlusCommissionEmployee
property BaseSalary
with base-class Employee
reference currentEmployee
. The compiler would generate errors in lines 58 and 61, because BaseSalary
is not a base-class member and cannot be used with a base-class variable. Although the actual method that’s called depends on the object’s type at execution time, a variable can be used to invoke only those methods that are members of that variable’s type, which the compiler verifies. Using a base-class Employee
variable, we can invoke only methods and properties found in class Employee
—methods Earnings
and ToString
, and properties FirstName
, LastName
and SocialSecurityNumber
—and method methods inherited from class object
.
Now that you’ve seen a complete application that processes diverse derived-class objects polymorphically, we summarize what you can and cannot do with base-class and derived-class objects and variables. Although a derived-class object also is a base-class object, the two are nevertheless different. As discussed previously, derived-class objects can be treated as if they were base-class objects. However, the derived class can have additional derived-class-only members. For this reason, assigning a base-class reference to a derived-class variable is not allowed without an explicit cast—such an assignment would leave the derived-class members undefined for a base-class object.
We’ve discussed four ways to assign base-class and derived-class references to variables of base-class and derived-class types:
as
operator. At execution time, if the object to which the reference refers is not a derived-class object, an exception will occur. The is
operator can be used to ensure that such a cast is performed only if the object is a derived-class object.sealed
Methods and ClassesOnly methods declared virtual
, override
or abstract
can be overridden in derived classes. A method declared sealed
in a base class cannot be overridden in a derived class. Methods that are declared private
are implicitly sealed
, because it’s impossible to override them in a derived class (though the derived class can declare a new method with the same signature as the private
method in the base class). Methods that are declared static
also are implicitly sealed
, because static
methods cannot be overridden either. A derived-class method declared both override
and sealed
can override a base-class method, but cannot be overridden in derived classes further down the inheritance hierarchy.
A sealed
method’s declaration can never change, so all derived classes use the same method implementation, and calls to sealed
methods are resolved at compile time—this is known as static binding. Since the compiler knows that sealed
methods cannot be overridden, it can often optimize code by removing calls to sealed
methods and replacing them with the expanded code of their declarations at each method-call location—a technique known as inlining the code.
The compiler can decide to inline a sealed
method call and will do so for small, simple sealed
methods. Inlining does not violate encapsulation or information hiding, but does improve performance, because it eliminates the overhead of making a method call.
A class that’s declared sealed
cannot be a base class (i.e., a class cannot extend a sealed
class). All methods in a sealed
class are implicitly sealed
. Class string
is a sealed
class. This class cannot be extended, so applications that use string
s can rely on the functionality of string
objects as specified in the Framework Class Library.
Attempting to declare a derived class of a sealed
class is a compilation error.
Our next example (Figs. 12.11–12.15) reexamines the payroll system of Section 12.5. Suppose that the company involved wishes to perform several accounting operations in a single accounts-payable application—in addition to calculating the payroll earnings that must be paid to each employee, the company must also calculate the payment due on each of several invoices (i.e., bills for goods purchased). Though applied to unrelated things (i.e., employees and invoices), both operations have to do with calculating some kind of payment amount. For an employee, the payment refers to the employee’s earnings. For an invoice, the payment refers to the total cost of the goods listed on the invoice. Can we calculate such different things as the payments due for employees and invoices polymorphically in a single application? Does C# offer a capability that requires that unrelated classes implement a set of common methods (e.g., a method that calculates a payment amount)? C# interfaces offer exactly this capability.
Interfaces define and standardize the ways in which people and systems can interact with one another. For example, the controls on a radio serve as an interface between a radio’s users and its internal components. The controls allow users to perform a limited set of operations (e.g., changing the station, adjusting the volume, choosing between AM and FM), and different radios may implement the controls in different ways (e.g., using push buttons, dials, voice commands). The interface specifies what operations a radio must permit users to perform but does not specify how they’re performed. Similarly, the interface between a driver and a car with a manual transmission includes the steering wheel, the gear shift, the clutch pedal, the gas pedal and the brake pedal. This same interface is found in nearly all manual-transmission cars, enabling someone who knows how to drive one particular manual-transmission car to drive just about any other. The components of each car may look a bit different, but the general purpose is the same—to allow people to drive the car.
Software objects also communicate via interfaces. A C# interface describes a set of methods and properties that can be called on an object—to tell it, for example, to perform some task or return some piece of information. The next example introduces an interface named IPayable
that describes the functionality of any object that must be capable of being paid and thus must offer a method to determine the proper payment amount due. An interface declaration begins with the keyword interface
and can contain only abstract methods, properties, indexers and events (events are discussed in Chapter 14, Graphical User Interfaces with Windows Forms: Part 1.) All interface members are implicitly declared both public
and abstract
. In addition, each interface can extend one or more other interfaces to create a more elaborate interface that other classes can implement.
It’s a compilation error to declare an interface member public
or abstract
explicitly, because they’re redundant in interface-member declarations. It’s also a compilation error to specify any implementation details, such as concrete method declarations, in an interface.
To use an interface, a class must specify that it implements the interface by listing the interface after the colon (:
) in the class declaration. This is the same syntax used to indicate inheritance from a base class. A concrete class implementing the interface must declare each member of the interface with the signature specified in the interface declaration. A class that implements an interface but does not implement all its members is an abstract class—it must be declared abstract
and must contain an abstract
declaration for each unimplemented member of the interface. Implementing an interface is like signing a contract with the compiler that states, “I will provide an implementation for all the members specified by the interface, or I will declare them abstract
.”
Failing to define or declare any member of an interface in a class that implements the interface results in a compilation error.
An interface is typically used when unrelated classes need to share common methods. This allows objects of unrelated classes to be processed polymorphically—objects of classes that implement the same interface can respond to the same method calls. You can create an interface that describes the desired functionality, then implement this interface in any classes requiring that functionality. For example, in the accounts-payable application developed in this section, we implement interface IPayable
in any class that must be able to calculate a payment amount (e.g., Employee
, Invoice
).
An interface often is used in place of an abstract
class when there’s no default implementation to inherit—that is, no fields and no default method implementations. Like abstract
classes, interfaces are typically public
types, so they’re normally declared in files by themselves with the same name as the interface and the .cs
file-name extension.
IPayable
HierarchyTo build an application that can determine payments for employees and invoices alike, we first create an interface named IPayable
. Interface IPayable
contains method GetPaymentAmount
that returns a decimal
amount to be paid for an object of any class that implements the interface. Method GetPaymentAmount
is a general-purpose version of method Earnings
of the Employee
hierarchy—method Earnings
calculates a payment amount specifically for an Employee
, while GetPaymentAmount
can be applied to a broad range of unrelated objects. After declaring interface IPayable
, we introduce class Invoice
, which implements interface IPayable
. We then modify class Employee
such that it also implements interface IPayable
. Finally, we update Employee
derived class SalariedEmployee
to “fit” into the IPayable
hierarchy (i.e., we rename SalariedEmployee
method Earnings
as GetPaymentAmount
).
By convention, the name of an interface begins with “I
”. This helps distinguish interfaces from classes, improving code readability.
When declaring a method in an interface, choose a name that describes the method’s purpose in a general manner, because the method may be implemented by a broad range of unrelated classes.
Classes Invoice
and Employee
both represent things for which the company must be able to calculate a payment amount. Both classes implement IPayable
, so an application can invoke method GetPaymentAmount
on Invoice
objects and Employee
objects alike. This enables the polymorphic processing of Invoice
s and Employee
s required for our company’s accounts-payable application.
The UML class diagram in Fig. 12.10 shows the interface and class hierarchy used in our accounts-payable application. The hierarchy begins with interface IPayable
. The UML distinguishes an interface from a class by placing the word “interface” in guillemets (« and ») above the interface name. The UML expresses the relationship between a class and an interface through a realization. A class is said to “realize,” or implement, an interface. A class diagram models a realization as a dashed arrow with a hollow arrowhead pointing from the implementing class to the interface. The diagram in Fig. 12.10 indicates that classes Invoice
and Employee
each realize (i.e., implement) interface IPayable
. As in the class diagram of Fig. 12.2, class Employee
appears in italics, indicating that it’s an abstract class. Concrete class SalariedEmployee
extends Employee
and inherits its base class’s realization relationship with interface IPayable
.
Fig. 12.10. IPayable
interface and class hierarchy UML class diagram.
IPayable
The declaration of interface IPayable
begins in Fig. 12.11 at line 3. Interface IPayable
contains public abstract
method GetPaymentAmount
(line 5). The method cannot be explicitly declared public
or abstract
. Interfaces can have any number of members and interface methods can have parameters.
Fig. 12.11. IPayable
interface declaration.
Invoice
We now create class Invoice
(Fig. 12.12) to represent a simple invoice that contains billing information for one kind of part. The class contains properties PartNumber
(line 11), PartDescription
(line 14), Quantity
(lines 27–41) and PricePerItem
(lines 44–58) that indicate the part number, the description of the part, the quantity of the part ordered and the price per item. Class Invoice
also contains a constructor (lines 17–24) and a ToString
method (lines 61–67) that returns a string representation of an Invoice
object. The set
accessors of properties Quantity
and PricePerItem
ensure that quantity
and pricePerItem
are assigned only nonnegative values.
Fig. 12.12. Invoice
class implements IPayable
.
Line 5 of Fig. 12.12 indicates that class Invoice
implements interface IPayable
. Like all classes, class Invoice
also implicitly inherits from class object
. C# does not allow derived classes to inherit from more than one base class, but it does allow a class to inherit from a base class and implement any number of interfaces. All objects of a class that implement multiple interfaces have the is-a relationship with each implemented interface type. To implement more than one interface, use a comma-separated list of interface names after the colon (:
) in the class declaration, as in:
public class ClassName : BaseClassName, FirstInterface, SecondInterface, ...
When a class inherits from a base class and implements one or more interfaces, the class declaration must list the base-class name before any interface names.
Class Invoice
implements the one method in interface IPayable
—method GetPaymentAmount
is declared in lines 70–73. The method calculates the amount required to pay the invoice. The method multiplies the values of quantity
and pricePerItem
(obtained through the appropriate properties) and returns the result (line 72). This method satisfies the implementation requirement for the method in interface IPayable
—we’ve fulfilled the interface contract with the compiler.
Employee
to Implement Interface IPayable
We now modify class Employee
to implement interface IPayable
. Figure 12.13 contains the modified Employee
class. This class declaration is identical to that of Fig. 12.4 with two exceptions. First, line 3 of Fig. 12.13 indicates that class Employee
now implements interface IPayable
. Because of this, we must rename Earnings
to GetPaymentAmount
throughout the Employee
hierarchy. As with method Earnings
in the version of class Employee
in Fig. 12.4, however, it does not make sense to implement method GetPaymentAmount
in class Employee
, because we cannot calculate the earnings payment owed to a general Employee
—first, we must know the specific type of Employee
. In Fig. 12.4, we declared method Earnings
as abstract
for this reason, and as a result, class Employee
had to be declared abstract
. This forced each Employee
derived class to override Earnings
with a concrete implementation.
Fig. 12.13. Employee
abstract base class.
In Fig. 12.13, we handle this situation the same way. Recall that when a class implements an interface, the class makes a contract with the compiler stating that the class either will implement each of the methods in the interface or will declare them abstract
. If the latter option is chosen, we must also declare the class abstract
. As we discussed in Section 12.4, any concrete derived class of the abstract class must implement the abstract
methods of the base class. If the derived class does not do so, it too must be declared abstract
. As indicated by the comments in lines 29–30, class Employee
of Fig. 12.13 does not implement method GetPaymentAmount
, so the class is declared abstract
.
SalariedEmployee
for Use with IPayable
Figure 12.14 contains a modified version of class SalariedEmployee
that extends Employee
and implements method GetPaymentAmount
. This version of SalariedEmployee
is identical to that of Fig. 12.5 with the exception that the version here implements method GetPaymentAmount
(lines 35–38) instead of method Earnings
. The two methods contain the same functionality but have different names. Recall that the IPayable
version of the method has a more general name to be applicable to possibly disparate classes. The remaining Employee
derived classes (e.g., HourlyEmployee
, CommissionEmployee
and BasePlusCommissionEmployee
) also must be modified to contain method GetPaymentAmount
in place of Earnings
to reflect the fact that Employee
now implements IPayable
. We leave these modifications to try on your own and use only SalariedEmployee
in our test application in this section.
Fig. 12.14. SalariedEmployee
class that extends Employee
.
When a class implements an interface, the same is-a relationship provided by inheritance applies. Class Employee
implements IPayable
, so we can say that an Employee
is an IPayable
, as are any classes that extend Employee
. As such, SalariedEmployee
objects are IPayable
objects. An object of a class that implements an interface may be thought of as an object of the interface type. Objects of any classes derived from the class that implements the interface can also be thought of as objects of the interface type. Thus, just as we can assign the reference of a SalariedEmployee
object to a base-class Employee
variable, we can assign the reference of a SalariedEmployee
object to an interface IPayable
variable. Invoice
implements IPayable
, so an Invoice
object also is an IPayable
object, and we can assign the reference of an Invoice
object to an IPayable
variable.
Inheritance and interfaces are similar in their implementation of the is-a relationship. An object of a class that implements an interface may be thought of as an object of that interface type. An object of any derived classes of a class that implements an interface also can be thought of as an object of the interface type.
The is-a relationship that exists between base classes and derived classes, and between interfaces and the classes that implement them, holds when passing an object to a method. When a method parameter receives an argument of a base class or interface type, the method polymorphically processes the object received as an argument.
IPayable
to Process Invoice
s and Employee
s PolymorphicallyPayableInterfaceTest
(Fig. 12.15) illustrates that interface IPayable
can be used to process a set of Invoice
s and Employee
s polymorphically in a single application. Line 10 declares payableObjects
and assigns it an array of four IPayable
variables. Lines 13–14 assign the references of Invoice
objects to the first two elements of payableObjects
. Lines 15–18 assign the references of SalariedEmployee
objects to the remaining two elements of payableObjects
. These assignments are allowed because an Invoice
is an IPayable
, a SalariedEmployee
is an Employee
and an Employee
is an IPayable
. Lines 24–29 use a foreach
statement to process each IPayable
object in payableObjects
polymorphically, printing the object as a string
, along with the payment due. Lines 27–28 implicitly invokes method ToString
off an IPayable
interface reference, even though ToString
is not declared in interface IPayable
—all references (including those of interface types) refer to objects that extend object
and therefore have a ToString
method. Line 28 invokes IPayable
method GetPaymentAmount
to obtain the payment amount for each object in payableObjects
, regardless of the actual type of the object. The output reveals that the method calls in lines 27–28 invoke the appropriate class’s implementation of methods ToString
and GetPaymentAmount
. For instance, when currentPayable
refers to an Invoice
during the first iteration of the foreach
loop, class Invoice
’s ToString
and GetPaymentAmount
methods execute.
All methods of class object
can be called by using a reference of an interface type—the reference refers to an object, and all objects inherit the methods of class object
.
Fig. 12.15. Tests interface IPayable
with disparate classes.
In this section, we overview several common interfaces defined in the .NET Framework Class Library. These interfaces are implemented and used in the same manner as those you create (e.g., interface IPayable
in Section 12.7.2). The Framework Class Library’s interfaces enable you to extend many important aspects of C# with your own classes. Figure 12.16 overviews several commonly used Framework Class Library interfaces.
Fig. 12.16. Common interfaces of the .NET Framework Class Library.
Object manipulations are accomplished by sending messages (in the form of method calls) to the objects. This method-call notation is cumbersome for certain kinds of classes, especially mathematical classes. For these classes, it would be convenient to use C#’s rich set of built-in operators to specify object manipulations. In this section, we show how to enable these operators to work with class objects—via a process called operator overloading.
C# enables you to overload most operators to make them sensitive to the context in which they’re used. Some operators are overloaded more frequently than others, especially the various arithmetic operators, such as +
and -
, where operator notation often is more natural. Figures 12.17 and 12.18 provide an example of using operator overloading with a ComplexNumber
class. For a list of overloadable operators, see msdn.microsoft.com/en-us/library/8edha89s.aspx
.
Fig. 12.17. Class that overloads operators for adding, subtracting and multiplying complex numbers.
Fig. 12.18. Overloading operators for complex numbers.
Class ComplexNumber
(Fig. 12.17) overloads the plus (+
), minus (-
) and multiplication (*
) operators to enable programs to add, subtract and multiply instances of class ComplexNumber
using common mathematical notation. Lines 9 and 12 define properties for the Real
and Imaginary
components of the complex number.
Lines 29–34 overload the plus operator (+
) to perform addition of ComplexNumber
s. Keyword operator
, followed by an operator symbol, indicates that a method overloads the specified operator. Methods that overload binary operators must take two arguments. The first argument is the left operand, and the second argument is the right operand. Class ComplexNumber
’s overloaded plus operator takes two ComplexNumber
references as arguments and returns a ComplexNumber
that represents the sum of the arguments. This method is marked public
and static
, which is required for overloaded operators. The body of the method (lines 32–33) performs the addition and returns the result as a new ComplexNumber
. Notice that we do not modify the contents of either of the original operands passed as arguments x
and y
. This matches our intuitive sense of how this operator should behave—adding two numbers does not modify either of the original numbers. Lines 37–51 provide similar overloaded operators for subtracting and multiplying ComplexNumber
s.
Overload operators to perform the same function or similar functions on class objects as the operators perform on objects of simple types. Avoid nonintuitive use of operators.
At least one parameter of an overloaded operator method must be a reference to an object of the class in which the operator is overloaded. This prevents you from changing how operators work on simple types.
Class ComplexTest
(Fig. 12.18) demonstrates the overloaded ComplexNumber
operators +
, -
and *
. Lines 14–27 prompt the user to enter two complex numbers, then use this input to create two ComplexNumber
s and assign them to variables x
and y
.
Lines 31–33 add, subtract and multiply x
and y
with the overloaded operators, then output the results. In line 31, we perform the addition by using the plus operator with ComplexNumber
operands x
and y
. Without operator overloading, the expression x
+ y
wouldn’t make sense—the compiler wouldn’t know how two objects of class Complex-Number
should be added. This expression makes sense here because we’ve defined the plus operator for two ComplexNumber
s in lines 29–34 of Fig. 12.17. When the two Complex-Number
s are “added” in line 31 of Fig. 12.18, this invokes the operator+
declaration, passing the left operand as the first argument and the right operand as the second argument. When we use the subtraction and multiplication operators in lines 32–33, their respective overloaded operator declarations are invoked similarly.
Each calculation’s result is a reference to a new ComplexNumber
object. When this new object is passed to the Console
class’s WriteLine
method, its ToString
method (Fig. 12.17, lines 22–26) is implicitly invoked. Line 31 of Fig. 12.18 could be rewritten to explicitly invoke the ToString
method of the object created by the overloaded plus operator, as in:
Console.WriteLine( "{0} + {1} = {2}", x, y, ( x + y ).ToString() );
This chapter introduced polymorphism—the ability to process objects that share the same base class in a class hierarchy as if they were all objects of the base class. The chapter discussed how polymorphism makes systems extensible and maintainable, then demonstrated how to use overridden methods to effect polymorphic behavior. We introduced the notion of an abstract class, which allows you to provide an appropriate base class from which other classes can inherit. You learned that an abstract class can declare abstract methods that each derived class must implement to become a concrete class, and that an application can use variables of an abstract class to invoke derived class implementations of abstract methods polymorphically. You also learned how to determine an object’s type at execution time. We showed how to create sealed
methods and classes. The chapter discussed declaring and implementing an interface as another way to achieve polymorphic behavior, often among objects of different classes. Finally, you learned how to define the behavior of the built-in operators on objects of your own classes with operator overloading.
You should now be familiar with classes, objects, encapsulation, inheritance, interfaces and polymorphism—the most essential aspects of object-oriented programming. Next, we take a deeper look at using exception handling to deal with runtime errors.