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.
In the following InheritanceExample
program, the class SubClass
inherits from the class BaseClass
:
// InheritanceExample -- Provide the simplest possible // demonstration of inheritance. using System; namespace InheritanceExample { public classBaseClass
{ 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...
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.)
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#.
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.
// 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...
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.
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 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.
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.
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 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 . . . }
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.
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.
Elegance in software is a goal worth achieving in its own right. It enhances understandability, reliability, and maintainability (and cures indigestion and gout).
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.
C# implements a set of features designed to support inheritance. I discuss these features in the following sections.
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.
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.
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.
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.
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.
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.
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.
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.
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.)
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.
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:
// 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...
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.
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.
A subclass constructor can invoke a specific base class constructor using the keyword base
.
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
:
// 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 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:
// 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.
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.
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.)