Team LiB
Previous Section Next Section

More on Methods

We've discussed methods in a fair amount of detail in this chapter and in Part One of this book. Now it's time to round out the discussion with a few more important observations about methods.

Message Chaining

In object-oriented programming languages like C#, it's quite commonplace to form complex expressions by concatenating one method call onto another via dot notation, a mechanism known as message chaining. Here's one hypothetical example:

Student s = new Student();
s.Name = "Fred";

Professor p = new Professor();
p.Name = "John";

Course c = new Course();
c.Name = "Math";


s.setFacultyAdvisor(p);
p.setCourseTaught(c);

Course c2 = new Course();
// A message "chain".
c2.Name = "Beginning " + (s.GetFacultyAdvisor().GetCourseTaught().Name);

As we saw in Chapter 1, we evaluate expressions from innermost to outermost parentheses, left to right, so let's evaluate the expression in the last line of this snippet:

  1. Looking for the deepest level of nested parentheses, we see that part of the expression is two sets of parentheses deep, so we evaluate the left-most deepest subexpression first:

            s.GetFacultyAdvisor()
    

    which returns a reference to Professor p.

  2. Next, we apply the GetCourseTaught() method to this Professor:

                  p.GetCourseTaught()
    

    which returns a reference to Course c.

  3. Next, we access the Name property of this Course:

                  c.Name
    

    which returns the string value "Math".

  4. We've now completed evaluating the expression enclosed within the innermost set of parentheses, effectively giving us the equivalent expression

                  c2.Name = "Beginning " + "Math";
    

So, we see in the final analysis that the outcome of the complex expression is to assign the name "Beginning Math" to Course object c2.

We'll see many such method chains in the SRS code.

Method Hiding

In Chapter 5, we learned that a derived class can override a method that has been declared as virtual in a base class: the derived class declares a new version of the base class method with the same signature, and includes the override keyword in the method declaration.

There is also a second way to replace the logic of a base class method, even one that hasn't been declared virtual in the base class, via a technique known as method hiding.

To "hide" a base class method, the derived class must define a method with the same signature using the new keyword in the method declaration in lieu of the override keyword. Here is a simple example in which the derived Student class hides the PrintDescription method first declared by the Person base class:

public class Person
{
  // details omitted

  // Note: no virtual keyword -- no provision for overriding was made
  // by the designers of the Person class.
  public void PrintDescription() {
    // details omitted
  }
}

public class Student : Person
{
  // The Student class HIDES the PrintDescription()
  // method from the Person class via the use of the
  // new keyword.
  public new void PrintDescription() {
    // details omitted
  }
}

Any base class method—whether virtual or not—can be hidden; thus, we have a work-around (of sorts) if we find ourselves in a position of wanting to modify the behavior of a method derived from a base class that wasn't "prepped" for overriding by the original designer of the base class via the inclusion of the virtual keyword in the base class's method signature. Note, however, that hiding a nonvirtual base class method differs from truly overriding a virtual base class method in one very significant way, having to do with the notion of polymorphism that we discussed in Chapter 7. Let's explore this issue.

Method Hiding and Polymorphism

We learned in Chapter 7 that, by virtue of inheritance plus overriding, the runtime identity of an object controls what version of a method will be executed for a given object, a phenomenon known as polymorphism. For example, assuming that ChooseMajor is a virtual method in the Student class that has been overridden by both the GraduateStudent and UndergraduateStudent derived classes, the version of ChooseMajor invoked on s in the following code will depend on what type of Student s is:

// Iterating through an ArrayList of Students.
for (int i = 0; i < students.size(); i++) {
  Student s = (Student) students[i];

  // This next line of code is said to be polymorphic:
  // If s refers to a GraduateStudent at run time, the GraduateStudent version
  // of the ChooseMajor method will be executed; and, if s refers to an
  // UndergraduateStudent at run time, the UndergraduateStudent version of
  // ChooseMajor will be executed.
  s.ChooseMajor();
}

By contrast, the version of a hidden method that is run for a given object is "locked in" at compile time. Let's now assume that ChooseMajor is a nonvirtual method in the Student class, and is hidden (not overridden) by both the GraduateStudent and UndergraduateStudent derived classes. While the following two invocations of ChooseMajor will indeed be of the derived classes' respective versions, because we're invoking that method on reference variables explicitly declared to be a GraduateStudent and an UndergraduateStudent, respectively:

GraduateStudent g = new GraduateStudent();
g.ChooseMajor(); // GraduateStudent version of this method will execute

UndergraduateStudent s = new UndergraduateStudent();
s.ChooseMajor(); // UndergraduateStudent version of this method will execute

polymorphism will not be enabled in the next example—the Student base class's version of the ChooseMajor method will be invoked for both GraduateStudents and UndergraduateStudents because s is declared to be of type Student at compile time:

// Iterating through an ArrayList of Students.
for (int i = 0; i < students.size(); i++) {
  Student s = (Student) students[i];

  // This next line of code is NOT polymorphic:
  // Regardless of whether s refers to a GraduateStudent at run time or to
  // an Undergraduate student, the Student version of the ChooseMajor method
  // will be executed.
  s.ChooseMajor();
}

We mentioned earlier that any method, virtual or not, can be hidden. Why might we want to hide a virtual base class method vs. overriding it if we lose the benefit of polymorphism? Because hidden methods yield better performance (they run faster) than virtual methods.

Final Notes Regarding Method Hiding

A few final points:

  • The original base class version of a hidden method can be called from within the "new" derived/hidden method using the base keyword, just as when overriding.

  • A derived class's "hidden" method can have a different return type than the base class version it's hiding.

  • An abstract method can't be hidden.

Overriding and Abstract Classes, Revisited

We learned in Chapter 7 that a class derived from an abstract class can be made concrete by providing nonabstract implementations of all of the abstract methods declared by the abstract base class. As it turns out, it's also possible to go the other direction as well: that is, a derived class can override a nonabstract method declared in a base class with an abstract method. Let's look at an example using predefined C# classes.

We know that every C# class implicitly derives from the Object class of the System namespace. One of the methods declared by the Object class is ToString, a nonabstract, virtual method that we discussed earlier in this chapter. It's perfectly acceptable to define a Person class that implicitly derives from Object (as all classes do), but which overrides the nonabstract ToString method it inherits from Object with an abstract version:

// Derives from Object implicitly.
public abstract class Person
{
  // We're overriding the nonabstract Object class's ToString method
  // with an ABSTRACT version.
  public abstract override string ToString();

  // Other Person class details omitted.
}

There are several things to note about the Person class:

  • As discussed earlier in this chapter, derivation from the Object class is implicit, so we don't need the syntax : Object in the Person class declaration.

  • Second, because the ToString method as defined in the Person class is abstract, the Person class itself is declared to be abstract.

  • Finally, because the Person's ToString method is overriding the Object class's version of that method, the override keyword is used in the method declaration.

Why might we wish to override a nonabstract method with an abstract one? The answer is to force derived classes of Person to implement their own class-specific versions of the ToString method. That is, we don't want any of the future classes derived from Person to be "lazy" by simply inheriting the default behavior of the Object class's ToString method—we want to effectively "erase" the details of how this behavior is carried out. We'll in fact do this very thing when we create the Person class in Chapter 14 that will be used in conjunction with the SRS application. The Person class will declare an abstract ToString method that will be overridden by its derived classes.


Team LiB
Previous Section Next Section