5.3. Exploring C# Polymorphism

Let's say that we are doing some statistical forecasting of book profits for a retail store. Before we do the GUI and the backend database work, we model the core components of this imaginary application. A Book class seems like an obvious place to start. Listing 5.1 shows the Book class.

Listing 5.1. The Book Class (C#)
public class Book {

  protected string isbn;
  protected float costprice;
  protected int copiessold;
  protected float sellingprice;
  protected float popularityfactor;

  public Book (string isbn, float costprice,
      float sellingprice, int copiessold, float popularityfactor)
  {
    this.isbn = isbn;
    this.costprice = costprice;
    this.sellingprice = sellingprice;
    this.copiessold = copiessold;
    this.popularityfactor = popularityfactor;
  }

  public float EstimatedProfit() {
    return TheoreticalProfit() * popularityfactor;
  }

  protected float TheoreticalProfit() {
    return (sellingprice  costprice)*copiessold;
  }

  public string GetGenre() {
    return "Book";
  }

  //static void Main(string[] args) {}
}

The following can be observed from this first implementation:

  • Book class is declared as public. Because this class is the core component of the application, it makes sense that the class be accessible to all parts of the application. The Book class has the following fields: isbn, costprice, sellingprice, copiessold, and popularityfactor. The fields are all protected. We anticipate that the subclasses will need access to these fields to make various calculations.

  • Most methods are public except for the TheoreticalProfit() method, which is protected.

  • The GetGenre() method is hardcoded for now to return the string "Book".

Listing 5.2 shows a subclass of the Book class.

Listing 5.2. Subclass of the Book Class (C#)
public class RomanceNovel : Book {

  public RomanceNovel (string isbn, float costprice,
         float sellingprice, int copiessold,
            float popularityfactor) :base (isbn,
               costprice, sellingprice, copiessold,
                                              popularityfactor) {
  }

  public float EstimatedProfit() {

    if (popularityfactor < 0.5f) {
      return TheoreticalProfit() * (1-popularityfactor);
    } else {
      return TheoreticalProfit() * (1-popularityfactor);
    }
  }
  public string GetGenre() { return "Romance"; }
}

The method TheoreticalProfit() and the fields are protected so that they can be used by the subclass. Note that the subclass calls the superclass constructor using the base keyword. Listing 5.3 shows the driver class that will serve as the entry point.

Listing 5.3. Entry Point for Listings 5.1 and 5.2 (C#)
using System;

public class BookTest {

  static void Main(string[] args) {
    Console.WriteLine("Reference = Book, Object = Book");
    Book book = new Book("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
    Console.WriteLine("Reference = RomanceNovel,
                                       Object = RomanceNovel");
    RomanceNovel novel = new RomanceNovel("85745", 45.6f,
                                             65.5f, 200, 0.2f);
    Console.WriteLine(novel.EstimatedProfit());
    Console.WriteLine("Reference = Book, Object = RomanceNovel");
    book = new RomanceNovel("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
  }

}

The output of Listing 5.3 is as follows:

Reference = Book, Object = Book
Calling Constructor of Book class
Calling EstimatedProfit of Book class
796.0001

Reference = RomanceNovel, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184

Reference = Book, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of Book class
796.0001

The first example uses a Book reference and assigns a Book object to it. Therefore, Book's constructor gets called and Book's EstimatedProfit() method gets called. No surprises there.

The second example assigns a RomanceNovel object to a RomanceNovel reference. The RomanceNovel constructor calls the superclass constructor first. The EstimatedProfit() of the RomanceNovel class gets called. So far, the behavior is similar to that of Java.

The third example is what makes Java programmers gasp and wonder what is going on. The reference is that of the Book class, but the object created is that of RomanceNovel; Java fundamentals suggest that the method being called (EstimatedProfit) should be on the object instantiated and not on the reference to which it is assigned.

The reason for this apparent discrepancy is that all C# methods are nonvirtual by default. Java methods are virtual by default, and this means that the virtual dispatching mechanism will invoke the method on the object instantiated and not on the reference to which it is assigned. That is not the case with C#. The EstimatedProfit() methods are not virtual, and therefore the method called will be on the reference. To get Java-like behavior you would have to cast the book reference to a RomanceNovel reference and then call the method on this new reference:

RomanceNovel rm = (RomanceNovel) book;
rm.EstimatedProfit();

C#, unlike Java, does not assume that subclasses should always be able to override a superclass's methods. As mentioned in earlier chapters, subclasses have the option to inherit the superclass implementation as is, hide the implementation, or override it. The default behavior, as you have seen from this shocking example, is that subclasses will neither hide nor override the superclass implementation without explicitly doing so (more about this later).

What are the implications of this important difference between C# and Java? Let's say you have an array of Account objects that you have treated generically by assigning all of them to the Account reference. The Account class has a method CalculateBalance that remains the same for all types of accounts. The Account class and the method have been working perfectly for years, and things are running smoothly. Now let's say someone decides to write a subclass of your golden Account class. The developer overrides the implementation of CalculateBalance() and writes a corrupt version of that method. Now suddenly your application behaves differently after years of consistency, simply because of a subclass that decided to change the way things get done. This example might seem far-fetched, but it is always a possibility.

Both C# and Java are derivatives of the C++ language. C++ has the virtual keyword, which is used for signaling explicit virtual dispatching. C# is closer to C++ in this regard. In Java, all methods are by default virtual and polymorphic. This makes for easier extension, but it is also slower because late-binding virtual method calls are usually slower than early-binding nonvirtual method calls.

5.3.1. Using the virtual and override Keywords

In OOP, the virtual keyword means that when a call to a method is made, the compiler should look at the real type of the object (and not only the reference) and then should call the appropriate method on the object. To activate virtual dispatching, we now add the virtual keyword to Listing 5.1 and add the override keyword to the subclass, as shown in Listings 5.4 and 5.5.

Listing 5.4. Listing 5.1 with the virtual Keyword (C#)
using System;

public class Book {

  private string isbn;
  protected float costprice;
  protected int copiessold;
  protected float sellingprice;
  protected float popularityfactor;

  public Book (string isbn, float costprice, float sellingprice,
                        int copiessold, float popularityfactor) {
    this.isbn = isbn;
    this.costprice = costprice;
    this.sellingprice = sellingprice;
    this.copiessold = copiessold;
    this.popularityfactor = popularityfactor;
    Console.WriteLine("Calling Constructor of Book class");
  }

  public virtual float EstimatedProfit() {
    Console.WriteLine("Calling EstimatedProfit of Book class");
    return TheoreticalProfit() * popularityfactor;
  }

  protected float TheoreticalProfit() {
    return (sellingprice  costprice)*copiessold;
  }

  public string GetGenre() {
    return "Book";
  }
}

Listing 5.5. Listing 5.2 with the override Keyword (C#)
using System;

public class RomanceNovel : Book {

  public RomanceNovel (string isbn, float costprice,
         float sellingprice, int copiessold,
            float popularityfactor) : base (isbn,
               costprice, sellingprice, copiessold,
                                              popularityfactor) {

    Console.WriteLine("Calling Constructor of RomanceNovel class");
  }

  public override float EstimatedProfit() {
    Console.WriteLine("Calling EstimatedProfit of RomanceNovel
                                                        class");
    if (popularityfactor < 0.5f) {
      return TheoreticalProfit() * (1-popularityfactor);
    } else {
      return TheoreticalProfit() * (1-popularityfactor);
    }
  }

  public string GetGenre() { return "Romance"; }
}

The entry point program (Listing 5.3), when run along with Listing 5.4 and Listing 5.5 instead of Listing 5.1 and Listing 5.2, now gives the following output:

Reference = Book, Object = Book
Calling Constructor of Book class
Calling EstimatedProfit of Book class
796.0001
Reference = RomanceNovel, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184
Reference = Book, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184

What happens when the reference on the left side is an interface and not the base class or the derived class? Let's create an interface, IBook (Listing 5.6).

Listing 5.6. Interface IBook (C#)
public interface IBook {
  float EstimatedProfit();
}

Next, we have the Book and the RomanceNovel classes implement that interface. Note that we have not shown the complete listings of the modified Book and RomanceNovel classes because only one line has changed in the classes:

public class Book : IBook
public class RomanceNovel : Book, IBook

Next, we rewrite the entry point class (Listing 5.3) so as to take into account our IBook interface, as shown in Listing 5.7.

Listing 5.7. Entry Point Program Using the IBook Interface (C#)
using System;
public class BookTest {

  static void Main(string[] args) {
    Console.WriteLine("Reference = IBook, Object = Book");
    IBook book = new Book("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
    Console.WriteLine("Reference = IBook, Object =
    RomanceNovel");
    IBook novel = new RomanceNovel("85745", 45.6f, 65.5f,
                                                   200, 0.2f);
    Console.WriteLine(novel.EstimatedProfit());
  }
}

Here is the output of Listing 5.7:

Reference = IBook, Object = Book
Calling Constructor of Book class
Calling EstimatedProfit of Book class
796.0001
Reference = IBook, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184

When the reference is that of an interface, then the methods called will always be those of the object instantiated.

So far, we have seen the use of the override and virtual keywords in relatively shallow class hierarchies. How do things work out when there are several levels of inheritance? To understand this we create a subclass of RomanceNovel (Listing 5.5). Listing 5.8 shows this subclass.

Listing 5.8. A Subclass of the RomanceNovel class (C#)
using System;

public class MillsAndBoone : RomanceNovel {

  public MillsAndBoone (string isbn, float costprice,
           float sellingprice, int copiessold,
              float popularityfactor) :base (isbn,
                 costprice, sellingprice, copiessold,
popularityfactor) {
    Console.WriteLine("Calling Constructor of
                             MillsAndBoone class");
  }
  public override float EstimatedProfit() {
    Console.WriteLine("Calling EstimatedProfit of
                             MillsAndBoone class");
    if (popularityfactor < 0.5f) {
      return TheoreticalProfit() * (1-popularityfactor);
    } else {
      return TheoreticalProfit() * (1-popularityfactor);
    }
  }

  public string GetGenre() { return "Romance"; }
}

The driver class (Listing 5.3) is now modified to accommodate this new subclass. The new driver class is shown in Listing 5.9.

Listing 5.9. A Driver Class Using the Subclass of the RomanceNovel Class (C#)
using System;

public class BookTest {

  static void Main(string[] args) {
    Console.WriteLine("Reference = Book, Object = Book");
    Book book = new Book("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
    Console.WriteLine("Reference = RomanceNovel,
                            Object = RomanceNovel");
    RomanceNovel novel = new RomanceNovel("85745", 45.6f,
                                             65.5f, 200, 0.2f);
    Console.WriteLine(novel.EstimatedProfit());
    Console.WriteLine("Reference = Book, Object = RomanceNovel");
    book = new RomanceNovel("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
    Console.WriteLine("Reference = Book, Object = MillsAndBoone");
    book = new MillsAndBoone("85745", 45.6f, 65.5f, 200, 0.2f);
    Console.WriteLine(book.EstimatedProfit());
  }

}

The output of Listing 5.9 is as follows:

Reference = Book, Object = Book
Calling Constructor of Book class
Calling EstimatedProfit of Book class
796.0001
Reference = RomanceNovel, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184
Reference = Book, Object = RomanceNovel
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling EstimatedProfit of RomanceNovel class
3184
Reference = Book, Object = MillsAndBoone
Calling Constructor of Book class
Calling Constructor of RomanceNovel class
Calling Constructor of MillsAndBoone class
Calling EstimatedProfit of MillsAndBoone class
3184

In the last output, the MillsAndBoone class defined the method to be overridden, and the method call was made on that object. A subclass can use the override modifier on a method only if the superclass has the virtual, override, or abstract modifier for that method. Table 5.1 shows the virtual and override combinations for our class hierarchy. This illustrates what happens when virtual and override modifiers are on different methods of an inheritance hierarchy. In hierarchy A, the methods on the Book, RomanceNovel, and MillsAndBoone class are all virtual. In hierarchy B, the method on the Book class is virtual, and the methods on the RomanceNovel and MillsAndBoone classes are override, and so on.

Table 5.2 shows the actual method that gets called in Listing 5.3 when an object of that specific class is instantiated and assigned to a Book reference. For hierarchy A, no matter which object is instantiated, method 1 (Book's EstimatedProfit()) always gets called. For hierarchy B, the virtual dispatching mechanism calls the appropriate method on the object instantiated. Hierarchy C shows that the runtime decided not to do any virtual dispatching. Hierarchy D shows that for both RomanceNovel and MillsAndBoone the method called was that of the RomanceNovel class.

Virtual dispatching is a mechanism by which the compiler generates a piece of code that identifies the type of the object the reference is pointing to. Instead of making the direct function call on the reference, the compiler discovers the object type and then calls the method on the type closest to the one pointed to by the reference. This process of microdiscovery—identifying the type of the object instantiated and traversing the object hierarchy to find which object the method has overridden—happens extremely fast. However, it is still slower than executing a direct function call on the reference. In the case of hierarchy C in Table 5.2, the runtime determined its actions based on the fact that the direct subclass of Book (i.e., RomanceNovel) does not override the method; the runtime converted the virtual dispatch call to a straight function call on the reference, thereby calling method 1 (Book's EstimatedProfit() method) for all three objects.

Table 5.1. virtual-override Keyword Combinations
(Method ID) Method Hierarchy AHierarchy BHierarchy CHierarchy D
(1) Book.Estimated Profit()virtualvirtualvirtualvirtual
(2) RomanceNovel.EstimatedProfit()virtualoverridevirtualoverride
(3) MillsAndBoone.EstimatedProfit()virtualoverrideoverridevirtual

Recall the critical difference between C# and Java discussed earlier: In C# all methods are nonvirtual by default, whereas in Java all methods are virtual. As a result, virtual dispatching is turned off by default in C#. A method call will be made on the reference rather than on the object that is instantiated and assigned to the reference. To turn on virtual dispatching in C#, you must use either the virtual or the override modifier on the superclass and explicitly add an override modifier on the subclass.

5.3.2. Effects of Method Parameters and Conversion Rules

So far we've looked at two things that influence method invocation: the override and virtual modifiers. Also influencing which method gets called are the method parameters and their conversion rules (Listing 5.10).

Table 5.2. Methods Called Based on Table 5.1 Keyword Combination
Method NumberHierarchy AHierarchy BHierarchy CHierarchy D
11111
21212
31312

Listing 5.10. Influence of Method Parameter Types on Method Invocation (C#)
using System;

public class SuperClass {

  public void SetVal(int i) {
    Console.WriteLine("SetVal Super class method");
  }

  public void Calc(short i) {
    Console.WriteLine("Calc Super class method");
  }
}

public class SubClass : SuperClass {

  public void SetVal(short i) {
    Console.WriteLine("SetVal Sub class method");
  }

  public void Calc(int i) {
    Console.WriteLine("Calc Sub class method");
  }

  static void Main(string[] args) {
    int i = 7;
    short j = 8;
    SubClass sc = new SubClass();
    sc.SetVal(i);
    sc.Calc(i);
    sc.SetVal(j);
    sc.Calc(j);
  }
}

In Listing 5.10, a subclass object is instantiated and assigned to a subclass reference, and therefore any method call on the reference should always call the subclass method. This, however, is not what happens; the first method call, sc.SetVal(i), is made on the superclass because the int argument cannot be converted to the short parameter of the subclass's SetVal method. The runtime calls the next best version of the method, which exists in the superclass. The output of Listing 5.10 is as follows:

SetVal Super class method
Calc Sub class method
SetVal Sub class method
Calc Sub class method

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

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