Chapter 6. Inheritance: Is That All I Get?

In This Chapter

  • Defining one class in terms of another, more fundamental class

  • Differentiating between is a and has a

  • Substituting one class object for another

  • Constructing static or instance members

  • Including constructors in an inheritance hierarchy

  • Invoking the base class constructor specifically

Object-oriented programming is based on four principles: the ability to control access (encapsulation), inherit from other classes, respond appropriately (polymorphism), and refer from one object to another indirectly (interfaces).

Inheritance is a common concept. I am a human, except when I first wake up. I inherit certain properties from the class Human, such as my ability to converse, more or less, and my dependence on air, food, and carbohydrate-based beverages with lots of caffeine. The class Human inherits its dependencies on air, water, and nourishment from the class Mammal, which inherits from the class Animal.

The ability to pass down properties is a powerful one. You can use it to describe items in an economical way. For example, if my son asks, "What's a duck?" I can say, "It's a bird that quacks." Despite what you may think, that answer conveys a considerable amount of information. My son knows what a bird is, and now he knows all those same characteristics about a duck plus the duck's additional property of "quackness."

Object-oriented languages express this inheritance relationship by allowing one class to inherit properties from another. This feature enables object-oriented languages to generate a model that's closer to the real world than the model generated by languages that don't support inheritance.

Class Inheritance

In the following InheritanceExample program, the class SubClass inherits from the class BaseClass:

Note

// InheritanceExample -- Provide the simplest possible
//    demonstration of inheritance.
using System;
namespace InheritanceExample
{
  public class BaseClass
  {
    public int _dataMember;
    public void SomeMethod()
    {
      Console.WriteLine("SomeMethod()");
    }
  }
  public class SubClass : BaseClass
  {
    public void SomeOtherMethod()
    {
      Console.WriteLine("SomeOtherMethod()");
    }
  }
  public class Program
  {
    public static void Main(string[] args)
    {
      // Create a base class object.
      Console.WriteLine("Exercising a base class object:");
      BaseClass bc = new BaseClass();
      bc._dataMember = 1;
      bc.SomeMethod();
      // Now create a subclass object.
      Console.WriteLine("Exercising a subclass object:");
      SubClass sc = new SubClass();
      sc._dataMember = 2;
      sc.SomeMethod();
      sc.SomeOtherMethod();
      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

The class BaseClass is defined with a data member and the simple method SomeMethod(). Main() creates and exercises the BaseClass object bc.

The class SubClass inherits from that class by placing the name of the class, BaseClass, after a colon in the class definition:

public class SubClass : BaseClass

SubClass gets all members of BaseClass as its own, plus any members it may add to the pile. Main() demonstrates that SubClass now has a data member, _dataMember, and a member method, SomeMethod(), to join the brand-new member of the family, little method SomeOtherMethod() — and what a joy it is, too.

The program produces the following expected output (I'm usually sort of surprised whenever one of my programs works as expected):

Exercising a base class object:
SomeMethod()
Exercising a subclass object:
SomeMethod()
SomeOtherMethod()
Press Enter to terminate...

Why You Need Inheritance

Inheritance serves several important functions. You may think, for example, that inheritance reduces the amount of typing. In a way, it does — you don't need to repeat the properties of a Person when you're describing a Student class. A more important, related issue is the major buzzword reuse. Software scientists have known for some time that starting from scratch with each new project and rebuilding the same software components makes little sense.

Compare the situation in software development to that of other industries. Think about the number of car manufacturers that start out by building their own wrenches and screwdrivers before they construct a car. Of those who do that, estimate how many would start over completely and build all new tools for the next model. Practitioners in other industries have found that starting with existing screws, bolts, nuts, and even larger off-the-shelf components such as motors and compressors makes more sense than starting from scratch.

Inheritance enables you to tweak existing software components. You can adapt existing classes to new applications without making internal modifications. The existing class is inherited into — or, as programmers often say, extended by — a new subclass that contains the necessary additions and modifications. If someone else wrote the base class, you may not be able to modify it, so inheritance can save the day.

This capability carries with it a third benefit of inheritance. Suppose that you inherit from — extend — an existing class. Later, you find that the base class has a bug you must correct. If you modified the class to reuse it, you must manually check for, correct, and retest the bug in each application separately. If you inherited the class without changes, you can generally stick the updated class into the other application with little hassle.

But the biggest benefit of inheritance is that it describes the way life is. Items inherit properties from each other. There's no getting around it. (Basta! — as my Italian grandmother would say.)

Inheriting from a BankAccount Class (A More Complex Example)

A bank maintains several types of accounts. One type, the savings account, has all the properties of a simple bank account plus the ability to accumulate interest. The following SimpleSavingsAccount program models this relationship in C#.

Warning

The version of this program on the Web site includes some modifications from the next section of this chapter, so it's a bit different from the code listing shown here.

Note

// SimpleSavingsAccount -- Implement SavingsAccount as a form of
//   bank account; use no virtual methods.
using System;
namespace SimpleSavingsAccount
{
// BankAccount -- Simulate a bank account, each of which
  //    carries an account ID (which is assigned
  //    on creation) and a balance.
  public class BankAccount    // The base class
  {
    // Bank accounts start at 1000 and increase sequentially.
    public static int _nextAccountNumber = 1000;
    // Maintain the account number and balance for each object.
    public int _accountNumber;
    public decimal _balance;
    // Init -- Initialize a bank account with the next account ID and the
    //    specified initial balance (default to zero).
    public void InitBankAccount()
    {
      InitBankAccount(0);
    }
    public void InitBankAccount(decimal initialBalance)
    {
      _accountNumber = ++_nextAccountNumber;
      _balance = initialBalance;
    }
    // Balance property.
    public decimal Balance
    {
      get { return _balance;}
    }
    // Deposit -- any positive deposit is allowed.
    public void Deposit(decimal amount)
    {
      if (amount > 0)
      {
        _balance += amount;
      }
    }
    // Withdraw -- You can withdraw any amount up to the
    //    balance; return the amount withdrawn.
    public decimal Withdraw(decimal withdrawal)
    {
      if (Balance <= withdrawal) // Use Balance property.
      {
        withdrawal = Balance;
      }
      _balance -= withdrawal;
      return withdrawal;
    }
    // ToString - Stringify the account.
    public string ToBankAccountString()
    {
      return String.Format("{0} - {1:C}",
        _accountNumber, Balance);
    }
  }
  // SavingsAccount -- A bank account that draws interest
  public class SavingsAccount : BankAccount   // The subclass
  {
    public decimal _interestRate;
    // InitSavingsAccount -- Input the rate expressed as a
    //    rate between 0 and 100.
public void InitSavingsAccount(decimal interestRate)
    {
      InitSavingsAccount(0, interestRate);
    }
    public void InitSavingsAccount(decimal initialBalance, decimal interestRate)
    {
      InitBankAccount(initialBalance);  // Note call to base class.
      this._interestRate = interestRate / 100;
    }
    // AccumulateInterest -- Invoke once per period.
    public void AccumulateInterest()
    {
      _balance = Balance + (decimal)(Balance * _interestRate);
    }
    // ToString -- Stringify the account.
    public string ToSavingsAccountString()
    {
      return String.Format("{0} ({1}%)",
        ToBankAccountString(), _interestRate * 100);
    }
  }
  public class Program
  {
    public static void Main(string[] args)
    {
      // Create a bank account and display it.
      BankAccount ba = new BankAccount();
      ba.InitBankAccount(100M); // M suffix indicates decimal.
      ba.Deposit(100M);
      Console.WriteLine("Account {0}", ba.ToBankAccountString());
      // Now a savings account
      SavingsAccount sa = new SavingsAccount();
      sa.InitSavingsAccount(100M, 12.5M);
      sa.AccumulateInterest();
      Console.WriteLine("Account {0}", sa.ToSavingsAccountString());
      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

The BankAccount class is not unlike some that appear in other chapters of this book. It begins with an overloaded initialization method InitBank Account(): one for accounts that start out with an initial balance and another for which an initial balance of zero will have to suffice. Notice that this version of BankAccount doesn't take advantage of the latest and greatest constructor advances. If you read this entire chapter and see that I clean up this topic in the final version of BankAccount, you can then see why I chose to "drop back" a little here.

The Balance property allows other people to read the balance without letting them modify it. The Deposit() method accepts any positive deposit. Withdraw() lets you take out as much as you want, as long as you have enough money in your account. (My bank's nice, but it isn't that nice.) ToBankAccountString() creates a string that describes the account.

The SavingsAccount class inherits all that good stuff from BankAccount. It also adds an interest rate and the ability to accumulate interest at regular intervals.

Main() does about as little as it can. It creates a BankAccount, displays the account, creates a SavingsAccount, accumulates one period of interest, and displays the result, with the interest rate in parentheses:

Account 1001 - $200.00
Account 1002 - $112.50 (12.500%)
Press Enter to terminate...

Tip

Notice that the InitSavingsAccount() method invokes InitBank Account(). It initializes the bank account–specific data members. The InitSavingsAccount() method could have initialized these members directly; however, a better practice is to allow BankAccount to initialize its own members. A class should be responsible for itself.

IS_A versus HAS_A — I'm So Confused_A

The relationship between SavingsAccount and BankAccount is the fundamental IS_A relationship in inheritance. In the following sections, I show you why, and then I show you what the alternative, the HAS_A relationship, would look like in comparison.

The IS_A relationship

The IS_A relationship between SavingsAccount and BankAccount is demonstrated by the modification to the class Program in the SimpleSavingsAccount program from the preceding section:

public class Program
{
  // Add this:
  // DirectDeposit -- Deposit my paycheck automatically.
  public static void DirectDeposit(BankAccount ba, decimal pay)
  {
    ba.Deposit(pay);
  }
  public static void Main(string[] args)
  {
    // Create a bank account and display it.
    BankAccount ba = new BankAccount();
    ba.InitBankAccount(100M);
    DirectDeposit(ba, 100M);
    Console.WriteLine("Account {0}", ba.ToBankAccountString());
    // Now a savings account
    SavingsAccount sa = new SavingsAccount();
    sa.InitSavingsAccount(12.5M);
    DirectDeposit(sa, 100M);
    sa.AccumulateInterest();
    Console.WriteLine("Account {0}", sa.ToSavingsAccountString());
    // Wait for user to acknowledge the results.
    Console.WriteLine("Press Enter to terminate...");
    Console.Read();
  }
}

In effect, nothing has changed. The only real difference is that all deposits are now being made through the local method DirectDeposit(), which isn't part of class BankAccount. The arguments to this method are the bank account and the amount to deposit.

Note

Notice (here comes the good part) that Main() could pass either a bank account or a savings account to DirectDeposit() because a SavingsAccount IS_A BankAccount and is accorded all the same rights and privileges. Because SavingsAccount IS_A BankAccount, you can assign a SavingsAccount to a BankAccount-type variable or method argument.

Gaining access to BankAccount by using containment

The class SavingsAccount could have gained access to the members of BankAccount in a different way, as shown in the following code, where the key lines are shown in boldface:

// SavingsAccount -- A bank account that draws interest
public class SavingsAccount_   // Notice the underscore: this isn't
                               // the SavingsAccount class.
{
  public BankAccount _bankAccount;  // Notice this, the contained BankAccount.
  public decimal _interestRate;
  // InitSavingsAccount -- Input the rate expressed as a
  //    rate between 0 and 100.
  public void InitSavingsAccount(BankAccount bankAccount, decimal interestRate)
  {
    this._bankAccount = bankAccount;
    this._interestRate = interestRate / 100;
  }
  // AccumulateInterest -- Invoke once per period.
  public void AccumulateInterest()
  {
    _bankAccount._balance = _bankAccount.Balance
                   + (_bankAccount.Balance * interestRate);
  }
  // Deposit -- Any positive deposit is allowed.
  public void Deposit(decimal amount)
  {
    // Delegate to the contained BankAccount object.
    _bankAccount.Deposit(amount);
  }
  // Withdraw -- You can withdraw any amount up to the
  //    balance; return the amount withdrawn.
  public double Withdraw(decimal withdrawal)
  {
    return _bankAccount.Withdraw(withdrawal);
  }
}

In this case, the class SavingsAccount_ contains a data member _bank Account (as opposed to inheriting from BankAccount). The _bankAccount object contains the balance and account number information needed by the savings account. The SavingsAccount_ class retains the data unique to a savings account and delegates to the contained BankAccount object as needed. That is, when the SavingsAccount needs, say, the balance, it asks the contained BankAccount for it.

In this case, you say that the SavingsAccount_ HAS_A BankAccount. Hard-core object-oriented jocks say that SavingsAccount composes a BankAccount. That is, SavingsAccount is partly composed of a BankAccount.

The HAS_A relationship

The HAS_A relationship is fundamentally different from the IS_A relationship. This difference doesn't seem so bad in the following application-code segment example:

// Create a new savings account.
BankAccount ba = new BankAccount()
SavingsAccount_ sa = new SavingsAccount_(); // HAS_A version of SavingsAccount
sa.InitSavingsAccount(ba, 5);
// And deposit 100 dollars into it.
sa.Deposit(100M);
// Now accumulate interest.
sa.AccumulateInterest();

The problem is that this modified SavingsAccount_ cannot be used as a BankAccount because it doesn't inherit from BankAccount. Instead, it contains a BankAccount — not the same concept. For example, this code example fails:

// DirectDeposit -- Deposit my paycheck automatically.
void DirectDeposit(BankAccount ba, int pay)
{
  ba.Deposit(pay);
}
void SomeMethod()
{
  // The following example fails.
  SavingsAccount_ sa = new SavingsAccount_();
  DirectDeposit(sa, 100);
  // . . . continue . . .
}

Note

DirectDeposit() can't accept a SavingsAccount_ in lieu of a BankAccount. No obvious relationship between the two exists, as far as C# is concerned, because inheritance isn't involved. Don't think, though, that this situation makes containment a bad idea. You just have to approach the concept a bit differently in order to use it.

When to IS_A and When to HAS_A

The distinction between the IS_A and HAS_A relationships is more than just a matter of software convenience. This relationship has a corollary in the real world.

For example, a Ford Explorer IS_A car (when it's upright, that is). An Explorer HAS_A motor. If your friend says, "Come on over in your car" and you show up in an Explorer, he has no grounds for complaint. He may have a complaint if you show up carrying your Explorer's engine in your arms, however. (Or at least you will.)

The class Explorer should extend the class Car, not only to give Explorer access to the methods of a Car but also to express the fundamental relationship between the two.

Unfortunately, the beginning programmer may have Car inherit from Motor, as an easy way to give the Car class access to the members of Motor, which the Car needs in order to operate. For example, Car can inherit the method Motor.Go(). However, this example highlights a problem with this approach: Even though humans become sloppy in their speech, making a car go isn't the same thing as making a motor go. The car's "go" operation certainly relies on that of the motor's, but they aren't the same thing — you also have to put the transmission in gear, release the brake, and complete other tasks.

Perhaps even more than that, inheriting from Motor misstates the facts. A car simply isn't a type of motor.

Note

Elegance in software is a goal worth achieving in its own right. It enhances understandability, reliability, and maintainability (and cures indigestion and gout).

Tip

Hard-core object-oriented jocks recommend preferring HAS_A over IS_A for simpler program designs. But use inheritance when it makes sense, as it probably does in the BankAccount hierarchy.

Other Features That Support Inheritance

C# implements a set of features designed to support inheritance. I discuss these features in the following sections.

Substitutable classes

A program can use a subclass object where a base class object is called for. In fact, you may have already seen this concept in one of my examples. SomeMethod() can pass a SavingsAccount object to the DirectDeposit() method, which expects a BankAccount object.

You can make this conversion more explicit:

BankAccount ba;
SavingsAccount sa = new SavingsAccount(); // The original, not SavingsAccount_
// OK:
ba = sa;                 // Implicitly converting subclass to base class.
ba = (BankAccount)sa;    // But the explicit cast is preferred.
// Not OK:
sa = ba;                 // ERROR: Implicitly converting base class to subclass
sa = (SavingsAccount)ba; // An explicit cast is allowed, however.

The first line stores a SavingsAccount object into a BankAccount variable. C# converts the object for you. The second line uses a cast to explicitly convert the object.

The final two lines attempt to convert the BankAccount object back into SavingsAccount. You can complete this operation explicitly, but C# doesn't do it for you. It's like trying to convert a larger numeric type, such as double, to a smaller one, such as float. C# doesn't do it implicitly because the process involves a loss of data.

Note

The IS_A property isn't reflexive. That is, even though an Explorer is a car, a car isn't necessarily an Explorer. Similarly, a BankAccount isn't necessarily a SavingsAccount, so the implicit conversion isn't allowed. The final line is allowed because the programmer has indicated her willingness to "chance it." She must know something.

Invalid casts at run time

Generally, casting an object from BankAccount to SavingsAccount is a dangerous operation. Consider this example:

public static void ProcessAmount(BankAccount bankAccount)
{
  // Deposit a large sum to the account.
  bankAccount.Deposit(10000.00M);
  // If the object is a SavingsAccount, collect interest now.
  SavingsAccount savingsAccount = (SavingsAccount)bankAccount;
  savingsAccount.AccumulateInterest();
}
public static void TestCast()
{
  SavingsAccount sa = new SavingsAccount();
  ProcessAmount(sa);
  BankAccount ba = new BankAccount();
  ProcessAmount(ba);
}

ProcessAmount() performs a few operations, including invoking the AccumulateInterest() method. The cast of ba to a SavingsAccount is necessary because the bankAccount parameter is declared to be a BankAccount. The program compiles properly because all type conversions are made by explicit cast.

All goes well with the first call to ProcessAmount() from within TestCast(). The SavingsAccount object sa is passed to the ProcessAmount() method. The cast from BankAccount to SavingsAccount causes no problem because the ba object was originally a SavingsAccount, anyway.

The second call to ProcessAmount() isn't as lucky, however. The cast to SavingsAccount cannot be allowed. The ba object doesn't have an AccumulateInterest() method.

Warning

An incorrect conversion generates an error during the execution of the program (a runtime error). Runtime errors are much more difficult to find and fix than compile-time errors. Worse, they can happen to a user other than you, which users tend not to appreciate.

Avoiding invalid conversions with the is operator

The ProcessAmount() method would work if it could ensure that the object passed to it is a SavingsAccount object before performing the conversion. C# provides two keywords for this purpose: is and as.

The is operator accepts an object on the left and a type on the right. The is operator returns true if the runtime type of the object on the left is compatible with the type on the right. Use it to verify that a cast is legal before you attempt the cast.

You can modify the example in the previous section to avoid the runtime error by using the is operator:

public static void ProcessAmount(BankAccount bankAccount)
{
  // Deposit a large sum to the account.
  bankAccount.Deposit(10000.00M);
  // If the object is a SavingsAccount . . .
  if (bankAccount is SavingsAccount)
  {
    // ...then collect interest now (cast is guaranteed to work).
    SavingsAccount savingsAccount = (SavingsAccount)bankAccount;
    savingsAccount.AccumulateInterest();
  }
  // Otherwise, don't do the cast -- but why is BankAccount not what
  // you expected? This could be an error situation.
}
public static void TestCast()
{
  SavingsAccount sa = new SavingsAccount();
  ProcessAmount(sa);
  BankAccount ba = new BankAccount();
  ProcessAmount(ba);
}

The added if statement checks the bankAccount object to ensure that it's of the class SavingsAccount. The is operator returns true when ProcessAmount() is called the first time. When passed a BankAccount object in the second call, however, the is operator returns false, avoiding the illegal cast. This version of the program doesn't generate a runtime error.

Tip

On one hand, I strongly recommend that you protect all casts with the is operator to avoid the possibility of a runtime error. On the other hand, you should avoid casts altogether, if possible. Read on.

Avoiding invalid conversions with the as operator

The as operator works a bit differently from is. Rather than return a bool if the cast would work, it converts the type on the left to the type on the right, but safely returns null if the conversion fails — rather than cause a runtime error. You should always use the result of casting with the as operator only if it isn't null. So, using as looks like this:

SavingsAccount savingsAccount = bankAccount as SavingsAccount;
if(savingsAccount != null)
{
  // Go ahead and use savingsAccount.
}
// Otherwise, don't use it: generate an error message yourself.

Note

Generally, you should prefer as because it's more efficient. The conversion is already done with the as operator, whereas you must complete two steps when you use is: First test with is and then complete the cast with the cast operator.

Warning

Unfortunately, as doesn't work with value-type variables, so you can't use it with types such as int, long, or double or with char. When you're trying to convert a value-type object, prefer the is operator.

The object Class

Consider these related classes:

public class MyBaseClass {}
public class MySubClass : MyBaseClass {}

The relationship between the two classes enables the programmer to make the following runtime test:

public class Test
{
  public static void GenericMethod(MyBaseClass mc)
  {
    // If the object truly is a subclass . . .
    MySubClass msc = mc as MyBaseClass;
    if(msc != null)
    {
      // ...then handle as a subclass.
      // . . . continue . . .
    }
  }
}

In this case, the method GenericMethod() differentiates between subclasses of MyBaseClass using the as keyword.

Note

To help you differentiate between seemingly unrelated classes using the same as operator, C# extends all classes from the common base class object. That is, any class that doesn't specifically inherit from another class inherits from the class object. Thus the following two statements declare classes with the same base class — object — and are equivalent:

class MyClass1 : object {}
class MyClass1 {}

Sharing the common base class of object provides for this generic method:

public class Test
{
  public static void GenericMethod(object o)
  {
    MyClass1 mc1 = o as MyClass1;
    if(mc1 != null)
    {
      // Use the converted object mc1.
      // . . .
    }
  }
}

GenericMethod() can be invoked with any type of object. The as keyword can dig the MyClass1 pearls from the object oysters. (The generic I'm referring to isn't the kind covered in Book I.)

Inheritance and the Constructor

The InheritanceExample program described earlier in this chapter relies on those awful Init...() methods to initialize the BankAccount and SavingsAccount objects to a valid state. Outfitting these classes with constructors is definitely the right way to go, but it introduces some complexity. That's why I used those ugly Init...() methods earlier in this chapter until I could cover the features in this section.

Invoking the default base class constructor

The default base class constructor is invoked any time a subclass is constructed. The constructor for the subclass automatically invokes the constructor for the base class, as this simple program demonstrates:

Note

// InheritingAConstructor -- Demonstrate that the base class
//    constructor is invoked automatically.
using System;
namespace InheritingAConstructor
{
public class Program
  {
    public static void Main(string[] args)
    {
      Console.WriteLine("Creating a BaseClass object");
      BaseClass bc = new BaseClass();
      Console.WriteLine("
now creating a SubClass object");
      SubClass sc = new SubClass();
      // Wait for user to acknowledge.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
  public class BaseClass
  {
    public BaseClass()
    {
      Console.WriteLine("Constructing BaseClass");
    }
  }
  public class SubClass : BaseClass
  {
    public SubClass()
    {
      Console.WriteLine("Constructing SubClass");
    }
  }
}

The constructors for BaseClass and SubClass do nothing more than output a message to the command line. Creating the BaseClass object invokes the default BaseClass constructor. Creating a SubClass object invokes the BaseClass constructor before invoking its own constructor.

Here's the output from this program:

Creating a BaseClass object
Constructing BaseClass

Now creating a SubClass object
Constructing BaseClass
Constructing SubClass
Press Enter to terminate...

Note

A hierarchy of inherited classes is much like the floor layout of a building. Each class is built on the classes it extends, as upper floors build on lower ones, and for a clear reason: Each class is responsible for itself. A subclass shouldn't be held responsible for initializing the members of the base class. The BaseClass must be given the opportunity to construct its members before the SubClass members are given a chance to access them. You want the horse well out in front of the cart.

Passing arguments to the base class constructor — mama sang base

The subclass invokes the default constructor of the base class, unless specified otherwise — even from a subclass constructor other than the default. The following slightly updated example demonstrates this feature:

using System;
namespace Example
{
  public class Program
  {
    public static void Main(string[] args)
    {
      Console.WriteLine("Invoking SubClass() default");
      SubClass sc1 = new SubClass();
      Console.WriteLine("
Invoking SubClass(int)");
      SubClass sc2 = new SubClass(0);
      // Wait for user to acknowledge.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
  public class BaseClass
  {
    public BaseClass()
    {
      Console.WriteLine("Constructing BaseClass (default)");
    }
    public BaseClass(int i)
    {
      Console.WriteLine("Constructing BaseClass (int)");
    }
  }
  public class SubClass : BaseClass
  {
    public SubClass()
    {
      Console.WriteLine("Constructing SubClass (default)");
    }
    public SubClass(int i)
    {
      Console.WriteLine("Constructing SubClass (int)");
    }
  }
}

Executing this program generates the following result:

Invoking SubClass()
Constructing BaseClass (default)
Constructing SubClass (default)

Invoking SubClass(int)
Constructing BaseClass (default)
Constructing SubClass (int)
Press Enter to terminate...

The program first creates a default object. As expected, C# invokes the default SubClass constructor, which first passes control to the default BaseClass constructor. The program then creates an object, passing an integer argument. Again as expected, C# invokes the SubClass(int). This constructor invokes the default BaseClass constructor, just as in the earlier example, because it has no data to pass.

Getting specific with base

A subclass constructor can invoke a specific base class constructor using the keyword base.

Note

This feature is similar to the way that one constructor invokes another within the same class by using the this keyword.

For example, consider this small program, InvokeBaseConstructor:

Note

// InvokeBaseConstructor -- Demonstrate how a subclass can
//    invoke the base class constructor of its choice using
//    the base keyword.
using System;
namespace InvokeBaseConstructor
{
  public class BaseClass
  {
    public BaseClass()
    {
      Console.WriteLine("Constructing BaseClass (default)");
    }
    public BaseClass(int i)
    {
      Console.WriteLine("Constructing BaseClass({0})", i);
    }
  }
  public class SubClass : BaseClass
  {
    public SubClass()
    {
      Console.WriteLine("Constructing SubClass (default)");
    }
    public SubClass(int i1, int i2) : base(i1)
    {
      Console.WriteLine("Constructing SubClass({0}, {1})", i1,  i2);
    }
  }
  public class Program
  {
    public static void Main(string[] args)
    {
      Console.WriteLine("Invoking SubClass()");
      SubClass sc1 = new SubClass();

      Console.WriteLine("
invoking SubClass(1, 2)");
      SubClass sc2 = new SubClass(1, 2);

      // Wait for user to acknowledge.
      Console.WriteLine("Press Enter to terminate...");
Console.Read();
    }
  }
}

The output from this program is

Invoking SubClass()
Constructing BaseClass (default)
Constructing SubClass (default)

Invoking SubClass(1, 2)
Constructing BaseClass(1)
Constructing SubClass(1, 2)
Press Enter to terminate...

This version begins the same as the previous examples, by creating a default SubClass object using the default constructor of both BaseClass and SubClass.

The second object is created with the expression new SubClass(1, 2). C# invokes the SubClass(int, int) constructor, which uses the base keyword to pass one of the values to the BaseClass(int) constructor. SubClass passes the first argument to the base class for processing and then uses the second value itself.

The Updated BankAccount Class

The program ConstructorSavingsAccount, found on the Web site, is an updated version of the SimpleBankAccount program. In this version, however, the SavingsAccount constructor can pass information back to the BankAccount constructors. Only Main() and the constructors themselves are shown here:

Note

// ConstructorSavingsAccount -- Implement a SavingsAccount as
//    a form of BankAccount; use no virtual methods, but
//    implement the constructors properly.
using System;
namespace ConstructorSavingsAccount
{
  // BankAccount -- Simulate a bank account, each of which carries an
  //    account ID (which is assigned upon creation) and a balance.
  public class BankAccount
  {
    // Bank accounts start at 1000 and increase sequentially.
    public static int _nextAccountNumber = 1000;
    // Maintain the account number and balance for each object.
    public int _accountNumber;
    public decimal _balance;
    // Constructors
    public BankAccount() : this(0)
    {
    }
    public BankAccount(decimal initialBalance)
    {
_accountNumber = ++_nextAccountNumber;
      _balance = initialBalance;
    }
    public decimal Balance
    {
      get { return _balance; }
      // Protected setter lets subclass use Balance property to set.
      protected set { _balance = value; }
    }
    // Deposit -- Any positive deposit is allowed.
    public void Deposit(decimal amount)
    {
      if (amount > 0)
      {
        Balance += amount;
      }
    }
    // Withdraw -- You can withdraw any amount up to the
    //    balance; return the amount withdrawn.
    public decimal Withdraw(decimal withdrawal)
    {
      if (Balance <= withdrawal)
      {
        withdrawal = Balance;
      }
      Balance -= withdrawal;
      return withdrawal;
    }
    // ToString -- Stringify the account.
    public string ToBankAccountString()
    {
      return String.Format("{0} - {1:C}",
        _accountNumber, Balance);
    }
  }
  // SavingsAccount -- A bank account that draws interest
  public class SavingsAccount : BankAccount
  {
    public decimal _interestRate;
    // InitSavingsAccount -- Input the rate expressed as a
    //   rate between 0 and 100.
    public SavingsAccount(decimal interestRate) : this(interestRate, 0) { }
    public SavingsAccount(decimal interestRate, decimal initial) : base(initial)
    {
      this._interestRate = interestRate / 100;
    }
    // AccumulateInterest -- Invoke once per period.
    public void AccumulateInterest()
    {
      // Use protected setter and public getter via Balance property.
      Balance = Balance + (decimal)(Balance * _interestRate);
    }
    // ToString -- Stringify the account.
    public string ToSavingsAccountString()
    {
      return String.Format("{0} ({1}%)",
        ToBankAccountString(), interestRate * 100);
    }
  }
  public class Program
  {
    // DirectDeposit -- Deposit my paycheck automatically.
public static void DirectDeposit(BankAccount ba, decimal pay)
    {
      ba.Deposit(pay);
    }
    public static void Main(string[] args)
    {
      // Create a bank account and display it.
      BankAccount ba = new BankAccount(100M);
      DirectDeposit(ba, 100M);
      Console.WriteLine("Account {0}", ba.ToBankAccountString());
      // Now a savings account
      SavingsAccount sa = new SavingsAccount(12.5M);
      DirectDeposit(sa, 100M);
      sa.AccumulateInterest();
      Console.WriteLine("Account {0}", sa.ToSavingsAccountString());
      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

BankAccount defines two constructors: one that accepts an initial account balance and the default constructor, which does not. To avoid duplicating code within the constructor, the default constructor invokes the BankAccount(initial balance) constructor using the this keyword.

The SavingsAccount class also provides two constructors. The SavingsAccount(interest rate) constructor invokes the SavingsAccount(interest rate, initial balance) constructor, passing an initial balance of 0. This most general constructor passes the initial balance to the BankAccount(initial balance) constructor using the base keyword, as shown in Figure 6-1.

The path for constructing an object using the default constructor.

Figure 6-1. The path for constructing an object using the default constructor.

Note

I've modified Main() to get rid of those infernal Init...() methods and replace them with constructors instead. The output from this program is the same.

Tip

Notice the Balance property in BankAccount, which has a public getter but a protected setter. Using protected here prevents use from outside of BankAccount but permits using the protected setter in subclasses, which occurs in SavingsAccount.AccumulateInterest, with Balance on the left side of the assignment operator. (Properties and the protected keyword are in Book I. You can look them up in this book's index.)

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

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