What’s in This Chapter
Wrox.com Downloads for This Chapter
Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/csharp5programmersref on the Download Code tab.
This chapter describes the basic concepts behind object-oriented programming (OOP). It explains how to define classes and how to derive one class from another. It also describes the three fundamental features of OOP programming languages: encapsulation, inheritance, and polymorphism. It explains how C# provides those features and what benefits you can gain from using them properly.
A class is a programming entity that packages the data and behavior of some sort of programming abstraction. It encapsulates the idea that it represents in a package that has a well-defined interface to code that lies outside of the package. The interface determines how other pieces of code can interact with objects defined by the class. The interface determines which pieces of data are visible outside of the class and which pieces of data are hidden inside the class.
The three main sets of characteristics of a class are the properties, methods, and events that it defines. The public (externally visible) properties, methods, and events let the program work with the class:
string
or int
), or it may be a more complex item (such as an array, list, or object containing its own properties, methods, and events). Properties determine some feature of an object such as its name, color, or behavior.For a concrete example, imagine a Job
class that represents a piece of work to be done by an employee. This class might have the properties shown in the following table.
Property | Purpose |
JobDescription | A string describing the job. |
EstimatedHours | The number of hours initially estimated for the job. |
ActualHours | The actual number of hours spent on the job. |
Status | An enumeration giving the job’s status (New , Assigned , InProgress , or Complete ). |
ActionTaken | A string describing the work performed, parts installed, and so forth. |
Customer | An object of the Customer class that describes the customer for whom the job is performed. (That class has properties such as Name , Address , PhoneNumber , and ContractNumber .) |
AssignedEmployee | An object of the Employee class that describes the employee assigned to the job. (That class has properties such as Name , PhoneNumber , EmployeeId , and SocialSecurityNumber .) |
The JobDescription
, EstimatedHours
, ActualHours
, Status
, and ActionTaken
properties are relatively simple string
and numeric values. The Customer
and AssignedEmployee
properties are objects themselves with their own properties, methods, and events.
This Job
class might provide the methods shown in the following table.
Method | Purpose |
AssignJob | Assigns the Job to an Employee |
PrintInvoice | Prints an invoice for the Customer after the job is completed |
EstimatedCost | Calculates and returns an estimated cost based on the customer’s service contract type and EstimatedHours |
The class could provide the events shown in the following table to keep the main program informed about the job’s progress.
Event | Purpose |
Created | Occurs when the Job is first created |
Assigned | Occurs when the Job is assigned to an Employee |
Rejected | Occurs if an Employee refuses to do the job, perhaps because the Employee doesn’t have the right skills or equipment to do the work |
Canceled | Occurs if the Customer cancels the job before it is started |
Finished | Occurs when the job is completed |
The class packages the data and behavior of some programming abstraction such as a Job
, Employee
, Customer
, Menu
, SquashMatch
, SoftwareProject
, or anything else you might want to manipulate as a single entity.
After you have defined a class, you can create as many instances of the class as you like. An instance of the class is an object of the class type. For example, the Job
class represents jobs in general. After you have defined the Job
class, you can make instances of the class to represent specific jobs. You could create instances to represent building a brick wall, planting a tree, or repairing a telephone switch. The process of creating an instance of a class is called instantiation.
There are a couple common analogies to describe instantiation. One compares the class to a blueprint. After you define the class, you can use it to create any number of instances of the class, much as you can use the blueprint to make any number of similar houses (instances).
The different houses have much in common. For example, the blueprint defines the number, size, and relative placement of the houses’ rooms. These are analogous to features defined by the class that apply to all the instances. (In the Job
class example, all instances have a PrintInvoice
method that prints an invoice. Although exactly what is printed depends on the instance’s properties.)
The houses can also have differences such as different colors, front doors, and appliances. Those correspond to the class’s property values. For example, a House
class could define ExteriorColor
and ExteriorTrim
properties that determine the color of each instance of the class.
A second analogy compares a class definition to a cookie cutter. After you create the cookie cutter, you can use it to make any number of cookies (instances). The cookie cutter (class) defines the cookies’ size and shape. Specific instances might have different properties such as thickness, dough type (chocolate chip, sugar, gingerbread, and so on), and frosting type (none, single color, and patterned).
Because all classes ultimately derive from the Object
class, every instance of every class is in some sense an Object
, so they are often simply called objects. If you don’t know or don’t care about an item’s class, you can simply refer to it as an object.
The following sections provide more details about the more important features provided by OOP languages in general and C# in particular.
A class’s public interface is the set of properties, methods, and events that are visible to code outside of the class. The class may also have private properties, methods, and events that it uses to do its job. For example, the Job
class described in the previous section provides an AssignJob
method. That method might call a private FindQualifiedEmployee
method that looks through an employee database to find someone who has the skills and equipment necessary to do the job. That routine is not used outside of the class, so it can be declared private.
The FindQualifiedEmployee
method should be declared private to hide it from code outside of the class. That makes the interface easier to understand and use. Making FindQualifiedEmployee
public would clutter the interface (and IntelliSense) with unnecessary information.
The class may also include private properties and events. These hidden properties, methods, and events are not part of the class’s public interface.
The class encapsulates the programming abstraction that it represents (a Job
in this ongoing example). Its public interface determines what is visible to the application outside of the class. It hides the ugly details of the class’s implementation from the rest of the world. Because the class hides its internals in this way, encapsulation is also sometimes called information hiding.
By hiding its internals from the outside world, a class prevents exterior code from messing around with those internals. It reduces the dependencies between different parts of the application, allowing only those dependencies that are explicitly permitted by its public interface.
Removing dependencies between different pieces of code makes the code easier to modify and maintain. If you must change the way the Job
class assigns a job to an employee, you can modify the AssignJob
method appropriately. The code that calls the AssignJob
routine doesn’t need to know that the details have changed. It simply continues to call the method and leaves the details up to the Job
class.
Removing dependencies also helps break the application into smaller, more manageable pieces. A developer who calls the AssignJob
method can concentrate on the job at hand, rather than on how the method works. This makes developers more productive and less likely to make mistakes while modifying the encapsulated code.
To make using a class as easy and safe as possible, you should hide as much information as possible about the class’s internals while still allowing outside code to do its job. If the external code doesn’t need to know something about how the class works, it shouldn’t.
To make public properties, methods, and events easy to use, you should make them simple (at least as seen from outside of the class) and tightly focused. A set of small methods that do a single simple task each is easier to use than a single method that can do a huge number of different things depending on its parameters.
For example, the Graphics
class provides methods to draw and fill various shapes. The DrawEllipse
, DrawRectangle
, DrawLine
, and DrawPolygon
methods outline different kinds of shapes. Similarly, the FillEllipse
, FillRectangle
, FillRegion
, and FillPolygon
methods fill different kinds of shapes.
You could probably replace all those methods with a single DrawShape
method that draws and outlines or fills various kinds of shapes depending on the parameters that it received. That would make the code harder to understand because you would have to carefully study the parameters in a specific call to figure out what it was doing. By using separate methods, the class makes the code obvious. If the program calls DrawRectangle
, it is drawing a rectangle.
Making methods perform a single, tightly focused task can be a difficult concept for beginning programmers. Adding more features seems like it would give developers more power, so you might think it would make their jobs easier. However, it often makes development more confusing and difficult. Instead of thinking in terms of giving the developer more power, you should think about the fact that this approach gives the developer more things to worry about and more ways to make mistakes. Ideally, you should not expose any more features than the developer actually needs.
Inheritance lets you derive a child class from a parent class. The child class inherits all the properties, methods, and events defined by the parent class. It can then modify, add to, or subtract from the parent class. Making a child class inherit from a parent class is also called deriving the child class from the parent, and subclassing the parent class to form the child class.
For example, suppose you define a Person
class that includes properties named FirstName
, LastName
, Street
, City
, State
, Zip
, Phone
, and Email
. It might also include a PrintEnvelope
method that prints an envelope addressed to the person represented by the Person
object.
Now you could derive the Employee
class from Person
. The Employee
class inherits the FirstName
, LastName
, Street
, City
, State
, Zip
, Phone
, and Email
properties. It then adds new EmployeeId
, SocialSecurityNumber
, OfficeNumber
, Extension
, and Salary
properties.
The Employee
class might override the Person
class’s PrintEnvelope
method, so it addresses the envelope to the employee’s office instead of the home address.
Now you can derive other classes from those classes to create a whole hierarchy of classes. You could derive the Manager
class from the Employee
class and add fields such as Secretary
that would refer to another Employee
object that represents the manager’s secretary. Similarly, you could derive a Secretary
class from Employee
that includes a reference to a Manager
object. You could derive ProjectManager
, DepartmentManager
, and DivisionManager
from the Manager
class; Customer
from the Person
class; and so on for other types of people that the application needs to use. Figure 11-1 shows an inheritance hierarchy containing these classes.
One of the key benefits of inheritance is code reuse. When you derive a class from a parent class, the child class gets to reuse the code you wrote for the parent class. For example, all the classes referred to in Figure 11-1 inherit their FirstName
, LastName
, Street
, City
, State
, Zip
, Phone
, and Email
properties from the Person
class, so they don’t need to implement those properties separately.
The Consultant
, Employee
, Customer
, and PreferredCustomer
classes also inherit the PrintEnvelope
method defined by the Person
class. The Employee
class overrides that method, so it doesn’t get to reuse the Person
class’s version. However, the new version does something different from the original version, so you have no choice but to write new code. In addition, the six classes that inherit from Employee
get to reuse the new version.
Code reuse not only saves you the effort needed to write the same code multiple times but also saves you time for debugging and maintenance. For example, if you find a bug in the PrintEnvelope
method, you need to fix it only in one place instead of in each of the classes that inherits it. That saves time and prevents you from fixing the bug in one class but forgetting to fix it in another.
Code reuse also helps you make modifications. Suppose you decide to change the Zip
property to store ZIP codes with the ZIP+4 format instead of the original 5-digit format. In that case you need to change only the representation in the Person
class, and all the other classes inherit the change.
Of course, then you’ll probably want to change the PrintEnvelope
method to use the new Zip
format. Again, you have to make the change only in the Person
class and the other classes inherit the change.
Similarly, if you need to add, modify, or delete a property or method, you need to make the change only in the class where it is defined, not in all the classes that inherit it. If you want to add a SendEmail
method, you need to add it only to the Person
class.
You can think about the relationship between a parent class and its child classes in a top-down or bottom-up way. Those two points of view lead to the ideas of refinement and abstraction.
Using a top-down view of inheritance, you can think of the child classes as refining the parent class. They provide extra detail that differentiates among different types of the parent class.
For example, suppose that you start with a class that covers a broad category such as Person
. The Person
class would need general fields that apply to all people such as FirstName
, LastName
, Address
, and PhoneNumber
.
Some kinds of people might need additional fields that don’t apply to every kind of person. For example, a school’s instructors might also need Title
, CoursesAssigned
, Advisees
, Office
, and OfficeHours
properties. Students might need StudentId
, Year
, CoursesTaken
, and GPA
fields.
You could add all these fields to the Person
class, but that would force the class to play two different roles. That would be complicated and confusing. It would also be somewhat wasteful because a Person
acting as an instructor wouldn’t use any of the student properties, and a Person
acting as a student wouldn’t use any of the instructor properties.
A better solution is to derive new Instructor
and Student
classes that refine the Person
class’s definition and add their new properties. Now each class represents a specific kind of person.
Using a bottom-up view of inheritance, you can think of the parent as abstracting the features of its children. The parent class gathers together the common features of the child classes. Because the parent class is more general than the child classes (it includes a larger group of objects), abstraction is sometimes called generalization.
For example, suppose you’re making a drawing application and you define various classes to represent shapes such as Circle
, Rectangle
, Ellipse
, and Polygon
. When you build the program, you may discover that these classes have a lot in common. For example, at times the program may need to find bounding rectangles for each of the objects. You may find that the objects have common ForegroundColor
and BackgroundColor
properties. You may want each class to provide a PointIsOver
method that returns true
if the given point is over the shape. You might also want to make each class raise a Clicked
event when the user clicks on its shape.
After you realize that these classes have so much in common, you might decide to extract those common features and move them into a common parent class named Drawable
. That class would define the common features that the other classes need to implement. For example, the Drawable
class could implement the ForegroundColor
and BackgroundColor
properties for the other classes to inherit.
Some of the child classes might override the default implementations in the Drawable
class. It might even be impossible for the Drawable
class to provide some of the features it defines. For example, each type of shape would need a different PointIsOver
method because you need to use different techniques to determine whether a point is over a rectangle, circle, or polygon. You could give the Drawable
class a default implementation, perhaps making it treat its shape as a rectangle, but you wouldn’t gain much by doing that. You would still need to override that method in every class except Rectangle
. Providing the default implementation would also incorrectly imply that the method is useful for something other than a single child class.
In this case you’re probably better off marking the method as abstract
and not providing a method body so that the child classes are required to override it. (For more information on the abstract
keyword, see the section “abstract” in Chapter 6.)
Making a Drawable
parent class also allows the program to treat all drawing objects uniformly as Drawable
s. For example, it can create a collection named AllDrawables
to hold the current picture’s drawing objects. Then when the user moves the mouse, the program could loop through the collection calling each object’s PointIsOver
method to determine whether the mouse was over an object. The section “Polymorphism” later in this chapter provides more details.
Often a program’s classes are defined by using refinement. You know the general categories of things the program needs such as Person
, Report
, and Job
. As you go through the project requirements, you can refine those to create the specific kinds of objects the program will need. Person
becomes Employee
, Manager
, Secretary
, Programmer
, Customer
, and Contractor
. Report
becomes TimeSheet
, JobRequirements
, ProgressReport
, and Invoice
.
During the project’s design phase, the classes tend to map naturally to the requirements, so it’s relatively easy to imagine their relationships.
Abstraction often arises during development. As you define and work with the classes, you may discover they have unexpected things in common. For example, suppose you’re working with the Person
class and the descendant classes described earlier. After a while you realize that Manager
and Programmer
are salaried positions but Secretary
is hourly. You could add a Salary
property to Manager
and Programmer
, and add HourlyRate
to Secretary
. Unfortunately, that would require a duplicate Salary
property. It would also mean you couldn’t treat Manager
and Programmer
objects uniformly.
The solution is to create a new SalariedEmployee
class to act as a new parent for the Manager
and Programmer
classes. Figure 11-2 shows the new class hierarchy.
Refinement is an important technique for building inheritance hierarchies, but it can sometimes lead to unnecessary refinement or over-refinement. For example, suppose that you define a Vehicle
class. You then refine this class by creating Auto
, Truck
, and Boat
classes. You refine the Auto
class into Wagon
and Sedan
classes and further refine those for different drive types (four-wheel drive, automatic transmission, and so forth). If you really go crazy, you could define classes for specific manufacturers, body styles, and colors.
The problem with this hierarchy is that it captures a lot more detail than you need. If you’re building a program to manage a fleet of delivery vehicles, then you probably need to know the vehicle’s capacity, but you probably don’t need to keep track of its manufacturer, transmission type, and color. You may want to track some of that information so that you can identify each vehicle properly, but you don’t need to make these separate classes.
As far as a delivery scheduling application is concerned, the color is irrelevant. Creating lots of unnecessary classes makes the object model harder to understand and can lead to confusion and mistakes.
Avoid unnecessary refinement by refining a class only when doing so lets you capture new information that the application actually needs to know.
Just as you can take refinement to ridiculous extremes, you can also overdo abstraction. Because abstraction is driven by code rather than intuition, it sometimes leads to unintuitive inheritance hierarchies.
For example, suppose that your application needs to mail purchase orders to vendors and invoices to customers. If the PurchaseOrder
and Invoice
classes have enough in common, you might decide to create a more abstract MailableItem
class that contains the code needed to create and mail a document to someone.
At some point you may discover that you also need to e-mail items to vendors or customers, so you create the idea of an EmailableItem
class. The MailableItem
and EmailableItem
classes probably share some common features because they both represent sending something to someone, so you may then be tempted to create an even more abstract SendableItem
class.
Although all this makes a sort of weird sense from a coding point of view, it doesn’t make much intuitive sense. That means programmers need to spend extra time figuring out what the classes are for and how to use them. It also means programmers are more likely to make mistakes that can slow development and create annoying bugs.
You can sometimes avoid over-abstraction by moving common features into libraries instead of creating separate classes to represent them. For example, you could make a library containing methods to send items via e-mail or postal mail. Then the program can call those methods as needed. Unless the program is some sort of a message tracking system, it probably doesn’t need an object to represent e-mails and letters.
Over-refinement and over-abstraction sometimes lead to inflated inheritance hierarchies. Sometimes the hierarchy grows tall and thin. Other times the design might include many separate but small inheritance hierarchies, a parent class with a single child, or a class that is never used.
If your inheritance hierarchy starts to take on one of these odd forms, you should spend some time to reevaluate your classes. Make sure each adds something meaningful to the application and that the relationships are reasonably intuitive. Too many classes with confusing relationships can drag a project to a halt as developers spend more time trying to understand the hierarchy than they spend writing code.
If you are unsure whether you should add a new class, leave it out. You can add it later if you discover that it is necessary after all. Usually it’s easier to add a new class than it is to remove an unnecessary class after developers have started using it.
In refinement you create child classes to differentiate among different kinds of objects. In abstraction you create a parent class to represent features that are common between two or more child classes. Both of these techniques create parent/child relationships between classes.
Another concept that sometimes masquerades as a parent/child relationship is containment. In containment, one object contains another object as an attribute.
The ideas of inheritance and containment are sometimes referred to as is-a and has-a relationships.
For example, a Student
is-a specific type of Person
object. The is-a relation maps naturally into inheritance hierarchies. Because a Student
is-a Person
, it makes sense to derive the Student
class from the Person
class.
In contrast a Person
object has-a street address, city, state, and ZIP code. The has-a relation maps most naturally to embedded items. For example, you could give the Person
class the Street
, City
, State
, and Zip
properties.
To see why the difference between the is-a and has-a relationships is important, suppose your program works with the Person
and Student
classes. Suppose it also works with FinancialAidPayment
, RegistrationFee
, and other classes that have street, city, state, and ZIP code information. Using abstraction, you might make a HasPostalAddress
class that contains those values. Then you could derive the Person
, FinancialAidPayment
, and RegistrationFee
classes as children of HasPostalAddress
. Unfortunately, that makes a rather unintuitive inheritance hierarchy. Deriving all those classes from the same parent class also makes them seem closely related when they are actually related only coincidentally.
A better solution is to encapsulate the postal address data in its own Address
class and then include an instance of that class in the Person
, FinancialAidPayment
, and RegistrationFee
classes.
You make a parent class through abstraction in part to avoid duplication of code. The parent class contains a single copy of the common variables and code, so the child classes don’t need to have their own separate versions for you to debug and maintain. Placing an instance of the Address
class in each of the other classes provides the same benefit without complicating the inheritance hierarchy.
Sometimes you can use either is-a or has-a to describe a relationship. For example, a Person
has-an address, but at the same time a Person
is-a thing that has an address. In cases like this, you need to use your common sense and intuition to decide which makes more sense. One hint is that it is easy to describe something that “has an address” but the phrase “is a thing that has an address” is more awkward and ill-defined.
You can also think about how a relationship might affect other classes. Are the Person
, FinancialAidPayment
, and RegistrationFee
classes truly closely related? Or do they just share some common information?
Adding new properties, methods, and events to a child class is easy. You simply declare them as you would in any other class. The parent class knows nothing about them, so the new items are added only to the child class.
The following code shows how you could implement the Person
and Employee
classes in C#.
// A general person.
public class Person
{
public string FirstName, LastName, Street, City, State, Zip, Phone, Email;
// Dial the phone.
public void DialPhone()
{
// Dial the number Phone...
}
}
// An employee.
public class Employee : Person
{
public string EmployeeId, SocialSecurityNumber, OfficeNumber, Extension;
public decimal Salary;
// Print a timesheet for the employee.
public void PrintTimesheet()
{
// Print the timesheet...
}
}
The Person
class defines name and address values. For simplicity, they are implemented as fields, but in practice you might want to make them properties. This class also defines a DialPhone
method that dials the person’s phone number.
The Employee
class is derived from the Person
class. (That’s what “: Person
” means at the end of the class declaration.) This class adds some new values and then defines a new PrintTimesheet
method.
There are two ways a child class can modify the behavior of a method defined in its parent class (or any ancestor class): hiding and overriding.
First, the child class can hide the parent class’s version of a method. To do that, add the keyword new
to indicate that you want to use a new version of the method. The following code shows how the Employee
class could hide the DialPhone
method to create a new version that includes the Employee
’s Extension
. The new
keyword is highlighted in bold.
// Dial the phone + extension.
public new void DialPhone()
{
// Dial the number Phone + Extension...
}
The second way a child class can modify a parent method is to override it.
This method requires some cooperation from the parent class. The parent class must mark the method with the virtual
or abstract
keyword. (The parent class can also have overridden the method. In that case, some ancestor class declared the original method with the virtual
or abstract
keyword.)
The following code shows the new version of the Person
class’s DialPhone
method with the virtual
keyword highlighted in bold.
// Dial the phone.
public virtual void DialPhone()
{
// Dial the number Phone...
}
The virtual
keyword indicates that this method can be overridden by descendant classes. The following code shows how the Employee
class could override the DialPhone
method.
// Dial the phone + extension.
public override void DialPhone()
{
// Dial the number Phone + Extension...
}
Overriding a method in this way is a powerful technique. When you invoke an overridden method for an object, you get the version defined by the object’s true class, even if you are referring to the object with a variable of an ancestor class.
In contrast, when you hide a method, you get only that version if you use a variable of the class that defined the new version.
These are confusing concepts, so here’s a detailed example. Consider the following stripped-down Person
and Employee
classes.
public class Person
{
public string Name;
public virtual void IsVirtual()
{
Console.WriteLine(Name + ": Person.IsVirtual");
}
public void HideMe()
{
Console.WriteLine(Name + ": Person.HideMe");
}
}
public class Employee : Person
{
public override void IsVirtual()
{
Console.WriteLine(Name + ": Employee.IsVirtual");
}
public new void HideMe()
{
Console.WriteLine(Name + ": Employee.HideMe");
}
}
The Person
class defines two methods. The IsVirtual
method is defined with the virtual
keyword. The HideMe
method is defined without the virtual
keyword.
The Employee
class overrides IsVirtual
and hides HideMe
.
All four of these methods simply display their object’s name, the method’s class, and the method’s name in the Console window.
Now consider the following code that uses these methods.
Person ann = new Person() { Name = "Ann" };
Employee bob = new Employee() { Name = "Bob" };
Person person = bob;
ann.IsVirtual();
bob.IsVirtual();
person.IsVirtual();
Console.WriteLine();
ann.HideMe();
bob.HideMe();
person.HideMe();
The code creates a Person
object and an Employee
object. It then creates a Person
variable and makes it refer to the Employee
object it just created. (You can do that because an Employee
is a kind of Person
.)
Next, the code calls each of the objects’ IsVirtual
methods. The object ann
is a Person
, so it calls the Person
version of IsVirtual
. The object bob
is an Employee
, so it calls the Employee
version of IsVirtual
.
The person
object has type Person
, but it actually refers to the Employee
object bob
. Because the Employee
class overrode the definition of this method, the person
object uses the version defined by the object’s true class, in this case Employee
. Even though it looks like the code is invoking Person.IsVirtual
, it actually invokes Employee.IsVirtual
.
The case with the HideMe
method is somewhat simpler. As before, the ann
and bob
objects call their respective class’s versions of the method.
The person
object has type Person
. Because this method is hidden in the Employee
class, the Person
class doesn’t know anything about that version. When the code calls the Person
object’s HideMe
method, it gets the Person
class’s version.
The following text shows the code’s output.
Ann: Person.IsVirtual
Bob: Employee.IsVirtual
Bob: Employee.IsVirtual
Ann: Person.HideMe
Bob: Employee.HideMe
Bob: Person.HideMe
The object person
invokes the Employee
class’s version of IsVirtual
(the third line of output) but it invokes the Person
class’s version of HideMe
(the last line of output).
There are two other keywords that affect overriding: abstract
and sealed
.
An abstract method is one that doesn’t have a method body. It defines the name, parameters, and return type of the method but doesn’t provide an implementation. (This is similar to the way interfaces define method signatures but don’t provide an implementation.)
If you give a class an abstract method, there’s a sort of placeholder in the class for the method. Because the placeholder is empty, you cannot create an instance of the class because it is incomplete. For that reason, if a class contains an abstract method, you must also mark the class as abstract.
Note that the class could define other non-abstract properties, methods, and events. (Some people call a non-abstract method, class, or other item concrete.) If it contains even one abstract method, then the class must be abstract.
An abstract method is also considered virtual, so a child class can override it to give it a method body. Then the child class can be concrete and you can create instances of it.
For an example, consider the following code.
public abstract class Report
{
public abstract void GenerateReport();
public abstract void DistributeReport();
}
public abstract class PersonnelReport : Report
{
public override void DistributeReport()
{
// Code to distribute the report to the personnel department...
}
}
public class Timesheet : PersonnelReport
{
public override void GenerateReport()
{
// Code to generate a timesheet.
}
}
The idea here is that the Report
class defines broad features that should be provided by any report. The class defines two abstract methods: GenerateReport
and DistributeReport
. Because the class contains an abstract method, it must also be abstract. (In this case that’s okay. The Report
class doesn’t represent a specific kind of report, so it doesn’t make sense to create an instance of one anyway.)
The PersonnelReport
class derived from Report
represents a report that should be sent to everyone in the personnel department. It overrides the DistributeReport
method to send the report to everyone in the department. The class still contains an abstract method (GenerateReport
) so it must be marked abstract.
The Timesheet
class derived from PersonnelReport
overrides the GenerateReport
method to actually create a report. This class has no abstract methods so this can be a concrete class.
The final keyword that affects overriding is sealed
. If you mark an overridden method as sealed
in a child class, then further descendant classes cannot override that method.
Sealed methods have a couple of odd quirks. First, you cannot seal a method in the class where it is originally defined, only in a class that overrides it. Second, you cannot override a sealed method in a descendant class but you can hide it, so sealing a method doesn’t completely protect it from later tampering.
For example, consider the following code.
public abstract class Animal
{
public abstract string FoodType();
}
public class Herbivore : Animal
{
public override sealed string FoodType()
{
return "Vegetation";
}
}
public class Koala : Herbivore
{
public new string FoodType()
{
return "Eucalyptus";
}
}
The Animal
class defines an abstract FoodType
method.
The Herbivore
class derived from Animal
overrides FoodType
to return the string "Vegetation"
.
The Herbivore
class marks the method sealed
so the Koala
class, which is derived from Herbivore
, cannot override FoodType
. It can, however, use the new
keyword to hide the Herbivore
implementation of the method with a new version.
Loosely speaking, polymorphism is the ability to treat one object as if it were an object of a different type. In OOP terms, it means that you can treat an object of one class as if it were from an ancestor class.
For example, suppose Employee
and Customer
are both derived from the Person
class. Then you can treat Employee
and Customer
objects as if they were Person
objects because, in a sense, they are. They are specific types of Person
objects. They inherited all the properties, methods, and events of a Person
object, so they should act as Person
objects.
C# enables you to make a variable of one class refer to an object of a derived class. In this example, you can use a Person
variable to hold a reference to an Employee
or Customer
object, as shown in the following code.
Employee employee = new Employee(); // Make an Employee
Customer customer = new Customer(); // Make a Customer
Person person = new Person(); // Make a Person.
person = employee; // Okay because an Employee is a type of Person.
person = customer; // Okay because a Customer is a type of Person.
employee = person; // Not okay because a Person is not necessarily an Employee.
One common reason to use polymorphism is to treat a collection of objects in a uniform way that makes sense in the context of the parent class. For example, suppose that the Person
class defines the FirstName
and LastName
properties. The program could define a collection named AllPeople
and add references to Customer
and Employee
objects to represent all the people that the program needs to manage. The code could then loop through the collection, treating each object as a Person
, as shown in the following code.
foreach (Person person in AllPeople)
{
Console.WriteLine(person.FirstName + " " + person.LastName);
}
When you use an object polymorphically, you can access only the features defined by the type of variable you actually use to refer to an object. For example, if you use a Person
variable to refer to an Employee
object, you can use only the features defined by the Person
class, not those added by the Employee
class.
If you know an object has a specific type, you can convert the object into that type before you work with it. For example, the following code loops through the AllPeople
list and takes special action for objects that are Employee
s.
foreach (Person person in AllPeople)
{
if (person is Employee)
{
// Do something Employee-specific with the person.
Employee employee = person as Employee;
...
}
}
The code uses the statement if (person is Employee)
to determine whether the variable person
can be treated as an Employee
object. It is important to realize that this doesn’t mean person
actually is an Employee
. It means person
is an Employee
or a class derived from Employee
, so it can be treated as an Employee
. For example, if the Manager
class is derived from Employee
, then person is Employee
returns true
for Manager
objects.
The code uses the following statement to convert the Person
looping variable into an Employee
variable.
Employee employee = person as Employee;
The as
keyword converts the variable person
into an Employee
if possible. If person
cannot be converted into an Employee
, then as
returns null
. (In this example, you know person
can be converted into an Employee
because the code just checked.) The as
statement is roughly equivalent to the following code.
Employee employee = null;
if (person is Employee) employee = (Employee)person;
Sometimes you might want to invoke the base class’s version of an overridden method. For example, suppose you override the Person
class’s ToString
method but you also want to be able to call the version defined in the Object
class from which Person
is derived. In that case you could add the following BaseToString
method to the Person
class.
public string BaseToString()
{
return base.ToString();
}
Here the keyword base
tells the program to use the version of ToString
defined in the parent class. Now the program can use the Object
class’s version of ToString
by calling the BaseToString
method.
The code inside the Person
class can invoke the parent class’s version of ToString
as shown here, but there is no way for the program to invoke an object’s overridden base class methods directly. If you don’t define a method similar to BaseToString
, the program cannot call the Object
class’s version of the method.
Also note that there is no way to call further up the inheritance chain.
Classes are programming abstractions that group data and related behavior in tightly encapsulated packages. After you define a class, you can create instances of that class.
Inheritance lets you derive a child class from a parent class, possibly adding, hiding, or overriding the parent’s behavior. The new
, virtual
, abstract
, override
, and sealed
keywords give you a fair amount of control over how methods are inherited and modified.
Interfaces give you another method for defining behavior that doesn’t follow an inheritance hierarchy. Like an abstract class, an interface lets you determine a class’s behavior without providing an implementation. Because interfaces don’t need to follow a derivation hierarchy, you can use them to implement nonhierarchical relationships such as multiple inheritance (interface inheritance).
Polymorphism enables you to treat an object as if it were of an ancestor’s type. For example, the following text shows the inheritance hierarchy for the Windows Forms PictureBox
control.
System.Object
System.MarshalByRefObject
System.ComponentModel.Component
System.Windows.Forms.Control
System.Windows.Forms.PictureBox
This means you can treat a PictureBox
as if it is a PictureBox
, Control
, Component
, MarshalByRefObject
, or Object
.
This chapter described these features and briefly mentioned how you can implement many of them in C#. The next chapter explains the syntax for creating classes and structures in greater detail. It also explains the differences between the two and how to decide when to use classes and when to use structures.
For the exercises that require you to build an inheritance hierarchy, draw abstract classes with dashed outlines and concrete classes with solid outliines.
PartTimeProgrammer
class to represent programmers who are paid hourly. How would you update the hierarchy?foreach (Person person in AllPeople)
{
if (person is Employee)
{
// Do something Employee-specific with the person.
Employee employee = person as Employee;
...
}
}
Rewrite the code so that it doesn’t use is
. (Hint: Place the as
statement before the new if
statement.) Which version is better?
Class | Properties |
Janitor | Name , Address , EmployeeId , Hours , HourlyPay |
ShiftManager | Name , Address , EmployeeId , Hours , Salary |
Customer | Name , Address , CustomerId , Pets |
Groomer | Name , Address , Hours , HourlyRate |
Supplier | Name , Address , SupplierId , Products |
StoreManager | Name , Address , EmployeeId , Hours , Salary |
SalesClerk | Name , Address , EmployeeId , Hours , HourlyPay |
Trainer | Name , Address , Classes , Hours , HourlyRate |
Are there some classes that should be abstract? Why? How would you make them abstract?
Pets
and Classes
, use string
as a placeholder. (If you’re not sure how to build the classes, read the next chapter and then come back to this exercise.)Human
, Elf
, or Dwarf
. Each player also has a weapon. Right now you have defined Sword
, Bow
, and Wand
, but you plan to add other weapons later such as other bladed weapons (Spear
, Dagger
, and Axe
), missile weapons (Sling
, Atlatl
, and Dart
), and magic weapons (Potion
, Pendant
, and VoodooDoll
).How would you design the Player
class to handle all this? Draw an inheritance hierarchy to show the relationships among the classes.
Fighter
and MagicUser
. A player can be one of the basic professions or can be a specialist. The initial Fighter
specialties are Knight
, Ranger
, and Archer
. The initial MagicUser
specialties are Illusionist
, Witch
, and Chemist
. How would you modify the definition of the Player
class? Draw the class inheritance hierarchy.Developer
class to handle this?Class | Properties |
Student | Name , Address , StudentId , CurrentClasses , PastClasses |
Instructor | Name , Address , EmployeeId , CurrentClasses , PastClasses |
TeachingAssistant | Name , Address , StudentId , EmployeeId , CurrentClasses , PastClasses , CurrentClassesTaught , PastClassesTaught |
ResearchAssistant | Name , Address , StudentId , EmployeeId , Sponsor |
(The Sponsor
property is the Instructor
for whom a ResearchAssistant
works.)
How would you implement these classes? Draw the inheritance hierarchy. (Hint: Change the names of some properties if that helps.)
CurrentClasses
, use string
as a placeholder. (If you’re not sure how to build the classes, read the next chapter and then come back to this exercise.) Make classes that implement an interface do so directly without delegation.IStudent
interface. What are the advantages and disadvantages of the approaches used in Exercises 9 and 10?Student
class implement the IStudent
interface directly and then make the TeachingAssistant
and ResearchAssistant
classes delegate the interface to a Student
object?LabAssistant
class from ResearchAssistant
. How would the new class handle the IStudent
interface?