5.2. Rules for Deriving Classes: The "Do's"

When deriving a new class, we can do several things to specialize the base class that we are starting out with:

  • We can extend the base class by adding members. In our GraduateStudent example, we added four members: two fields—undergraduateDegree and undergraduateInstitution—and two properties—UndergraduateDegree and UndergraduateInstitution.

  • We can specialize the way that a derived class performs one or more of the methods inherited from its base class. For example, when "general" students enroll for a course, the students may first need to ensure that

    • They have taken the necessary prerequisite courses.

    • The course is required for the degree that the student is seeking.

    • When graduate students enroll for a course, on the other hand, they may need to do both of these things as well as to ensure that their graduate committee feels that the course is appropriate.

Specializing the way that a derived class implements a particular method—as compared with the way that its base class implemented the method—is accomplished via a technique known as overriding.

5.2.1. Overriding

Overriding involves "rewiring" how a method or property works internally in a derived class, without changing the interface to/signature of that method as declared in the base class. For example, suppose that in a simple Student class a Print method were declared to print out the values of all of a student's fields:

public class Student
{
  // Fields.
  private string name;
  private string studentId;
  private string majorField;
  private double gpa;
  // etc.

  // Properties for each field would also be provided; details omitted.
  public void Print() {
						// Print the values of all of the fields that the Student class
						// knows about; note use of get accessors.
						Console.WriteLine("Student Name:  " + Name + "
" +
						"Student No.:  " + StudentId + "
" +
						"Major Field:  " + MajorField + "
" +
						"GPA:  " + Gpa);
						}
}

The Print method shown in the preceding code assumes that properties have been written for all the Student class fields. The Print method uses a property to access the value of the associated field, instead of accessing the fields directly:

// This example accesses fields directly by name, bypassing the
						// associated properties; this approach is discouraged.
						Console.WriteLine("Student Name:  " + name + "
" +
						"Student No.:  " + studentId + "
" +
						"Major Field:  " + majorField + "
" +
						"GPA:  " + gpa);

Using get accessors within a class's own methods reflects a "best practice" discussion that we had in Chapter 4; it allows us to take advantage of any value checking or other operations that the get accessor may provide.

By virtue of inheritance, all the derived classes of Student will inherit this method. However, there is a problem: we added two new fields to the GraduateStudent-derived class: undergraduateDegree and undergraduateInstitution. If we take the "lazy" approach of just letting GraduateStudent inherit the Print method of Student as is, then whenever we invoke the Print method for a GraduateStudent, all that will be printed are the values of the four fields inherited from Studentname, studentId, majorField, and gpa—because these are the only fields that the Print method has been explicitly programmed to print the values of. Ideally, we would like the Print method, when invoked for a GraduateStudent, to print these same four fields plus the two additional fields of undergraduateDegree and undergraduateInstitution.

With an OOPL, we are able to override, or supersede, the Student version of the Print method that the GraduateStudent class has inherited. In order to override a base class's method in C#, the method to be overridden first has to be declared to be a virtual method in the base class using the virtual keyword. Declaring a method to be virtual means that it may be (but doesn't have to be) overridden by a derived class.

The derived class can then override the method by reimplementing the method with the override keyword in the derived class's method declaration. The overridden method in the derived class must have the same accessibility, return type, name, and parameter list as the base class method it's overriding.

Let's look at how the GraduateStudent class would go about overriding the Print method of the Student class:

public class Student
{
  // Fields.
  private string name;
  private string studentId;
  private string majorField;
  private double gpa;
  // etc.

  // Properties for each field would also be provided; details omitted.

  //  The Student class Print method is declared to be virtual
						public virtual void Print() {
    // Print the values of all the fields that the Student class

// knows about; again, note the use of get accessors.
    Console.WriteLine("Student Name:  " + Name + "
" +
                       "Student No.:  " + StudentId + "
" +
                       "Major Field:  " + MajorField + "
" +
                       "GPA:  " + Gpa);
  }
}

public class GraduateStudent : Student
{
  private string undergraduateDegree;
  private string undergraduateInstitution;

  // Properties for each newly added field would also be provided;
  // details omitted.
  // We are overriding the Student class's Print method.
						public override void Print() {
						// We print the values of all the fields that the
						// GraduateStudent class knows about:  namely, those that it
						// inherited from Student plus those that it explicitly declares.
						Console.WriteLine("Student Name:  " + Name + "
" +
						"Student No.:  " + StudentId + "
" +
						"Major Field:  " + MajorField + "
" +
						"GPA:  " + Gpa + "
" +
						"Undergrad. Deg.:  " + UndergraduateDegree + "
" +
						"Undergrad. Inst.:  " + UndergraduateInstitution);
						}
}

The GraduateStudent class's version of Print thus overrides, or supersedes, the version that would otherwise have been inherited from the Student class.

The preceding example is less than ideal because the first four lines of the Print method of GraduateStudent duplicate the code from the Student class's version of Print. You've probably started to sense that redundancy in an application is to be avoided because redundant code represents a maintenance nightmare: when we have to change code in one place in an application, we don't want to have to remember to change it in countless other places or, worse yet, forget to do so and wind up with inconsistency in our logic. We like to avoid code duplication and encourage code reuse in an application whenever possible, so our Print method for the GraduateStudent class would actually be written as follows:

public class GraduateStudent : Student
{
  // details omitted...

  public override void Print() {
    // Reuse code by calling the Print method defined by the Student
						// base class...
						base.Print();

//...and then go on to print this derived class's specific fields.
						Console.WriteLine("Undergrad. Deg.:  " + UndergraduateDegree + "
" +
						"Undergrad. Inst.:  " + UndergraduateInstitution);
  }
}

We use a C# keyword, base, as the qualifier for the method name when we want to invoke a version of a method that was defined in a base class:

base.methodName(arguments);

Sometimes, in a complex inheritance hierarchy, we have occasion to override a method multiple times. In the hierarchy shown in Figure 5-6

  • Root class A (Person) declares a method with the following header that prints out all of the fields declared for the Person class:

    public virtual void Print()

  • Derived class B (Student) overrides this method, changing the internal logic of the method body to print not only the fields inherited from Person, but also those that were added by the Student class itself. The overridden method would have the following header:

    public override void Print()

  • Derived class E (GraduateStudent) overrides this method again, to print not only the fields inherited from Student (which include those inherited from Person) but also those that were added by the GraduateStudent class itself. The GraduateStudent version of the Print method would also use the override keyword:

    public override void Print()

Note that, in all cases, the accessibility, return type, and method signature must remain the same—public void Print()—for overriding to take place.

Figure 5.7. A method may be overridden multiple times within a class hierarchy.

  • Under such circumstances, any class not specifically overriding a given method itself will inherit the definition of that method used by its most immediate ancestor.

5.2.2. Rules for Deriving Classes: The "Don'ts"

When deriving a new class, there are some things that we should not attempt to do. (And, as it turns out, OOPLs will actually prevent us from successfully compiling programs that attempt to do most of these things.)

5.2.2.1. Don't Change the Semantics of a Member

We shouldn't change the semantics—that is, the intention, or meaning—of a member. For example:

  • If the Print method of a base class such as Student is intended to display the values of all of an object's fields on the computer screen, then the Print method of a derived class such as GraduateStudent shouldn't, for example, be overridden so that it directs all of its output to a file instead.

  • If the name field of a base class such as Person is intended to store a person's name in "last name, first name" order, then the name field of a derived class such as Student should be used in the same fashion.

5.2.2.2. Don't Eliminate Members

We shouldn't try to effectively eliminate members inherited from base classes by ignoring them in derived classes. To attempt to do so would break the spirit of the "is a" hierarchy. By definition, inheritance requires that all members of all base classes of a class A must also apply to class A itself in order for A to truly be a proper base class. If a GraduateStudent could eliminate the degreeSought field that it inherits from Student, for example, is a GraduateStudent really a Student after all?

5.2.2.3. Don't Change the Type of a Property

A derived class can override a base class property, but the type of the property must remain the same as the base class version of that property. For example, if the Person class declared a Birthdate property of type string:

public class Person
{
  // Details omitted.

  // Base class introduces a property.
  public virtual string Birthdate {
							get {
							// details omitted.
							}
							}
}

then a Student class that derives from Person could not, in overriding the Birthdate property, change its type to, say, DateTime:

public class Student : Person
{
  // Details omitted.

  // Derived class overrides property, attempting to modify its type in the
  // process.
  public override DateTime Birthdate {  // this won't compile
							get {
							// details omitted.
							}
							}
}

If we tried to compile the Student class, the following compiler error would occur:

error:  'Student.Birthdate' type must be 'string' to match overridden
member 'Person.Birthdate'

NOTE

It turns out that a derived class can change the type of a base class property by hiding the base class property. We'll discuss property and method hiding in Chapter 13.

5.2.2.4. Don't Attempt to Change a Method Header

For example, if the Print method inherited by the Student class from the Person class has the header public void Print(), where the method takes no arguments, then the Student class can't change this method to require an argument, say, public void Print(int noOfCopies). To do so is to create a different method entirely, due to another C# language feature known as overloading, discussed next.

5.2.3. Overloading

Overloading allows two or more different methods belonging to the same class to have the same name as long as they have different argument signatures (as defined in Chapter 4). An overloaded method is thus a different concept from an overridden method in which a derived class reimplements a method with the same header declared in a base class. As an example of overloaded methods, the Student class may legitimately define the following five different Print methods:

void Print(string fileName) -  a single parameter
void Print(int detailLevel) -  a different parameter type from above
void Print(int detailLevel, string fileName) -  two parameters
int Print(string reportTitle, int maxPages) -  different parameter types
bool Print()-  no parameters

Hence the Print method is said to be overloaded. Note that all five of the signatures differ in terms of their argument signatures:

  • The first takes a single string as an argument.

  • The second takes a single int.

  • The third takes two arguments: an int and a string.

  • The fourth takes two arguments: a string and an int (although these are the same parameter types as in the previous signature, they are in a different order).

  • The fifth takes no arguments at all.

So all five of these headers represent valid different methods, and all can coexist happily within the Student class without any complaints from the compiler! We can pick and choose among which of these five "flavors" of Print method we'd like a Student object to perform based on what form of method is invoked on the Student object:

Student s = new Student();

// Calling the version that takes a single string argument.
						s.Print("output.rpt");
						// Calling the version that takes a single int argument.
						s.Print(2);
						// Calling the version that takes two arguments, an int and a string.
						s.Print(2, "output.rpt");

// etc.

The compiler is able to unambiguously match up which version of the Print method is being called in each instance based on the argument signatures.

This example hints at something significant; only the parameter types and their order matter when determining when a new method can be added because the names of the parameters and the return type of the method aren't evident in a method call. For example:

  • We already know that we can't introduce the following additional method as a sixth method of Student:

    bool Print(int levelOfDetail)

    Its argument signature—a single int—duplicates the argument signature of an existing method despite the fact that both the return type (bool vs. int) and the parameter names are different in the two headers:

    int Print(int detailLevel)

  • Let's suppose for a moment that we could introduce the bool Print(int levelOfDetail) header as a sixth "flavor" of the Print method for the Student class. If the compiler were to then see a method call in client code of the following form, it couldn't sort out which of these two methods were to be invoked because all we see in a method call like this is (a) the method name and (b) the argument type (an integer literal, in this case):

    s.Print(3);

    So, to make life simple, the compiler prevents this type of ambiguity from arising by preventing classes from declaring methods with identical signatures in the first place.

Constructors, which as we learned in Chapter 4 are a special type of function member used to initialize objects, are commonly overloaded. Here is an example of a class that provides several overloaded constructors:

public class Student
{
  private string name;
  private string id;
  private int age;
  // etc.

  // Constructor #1.
						public Student() {
						// Assign default values to selected fields, if desired.
						id = "?";
						// Those which aren't explicitly initialized in the constructor
						// will automatically assume
						// the zero-equivalent value for their respective type.
						}
						// Constructor #2.
						public Student(string s) {
						id = s;
						}
						// Constructor #3.
						public Student(string s, string n, int i) {
						id = s;
						name = n;
						age = i;
						}

  // etc. -- other methods omitted from this example
}

By providing different "flavors" of constructor, we made this class more flexible by giving client code a variety of constructors to choose from.

The ability to overload methods allows us to create an entire family of similarly named methods that do essentially the same job, but which accept different types of arguments. Think back to Chapter 1, where we introduced the Write method, which is used to display printed output to the console. As it turns out, there is not one, but many Write methods; each one accepts a different argument type (Write(int), Write(string), Write(double), and so on). Using a set of overloaded methods named Write is much simpler and neater than having to use differently named methods such as WriteString, WriteInt, WriteDouble, and so on.

Note that there is no such thing as "field overloading"; that is, if a class tries to declare two fields with the same name:

public class SomeClass
{
  private string foo;
						private int foo;
  // etc.

the compiler will generate an error message:

SomeClass.cs(5,15): error: The type 'SomeClass' already contains
 a definition for 'foo'

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset