Chapter 5
IN THIS CHAPTER
Including constructors in a hierarchy
Invoking the base-class constructor
Differentiating between is a and has a
Substituting one class object for another
Object-oriented programming is based on four principles: the capability 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. You are a human. You inherit certain properties from the class Human
, such as your ability to converse and your dependence on air, food, and beverages. The class Human
inherits its dependencies on air, water, and nourishment from the class Mammal
, which inherits from the class Animal
.
The capability to pass down properties is a powerful one. You can use it to describe items in an economical way. For example, if your son asks, “What's a duck?” you can say, “It’s a bird that quacks.” Despite what you may think, that answer conveys a considerable amount of information. Your 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. This chapter discusses C# inheritance in detail so that you can understand the relationships between the various classes that you use.
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. Computer 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 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.
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 sections discuss the BankAccount
class and its subclass, SavingsAccount
, starting with a basic application and then looking at how the constructors and other features work.
It's important to start with the basics. The BankAccount
class acts as the base class for the SimpleSavingsAccount
program:
// BankAccount -- Simulate a bank account, each of which
// carries an account ID (which is assigned
// on creation) and a balance.
internal class BankAccount // The base class
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;
// Maintain the account number for each object.
private int _accountNumber;
// Constructor -- Initialize a bank account with the next account
// ID and the specified initial balance (default to zero).
internal BankAccount() : this(0) { }
internal BankAccount(decimal initialBalance)
{
_accountNumber = ++_nextAccountNumber;
Balance = initialBalance;
}
// Balance property.
protected decimal Balance
{ get; set; }
// 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 override string ToString()
{
return $"{_accountNumber} - {Balance:C}";
}
}
This example uses chaining to chain one constructor to another using this
. When you see internal BankAccount() : this(0) { }
, it means to call the next constructor in the chain using a value of 0
for the initialBalance
parameter. Using this approach greatly reduces the amount of code you have to write, reduces the potential for error, and makes the code more maintainable. The second constructor obtains the next account number and sets the Balance
property.
The Balance
property has a protected
access modifier because only the base class, BankAccount
, and the derived class, SavingsAccount
, will access it. Using this approach means that you can simplify the code and not have to maintain a separate internal variable of your own.
The three methods, Deposit()
, Withdraw()
, and ToString()
, allow controlled public access of BankAccount features. ToString()
overrides the base ToString()
to display the account number and balance as a dollar amount.
The SavingsAccount
class adds the ability to change the balance based on interest accumulation, as shown here:
internal class SavingsAccount : BankAccount // The subclass
{
private decimal _interestRate = 0;
// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate) { }
internal SavingsAccount(decimal initialBalance,
decimal interestRate) : base(initialBalance)
{
_interestRate = interestRate / 100;
}
// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
Balance = Balance + (decimal)(Balance * _interestRate);
}
// ToString -- Stringify the account.
public override string ToString()
{
return $"{base.ToString()} ({_interestRate:P})";
}
}
As with BankAccount
, SavingsAccount
provides two constructors, one of which is chained to the other. SavingsAccount
is a subclass of BankAccount
, so it already has access to elements like a Balance
. The only thing it needs to add is the ability to accumulate interest, which appears in AccumulateInterest()
. Notice how the SavingsAccount
class ToString()
builds on the base class ToString()
. Also note how the SavingsAccount()
constructor calls initializeBalance()
in the base
class.
does about as little as it can. It creates a Main()
, makes a deposit, displays the account, creates a BankAccount
, accumulates one period of interest, and displays the result, with the interest rate as shown here: SavingsAccount
static void Main(string[] args)
{
// Create a bank account and display it.
BankAccount ba = new BankAccount(100M);
ba.Deposit(100M);
Console.WriteLine($"Account {ba.ToString()}");
// Now a savings account
SavingsAccount sa = new SavingsAccount(12.5M);
sa.Deposit(100M);
sa.AccumulateInterest();
Console.WriteLine($"Account {sa.ToString()}");
Console.Read();
}
You see the following output when you run this application:
Account 1001 - $200.00
Account 1002 - $112.50 (12.500%)
It can be hard to track precisely how the BankAccount
and SavingsAccount
classes work unless you single-step through them using the debugger. Another technique is to add some Console.WriteLine()
method entries, as shown in the following listing (and in SimpleSavingsAccountTracked
):
internal class BankAccount // The base class
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;
// Maintain the account number for each object.
private int _accountNumber;
// Constructor -- Initialize a bank account with the next account
// ID and the specified initial balance (default to zero).
internal BankAccount() : this(0)
{
Console.WriteLine("*** Called BankAccount Default Constructor ***");
}
internal BankAccount(decimal initialBalance)
{
_accountNumber = ++_nextAccountNumber;
Balance = initialBalance;
Console.WriteLine("*** Called BankAccount Constructor ***");
}
// Balance property.
protected decimal Balance
{ get; set; }
// Deposit -- any positive deposit is allowed.
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
Console.WriteLine("*** Called BankAccount Deposit() ***");
}
}
// 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;
Console.WriteLine("*** Called BankAccount Withdrawal() ***");
return withdrawal;
}
// ToString - Stringify the account.
public override string ToString()
{
Console.WriteLine("*** Called BankAccount ToString() ***");
return $"{_accountNumber} - {Balance:C}";
}
}
// SavingsAccount -- A bank account that draws interest
internal class SavingsAccount : BankAccount // The subclass
{
private decimal _interestRate = 0;
// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate)
{
Console.WriteLine("*** Called SavingsAccount Constructor 1 ***");
}
internal SavingsAccount(decimal initialBalance,
decimal interestRate) : base(initialBalance)
{
_interestRate = interestRate / 100;
Console.WriteLine("*** Called SavingsAccount Constructor 2 ***");
}
// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
Balance = Balance + (decimal)(Balance * _interestRate);
Console.WriteLine("*** Called SavingsAccount " +
"AccumulateInterest() ***");
}
// ToString -- Stringify the account.
public override string ToString()
{
Console.WriteLine("*** Called SavingsAccount ToString() ***");
return $"{base.ToString()} ({_interestRate:P})";
}
}
The code in Main()
is unchanged, although, you could certainly play around with it to see what happens. When you run this version of the code, you see the following output:
*** Called BankAccount Constructor ***
*** Called BankAccount Deposit() ***
*** Called BankAccount ToString() ***
Account 1001 - $200.00
*** Called BankAccount Constructor ***
*** Called SavingsAccount Constructor 2 ***
*** Called SavingsAccount Constructor 1 ***
*** Called BankAccount Deposit() ***
*** Called SavingsAccount AccumulateInterest() ***
*** Called SavingsAccount ToString() ***
*** Called BankAccount ToString() ***
Account 1002 - $112.50 (12.50%)
Creating and using the BankAccount
object ba
is straightforward. The code calls the constructor that accepts an initial amount, deposits $100.00, and then prints the result.
However, creating and using the SavingsAccount
object, sa
, requires a little more work. In this case, the code:
BankAccount
constructor that accepts an initial amount first because that's the constructor called by the base(initialBalance)
portion of the second SavingsAccount
constructor.SavingsAccount
constructor because that's the constructor called by the this(0, interestRate)
portion of the first SavingsAccount
constructor.SavingsAccount
constructor (the one that accepts only an interest rate as input).BankAccount.Deposit()
method (despite the fact that the call appears as sa.Deposit(100M)
).SavingsAccount.AccumulateInterest()
method because this method is unique to the SavingsAccount
class.SavingsAccount.ToString()
override, which calls the BankAccount.ToString()
override to print the savings account string.The relationship between SavingsAccount
and BankAccount
is the fundamental IS_A relationship in inheritance. In the following sections, you discover why. You also see what the alternative, the HAS_A relationship, would look like in comparison.
The IS_A relationship between SavingsAccount
and BankAccount
is demonstrated by modifications to the class Program
(shown in bold) in the SimpleSavingsAccount
program from the preceding section (as found in SimpleSavingsAccount2
):
class Program
{
public static void DirectDeposit(BankAccount ba, decimal pay)
{
ba.Deposit(pay);
}
static void Main(string[] args)
{
// Create a bank account and display it.
BankAccount ba = new BankAccount(100M);
DirectDeposit(ba, 100M);
Console.WriteLine($"Account {ba.ToString()}");
// Now a savings account
SavingsAccount sa = new SavingsAccount(12.5M);
DirectDeposit(sa, 100);
sa.AccumulateInterest();
Console.WriteLine($"Account {sa.ToString()}");
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.
The class SavingsAccount
could have gained access to the members of BankAccount
in a different way, as shown in the following code (and in SimpleSavingsAccount3
):
internal class SavingsAccount
{
private BankAccount _bankAccount;
private decimal _interestRate = 0;
// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate)
{ }
internal SavingsAccount(decimal initialBalance,
decimal interestRate)
{
_bankAccount = new BankAccount(initialBalance);
_interestRate = interestRate / 100;
}
// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
_bankAccount.Balance = _bankAccount.Balance +
(decimal)(_bankAccount.Balance * _interestRate);
}
public void Deposit(decimal amount)
{
_bankAccount.Deposit(amount);
}
public decimal Withdraw(decimal withdrawal)
{
return _bankAccount.Withdraw(withdrawal);
}
// ToString -- Stringify the account.
public override string ToString()
{
return $"{_bankAccount.ToString()} ({_interestRate:P})";
}
}
In this case, the class SavingsAccount_
contains a data member _
bankAccount
(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. Notice that this strategy requires the inclusion of instantiating _bankAccount
within the second constructor and using _bankAccount anywhere that you might have seen base
used in the past.
https://www.dotnetcurry.com/csharp/dynamic-class-creation-roslyn
).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()
// HAS_A version of SavingsAccount
SavingsAccount_ sa = new SavingsAccount_(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_(sa, 100);
// … continue …
}
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. 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.
C# implements a set of features designed to support inheritance. The following sections discuss these features.
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 the 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;
// The original, not SavingsAccount_
SavingsAccount sa = new SavingsAccount();
// OK:
ba = sa; // Implicitly convert subclass to base class.
ba = (BankAccount)sa; // But the explicit cast is preferred.
sa = (SavingsAccount)ba; // An explicit cast is allowed.
// Not OK:
sa = ba; // No implicit conversion of base class to subclass
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 may involve a loss of data.
Generally, casting an object from BankAccount
to SavingsAccount
is a dangerous operation. Consider this example from SimpleSavingsAccount4
:
class Program
{
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();
}
static void Main(string[] args)
{
SavingsAccount sa = new SavingsAccount(100M, 12.5M);
ProcessAmount(sa);
Console.WriteLine(sa.ToString());
BankAccount ba = new BankAccount(100M);
ProcessAmount(ba);
Console.WriteLine(ba.ToString());
Console.Read();
}
}
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 Main()
. 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. When you run this example, you see the following output when you use the Debug⇒ Start Without Debugging command:
1001 - $11,362.50 (12.50%)
Unhandled Exception: System.InvalidCastException: Unable to cast object of
type 'SimpleSavingsAccount.BankAccount' to type
'SimpleSavingsAccount.SavingsAccount'. at
SimpleSavingsAccount.Program.ProcessAmount(BankAccount bankAccount) in
E:CSAIO4D2EBK02CH05SimpleSavingsAccount4Program.cs:line 96 at
SimpleSavingsAccount.Program.Main(String[] args) in
E:CSAIO4D2EBK02CH05SimpleSavingsAccount4Program.cs:line 107
Press any key to continue …
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 ProcessAmount()
in the previous section (as shown in SimpleSavingsAccount5
) 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.
SavingsAccount savingsAccount = (SavingsAccount)bankAccount;
savingsAccount.AccumulateInterest();
}
}
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 as shown here.
1001 - $11,362.50 (12.50%)
1002 - $10,100.00
The as
operator works a bit differently from is
. Rather than return a bool
if the cast should work (but doesn't), it converts the type on the left to the type on the right. It 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.
3.22.27.45