Chapter 6
IN THIS CHAPTER
Hiding or overriding a base class method
Building abstract classes and methods
Using ToString()
Sealing a class from being subclassed
In inheritance, one class adopts the members of another. Thus it's possible to create a class SavingsAccount
that inherits data members, such as account id
, and methods, such as Deposit()
, from a base class BankAccount
. That's useful, but this definition of inheritance isn’t sufficient to mimic what’s going on out there in the business world. (See Chapter 5 of this minibook if you don’t know or remember much about class inheritance.)
A microwave oven is a type of oven, not because it looks like an oven, but rather because it performs the same functions as an oven. A microwave oven may perform additional functions, but it performs, at the least, the base oven functions, such as heating food. It’s not important to know what the oven must do internally to make that happen, any more than it’s important to know what type of oven it is, who made it, or whether it was on sale when purchased.
From a human vantage point, the relationship between a microwave oven and a conventional oven doesn’t seem like such a big deal, but consider the problem from the oven’s point of view. The steps that a conventional oven performs internally are completely different from those that a microwave oven may take.
The power of inheritance lies in the fact that a subclass doesn’t have to inherit every single method from the base class just the way it’s written. A subclass can inherit the essence of the base class method while implementing the details differently.
As described in the “Overloading a method doesn’t mean giving it too much to do” section of Chapter 2 of this minibook, two or more methods can have the same name as long as the number or type of arguments differs (or as long as both differ). If you need a quick, but complete, summary of overloading, check out the article at https://www.geeksforgeeks.org/c-sharp-method-overloading/
as well. The following sections extend the concept of method overloading to inherited methods.
public class MyClass
{
public static void AMethod()
{
// Do something.
}
public static void AMethod(int)
{
// Do something else.
}
public static void AMethod(double d)
{
// Do something even different.
}
public static void Main(string[] args)
{
AMethod();
AMethod(1);
AMethod(2.0);
}
}
C# can differentiate the methods by their arguments. Each of the calls within Main()
accesses a different method.
Not surprisingly, the class to which a method belongs is also a part of its extended name. Consider this code segment:
public class MyClass
{
public static void AMethod1();
public void AMethod2();
}
public class UrClass
{
public static void AMethod1();
public void AMethod2();
}
public class Program
{
public static void Main(string[] args)
{
UrClass.AMethod1(); // Call static method.
// Invoke the MyClass.AMethod2() instance method:
MyClass mcObject = new MyClass();
mcObject.AMethod2();
}
}
The name of the class is a part of the extended name of the method. The method MyClass.AMethod1()
has nothing to do with UrClass.AMethod1()
.
So a method in one class can overload another method in its own class by having different arguments. As it turns out, a method can also overload a method in its own base class. Overloading a base class method is known as hiding the method.
Suppose that your bank adopts a policy that makes savings account withdrawals different from other types of withdrawals. Suppose, just for the sake of argument, that withdrawing from a savings account costs $1.50.
Taking the procedural approach, you could implement this policy by setting a flag (variable) in the class to indicate whether the object is a SavingsAccount
or just a simple BankAccount
. Then the withdrawal method would have to check the flag to decide whether it needs to charge $1.50, as shown here:
public class BankAccount
{
private decimal _balance;
private bool _isSavingsAccount; // The flag
// Indicate the initial balance and whether the account
// you're creating is a savings account.
public BankAccount(decimal initialBalance, bool isSavingsAccount)
{
_balance = initialBalance;
_isSavingsAccount = isSavingsAccount;
}
public decimal Withdraw(decimal amountToWithdraw)
{
// If the account is a savings account …
if (_isSavingsAccount)
{
// …then skim off $1.50.
_balance -= 1.50M;
}
// Continue with the usual withdraw code:
if (amountToWithdraw > _balance)
{
amountToWithdraw = _balance;
}
_balance -= amountToWithdraw;
return amountToWithdraw;
}
}
class MyClass
{
public void SomeMethod()
{
// Create a savings account:
BankAccount ba = new BankAccount(0, true);
}
}
Your method must tell the BankAccount
whether the object you're instantiating is a SavingsAccount
in the constructor by passing a flag. The constructor saves that flag and uses it in the Withdraw()
method to decide whether to charge the extra $1.50.
The more object-oriented approach hides the method Withdraw()
in the base class BankAccount
with a new method of the same name in the SavingsAccount
class. The BankAccount
and SavingsAccount
classes of the HidingWithdrawal
example show how this approach works:
// BankAccount -- A very basic bank account
internal class BankAccount
{
internal BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
internal decimal Balance
{ get; private set; }
internal decimal Withdraw(decimal amount)
{
// Good practice means avoiding modifying an input parameter.
// Modify a copy.
decimal amountToWithdraw = amount;
if (amountToWithdraw > Balance)
{
amountToWithdraw = Balance;
}
Balance -= amountToWithdraw;
return amountToWithdraw;
}
}
// SavingsAccount -- A bank account that draws interest
internal class SavingsAccount : BankAccount
{
private decimal InterestRate
{ get; set; }
// SavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
public SavingsAccount(decimal initialBalance, decimal interestRate)
: base(initialBalance)
{
InterestRate = interestRate / 100;
}
// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
internal decimal Withdraw(decimal withdrawal)
{
// Take the $1.50 off the top.
base.Withdraw(1.5M);
// Now you can withdraw from what's left.
return base.Withdraw(withdrawal);
}
}
The two classes provide some basics for creating the accounts and withdrawing money. Notice that both classes have a Withdraw()
method with precisely the same signature, so the Withdraw()
method in SavingsAccount
completely hides the Withdraw()
method in BankAccount
. Here's the Main()
method used to exercise these two classes.
static void Main(string[] args)
{
BankAccount ba;
SavingsAccount sa;
// Create a bank account and withdraw $100.
ba = new BankAccount(200M);
ba.Withdraw(100M);
// Try the same trick with a savings account.
sa = new SavingsAccount(200M, 12);
sa.Withdraw(100M);
// Display the resulting balance.
Console.WriteLine("When invoked directly:");
Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
Console.Read();
}
Main()
in this case creates a BankAccount
object with an initial balance of $200 and then withdraws $100. Main()
repeats the trick with a SavingsAccount
object. When Main()
withdraws money from the base class, BankAccount.Withdraw()
performs the withdraw function with great aplomb. When Main()
then withdraws $100 from the savings account, the method SavingsAccount.Withdraw()
tacks on the extra $1.50.
On the surface, adding a flag to the BankAccount.Withdraw()
method may seem simpler than all this method-hiding stuff. After all, it's just four little lines of code, two of which are nothing more than braces.
The problems are manifold. One problem is that the BankAccount
class has no business worrying about the details of SavingsAccount
. More formally, it's known as breaking the encapsulation of SavingsAccount
. Base classes don’t normally know about their subclasses, which leads to the real problem: Suppose that your bank subsequently decides to add a CheckingAccount
or a CDAccount
or a TBillAccount
. All those likely additions have different withdrawal policies, each requiring its own flag. After adding three or four different types of accounts, the old Withdraw()
method starts looking complicated. Each of those types of classes should worry about its own withdrawal policies and leave BankAccount.Withdraw()
alone. Classes are responsible for themselves.
Oddly enough, you can hide a base class method accidentally. For example, you may have a Vehicle.TakeOff()
method that starts the vehicle rolling. Later, someone else extends your Vehicle
class with an Airplane
class. Its TakeOff()
method is entirely different. In airplane lingo, “take off” means more than just “start moving.” Clearly, this is a case of mistaken identity — the two methods have no similarity other than their identical name. Fortunately, C# detects this problem.
C# generates an ominous-looking warning when it compiles the earlier HidingWithdrawal
program example. The text of the warning message is long, but here's the important part:
’…SavingsAccount.Withdraw(decimal)’ hides inherited member
'…BankAccount.Withdraw(decimal)'.
Use the new keyword if hiding was intended.
C# is trying to tell you that you’ve written a method in a subclass that has the same name as a method in the base class. Is that what you meant to do?
The descriptor new
(shown in bold below) tells C# that the hiding of methods is intentional and not the result of an oversight (and it makes the warning disappear):
internal new decimal Withdraw(decimal withdrawal)
{
// Take the $1.50 off the top.
base.Withdraw(1.5M);
// Now you can withdraw from what's left.
return base.Withdraw(withdrawal);
}
You can overload a method in a base class with a method in the subclass. As simple as this process sounds, it introduces considerable capability, and with capability comes danger.
Here’s a thought experiment: Should you make the decision to call BankAccount.Withdraw()
or SavingsAccount.Withdraw()
at compile-time or at runtime? To illustrate the difference, the following example changes the previous HidingWithdrawal
program in a seemingly innocuous way. (The HidingWithdrawalPolymorphically
version streamlines the listing by leaving out the stuff that doesn't change.) The new version is shown here:
class Program
{
public static void MakeAWithdrawal(BankAccount ba, decimal amount)
{
ba.Withdraw(amount);
}
static void Main(string[] args)
{
BankAccount ba;
SavingsAccount sa;
// Create a bank account, withdraw $100, and
// display the results.
ba = new BankAccount(200M);
MakeAWithdrawal(ba, 100M);
// Try the same trick with a savings account.
sa = new SavingsAccount(200M, 12);
MakeAWithdrawal(sa, 100M);
// Display the resulting balance.
Console.WriteLine("When invoked through intermediary:");
Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
Console.Read();
}
}
The following output from this program may or may not be confusing, depending on what you expected:
When invoked through intermediary:
BankAccount balance is $100.00
SavingsAccount balance is $100.00
This time, rather than perform a withdrawal in Main()
, the program passes the bank account object to the method MakeAWithdrawal()
.
The first question is fairly straightforward: Why does the MakeAWithdrawal()
method even accept a SavingsAccount
object when it clearly states that it's looking for a BankAccount
? The answer is obvious: “Because a SavingsAccount
IS_A BankAccount
.” (See the “IS_A versus HAS_A — I'm So Confused_A” section of Chapter 5 of this minibook.)
The second question is subtle. When passed a BankAccount
object, MakeAWithdrawal()
invokes BankAccount.Withdraw()
— that's clear enough. But when passed a SavingsAccount
object, MakeAWithdrawal()
calls the same method. Shouldn't it invoke the Withdraw()
method in the subclass?
The prosecution intends to show that the call ba.Withdraw()
should invoke the method BankAccount.Withdraw()
. Clearly, the ba
object is a BankAccount
. To do anything else would merely confuse the state. The defense has witnesses back in Main()
to prove that although the ba
object is declared BankAccount
, it is in fact a SavingsAccount
. The jury is deadlocked. Both arguments are equally valid.
In this case, C# comes down on the side of the prosecution: The safer of the two possibilities is to go with the declared type because it avoids any miscommunication. The object is declared to be a BankAccount
and that's that. However, that may not be what you want.
In some cases, you don’t want to choose the declared type. What you want is to make the call based on the real type — the runtime type — as opposed to the declared type. For example, you want to use the SavingsAccount
stored in a BankAccount
variable. This capability to decide at runtime is known as polymorphism, or late binding. Using the declared type every time is called early binding because it sounds like the opposite of late binding.
The term polymorphism comes from the Greek language: Poly means more than one, morph means transform, and ism relates to an ideology or philosophy. Consequently, polymorphism is the idea or concept of transforming a single object, BankAccount
, into many different objects, BankAccount
or SavingsAccount
(in this case). Polymorphism and late binding aren't exactly the same concept — but the difference is subtle:
Polymorphism is the key to the power of Object-Oriented Programming (OOP). It’s so important that languages that don’t support it can’t advertise themselves as OOP languages.
Without polymorphism, inheritance has little meaning. As another example, suppose that you had written a great program that uses a class named Student
. After months of design, coding, and testing, you release this application to rave reviews from colleagues and critics alike.
Time passes, and your boss asks you to add to this program the capability of handling graduate students, who are similar but not identical to undergraduate students. (The graduate students probably claim that they aren’t similar in any way.) Suppose that the formula for calculating the tuition amount for a graduate student is completely different from the formula for an undergrad. Now, your boss doesn’t know or care that, deep within the program, are numerous calls to the member method CalcTuition()
. The following example shows one of those many calls to CalcTuition()
:
void SomeMethod(Student s) // Could be grad or undergrad
{
// … whatever it might do …
s.CalcTuition();
// … continues on …
}
If C# didn't support late binding, you would need to edit SomeMethod()
to check whether the student
object passed to it is a GraduateStudent
or a Student
. The program would call Student.CalcTuition()
when s
is a Student
and GraduateStudent.CalcTuition()
when it's a GraduateStudent
. Editing SomeMethod()
doesn't seem so bad, except for two problems:
CalcTuition()
is called from many places.CalcTuition()
might not be the only difference between the two classes. The chances aren't good that you’ll find all items that need to be changed.Using polymorphism, you can let C# decide which method to call.
C# provides one approach to manually solving the problem of making your program polymorphic, using the keyword is
. (The “Avoiding invalid conversions with the is operator” section of Chapter 5 of this minibook introduces is
and its cousin as
.) The expression ba is SavingsAccount
returns true
or false
depending on the runtime class of the object. The declared type may be BankAccount
, but which type is it really? The following code chunk uses is
to access the SavingsAccount
version of Withdraw()
specifically (as found in HidingWithdrawalPolymorphically2
):
public static void MakeAWithdrawal(BankAccount ba, decimal amount)
{
if (ba is SavingsAccount)
{
SavingsAccount sa = (SavingsAccount)ba;
sa.Withdraw(amount);
}
else
{
ba.Withdraw(amount);
}
}
Now, when Main()
passes the method a SavingsAccount
object, MakeAWithdrawal()
checks the runtime type of the ba
object and invokes SavingsAccount.Withdraw()
.
((SavingsAccount)ba).Withdraw(amount); // Notice locations of parentheses.
You often see this technique used in programs written by experienced developers who hate typing any more than necessary. Although you can use this approach, it's more difficult to read than when you use multiple lines, as shown in the example code. Anything written confusingly or cryptically tends to be more error-prone, too.
The is
approach works, but it’s a bad idea. It requires MakeAWithdrawal()
to be aware of all the different types of bank accounts and which of them is represented by different classes. That puts too much responsibility on poor old MakeAWithdrawal()
. Right now, your application handles only two types of bank accounts, but suppose that your boss asks you to implement a new account type, CheckingAccount
, and it has different Withdraw()
requirements. Your program doesn't work properly if you don’t search out and find every method that checks the runtime type of its argument.
As the author of MakeAWithdrawal()
, you don’t want to know about all the different types of accounts. You want to leave to the programmers who use MakeAWithdrawal()
the responsibility to know about their account types and just leave you alone. You want C# to make decisions about which methods to invoke based on the runtime type of the object.
You tell C# to make the runtime decision of the version of Withdraw()
by marking the base class method with the keyword virtual
and marking each subclass version of the method with the keyword override
.
The following example relies on polymorphism. It has output statements in the Withdraw()
methods to prove that the proper methods are indeed being invoked. Here are the BankAccount
and SavingsAccount
classes of the PolymorphicInheritance
program:
// BankAccount -- A very basic bank account
internal class BankAccount
{
internal BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
internal decimal Balance
{ get; private set; }
internal virtual decimal Withdraw(decimal amount)
{
decimal amountToWithdraw = amount;
if (amountToWithdraw > Balance)
{
amountToWithdraw = Balance;
}
Console.WriteLine($"In BankAccount.Withdraw() for " +
$"${amountToWithdraw}.");
Balance -= amountToWithdraw;
return amountToWithdraw;
}
}
// SavingsAccount -- A bank account that draws interest
internal class SavingsAccount : BankAccount
{
private decimal InterestRate
{ get; set; }
// SavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
public SavingsAccount(decimal initialBalance, decimal interestRate)
: base(initialBalance)
{
InterestRate = interestRate / 100;
}
// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
internal override decimal Withdraw(decimal withdrawal)
{
// Take the $1.50 off the top.
Console.WriteLine("Deductng the SavingsAccount fee.");
base.Withdraw(1.5M);
// Now you can withdraw from what's left.
Console.WriteLine($"In SavingsAccount.Withdraw() for " +
$"${withdrawal}.");
return base.Withdraw(withdrawal);
}
}
The Withdraw()
method is marked as virtual
in the base class BankAccount
. Likewise, you see it marked override in the subclass SavingsAccount
. This version of the example also adds some Console.WriteLine()
calls so you can see what's happening. Here’s the Program
class code:
class Program
{
public static void MakeAWithdrawal(BankAccount ba, decimal amount)
{
ba.Withdraw(amount);
}
static void Main(string[] args)
{
BankAccount ba;
SavingsAccount sa;
// Create a bank account, withdraw $100, and
// display the results.
Console.WriteLine("Withdrawal: MakeAWithdrawal(ba, …)");
ba = new BankAccount(200M);
MakeAWithdrawal(ba, 100M);
Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
// Try the same trick with a savings account.
Console.WriteLine("
Withdrawal: MakeAWithdrawal(sa, …)");
sa = new SavingsAccount(200M, 12);
MakeAWithdrawal(sa, 100M);
Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
Console.Read();
}
}
Notice that all the decision-making code is removed from the HidingWithdrawalPolymorphically2
example. The output from executing this program is shown here:
Withdrawal: MakeAWithdrawal(ba, …)
In BankAccount.Withdraw() for $100.
BankAccount balance is $100.00
Withdrawal: MakeAWithdrawal(sa, …)
Deductng the SavingsAccount fee.
In BankAccount.Withdraw() for $1.5.
In SavingsAccount.Withdraw() for $100.
In BankAccount.Withdraw() for $100.
SavingsAccount balance is $98.50
Much of the power of polymorphism springs from polymorphic objects sharing a common interface. For example, given a hierarchy of Shape
objects — Circles
, Squares
, and Triangles
, for example — you can count on all shapes having a Draw()
method. Each object's Draw()
method is implemented quite differently, of course. But the point is that, given a collection of these objects, you can freely use a foreach
loop to call Draw()
or any other method in the polymorphic interface on the objects.
A duck is a type of bird. So are the cardinal and the hummingbird. In fact, every bird out there is a subtype of bird. The flip side of that argument is that no bird exists that isn't a subtype of Bird
. That statement doesn’t sound profound, but in a way, it is. The software equivalent of that statement is that all bird
objects are instances of the Bird
subclasses — there's never an instance of class Bird
. What’s a bird? It’s always a robin or a grackle or another specific species.
Different types of birds share many properties (otherwise, they wouldn’t be birds), yet no two types share every property. If they did, they wouldn’t be different types. For example, not all birds Fly()
the same way (or possibly at all). Ducks have one style, cardinals another. The hummingbird's style is completely different. And ostriches are only interested in sticking their heads in the sand and not flying at all (or perhaps not — see https://www.scienceworld.ca/stories/do-ostriches-really-bury-their-heads-sand/
). But if not all birds fly the same way and there’s no such thing as an instance of a generic Bird
, what the heck is Bird.Fly()
? The Bird.Fly()
method would need to be different for each subclass of Bird
. The following sections discuss this issue in detail.
People generate taxonomies of objects by factoring out commonalities. To see how factoring works, consider the two classes HighSchool
and University
, shown in Figure 6-1. This figure uses the Unified Modeling Language (UML), a graphical language that describes a class along with the relationship of that class to others. UML has become universally popular with programmers and is worth learning (to a reasonable extent) in its own right.
High schools and universities have several similar properties (refer to Figure 6-1) — many more than you may think. Both schools offer a publicly available Enroll()
method for adding Student
objects to the school. In addition, both classes offer a private member, numStudents
, which indicates the number of students attending the school. Another common feature is the relationship between students: One school can have any number of students — a student can attend only a single school at one time. Even high schools and most universities offer more than described, but only one of each type of member is needed for illustration.
In addition to the features of a high school, the university contains a method GetGrant()
and a data member avgSAT
. High schools have no SAT entrance requirements and students receive no federal grants.
Figure 6-1 is acceptable, as far as it goes, but some information is duplicated, and duplication in code (and UML diagrams) stinks. You can reduce the duplication by allowing the more complex class University
to inherit from the simpler HighSchool
class, as shown in Figure 6-2.
The HighSchool
class is left unchanged, but the University
class is easier to describe. You say that “a University
is a HighSchool
that also has an avgSAT
and a GetGrant()
method.” But this solution has a fundamental problem: A university isn't a high school with special properties.
You say, “So what? Inheriting works, and it saves effort.” True, but the problems are more than stylistic trivialities. This type of misrepresentation is confusing to the programmer, both now and in the future. Someday, a programmer who is unfamiliar with your programming tricks will have to read and understand what your code does. Misleading representations are difficult to reconcile and understand.
In addition, this type of misrepresentation can lead to problems down the road. Suppose that the high school decides to name a “favorite” student at the prom. The clever programmer adds the NameFavorite()
method to the HighSchool
class, which the application invokes to name the favorite Student
object.
But now you have a problem: Most universities don't name a favorite student. However, as long as University
inherits from HighSchool
, it inherits the NameFavorite()
method. One extra method may not seem like a big deal. “Just ignore it,” you say. However, one method is just one more brick in the wall of confusion. Extra methods and properties accumulate over time, until the University
class is carrying lots of extra baggage. Pity the poor software developer who has to understand which methods are “real” and which aren't.
To fix the source of the problem you must consider that a university isn’t a particular type of high school. A relationship exists between the two, but neither IS_A or HAS_A are the right ones. Instead, both high schools and universities are special types of schools. That’s what they have the most in common.
Figure 6-3 describes a better relationship. The newly defined class School
contains the common properties of both types of schools, including the relationship they both have with Student
objects. School
even contains the common Enroll()
method, although it's abstract because HighSchool
and University
usually don't implement Enroll()
the same way.
The classes HighSchool
and University
now inherit from a common base class. Each contains its unique members: NameFavorite()
in the case of HighSchool
, and GetGrant()
and avgSAT
for the University
. In addition, both classes override the Enroll()
method with a version that describes how that type of school enrolls students. In effect, the example extracts a superclass, or base class, from two similar classes, which now become subclasses. The introduction of the School
class has at least two big advantages:
University
is a School
, but it isn't a HighSchool
. Matching reality is nice but not conclusive.CommencementSpeech()
method to the University
class doesn't affect HighSchool
.This process of culling common properties from similar classes is known as factoring. This feature of object-oriented languages is important for the reasons described earlier in this minibook, plus one more: reducing redundancy.
Factoring can and usually does result in multiple levels of abstraction. For example, a program written for a more developed school hierarchy may have a class structure more like the one shown in Figure 6-4.
You can see that Figure 6-4 inserts a pair of new classes between University
and School
: HigherLearning
and LowerLevel
. It subdivides the new class HigherLearning
into College
and University
. This type of multitiered class hierarchy is common and desirable when factoring out relationships. They correspond to reality, and they can teach you subtle features of your solution.
Note, however, that no Unified Factoring Theory exists for any given set of classes. The relationship shown in Figure 6-4 seems natural, but suppose that an application cared more about differentiating types of schools administered by local politicians from those that aren't. This relationship, shown in Figure 6-5, is a more natural fit for that type of problem. No correct factoring exists: The proper way to break down the classes is partially a function of the problem being solved.
As intellectually satisfying as factoring is, it reveals a problem of its own. Revisit BankAccount
, introduced at the beginning of this chapter. Think about how you may go about defining the different member methods defined in BankAccount
.
Most BankAccount
member methods are no problem to refactor because both account types implement them in the same way. You should implement those common methods in BankAccount
. Withdraw()
is different, however. The rules for withdrawing from a savings account differ from those for withdrawing from a checking account. You have to implement SavingsAccount.Withdraw()
differently from CheckingAccount.Withdraw()
. But how are you supposed to implement BankAccount.Withdraw()
? Ask the bank manager for help. This is the conversation that could take place:
The problem is that the question doesn't make sense. No such thing as “just an account” exists. All accounts (in this example) are either checking accounts or savings accounts. The concept of an account is abstract: It factors out properties common to the two concrete classes. It’s incomplete because it lacks the critical method Withdraw()
. (After you delve into the details, you may find other properties that a simple account lacks.)
Abstract classes are used to describe abstract concepts. An abstract class is a class with one or more abstract methods. An abstract method is a method marked abstract
and has no implementation because it has no method body. You create the method body when you subclass from the abstract class. Consider the classes of the AbstractInheritance
program:
// AbstractBaseClass -- Create an abstract base class with nothing
// but an Output() method. You can also say "public abstract."
abstract public class AbstractBaseClass
{
// Output -- Abstract method that outputs a string
abstract public void Output(string outputString);
}
// SubClass1 -- One concrete implementation of AbstractBaseClass
public class SubClass1 : AbstractBaseClass
{
override public void Output(string source) // Or "public override"
{
string s = source.ToUpper();
Console.WriteLine($"Call to SubClass1.Output() from within {s}");
}
}
// SubClass2 -- Another concrete implementation of AbstractBaseClass
public class SubClass2 : AbstractBaseClass
{
public override void Output(string source) // Or "override public"
{
string s = source.ToLower();
Console.WriteLine($"Call to SubClass2.Output() from within {s}");
}
}
The program first defines the class AbstractBaseClass
with a single abstract Output()
method. Because it's declared abstract
, Output()
has no implementation — that is, no method body. Two classes inherit from AbstractBaseClass
: SubClass1
and SubClass2
. Both are concrete classes because they override the Output()
method with real methods and contain no abstract methods themselves.
The two subclass Output()
methods differ in a trivial way: Both accept input strings, which they send back to users. However, one converts the string to all caps before output and the other converts it to all-lowercase characters. Here is the Program
class for this example:
class Program
{
public static void Test(AbstractBaseClass ba)
{
ba.Output("Test");
}
static void Main(string[] args)
{
// You can't create an AbstractBaseClass object because it's
// abstract. C# generates a compile-time error if you
// uncomment the following line.
// AbstractBaseClass ba = new AbstractBaseClass();
// Now repeat the experiment with SubClass1.
Console.WriteLine("
creating a SubClass1 object");
SubClass1 sc1 = new SubClass1();
Test(sc1);
// And, finally, a SubClass2 object
Console.WriteLine("
creating a SubClass2 object");
SubClass2 sc2 = new SubClass2();
Test(sc2);
Console.Read();
}
}
This code looks much the same as the
example earlier in the chapter. PolymorphicInheritance
Main()
instantiates an object of each of the subclasses and then calls the Test()
method, which calls on the Output()
method in each class. The following output from this program demonstrates the polymorphic nature of AbstractInheritance
:
Creating a SubClass1 object
Call to SubClass1.Output() from within TEST
Creating a SubClass2 object
Call to SubClass2.Output() from within test
Notice something about the AbstractInheritance
program: It isn't legal to create an AbstractBaseClass
object, but the argument to Test()
is declared to be an object of the class AbstractBaseClass
or one of its subclasses. It's the subclasses clause that’s critical here. The SubClass1
and SubClass2
objects can be passed because each one is a concrete subclass of AbstractBaseClass
. The IS_A relationship applies. This powerful technique lets you write highly general methods.
You may decide that you don't want future generations of programmers to be able to extend a particular class. You can lock the class by using the keyword sealed
. A sealed class cannot be used as the base class for any other class. Consider this code snippet:
public class BankAccount
{
// Withdrawal -- You can withdraw any amount up to the
// balance; return the amount withdrawn
virtual public void Withdraw(decimal withdrawal)
{
Console.WriteLine("invokes BankAccount.Withdraw()");
}
}
public sealed class SavingsAccount : BankAccount
{
override public void Withdraw(decimal withdrawal)
{
Console.WriteLine("invokes SavingsAccount.Withdraw()");
}
}
public class SpecialSaleAccount : SavingsAccount // Oops!
{
override public void Withdraw(decimal withdrawal)
{
Console.WriteLine("invokes SpecialSaleAccount.Withdraw()");
}
}
This snippet generates the following compiler error:
'SpecialSaleAccount' : cannot inherit from sealed class 'SavingsAccount'
You use the
keyword to protect your class from the prying methods of a subclass. For example, allowing a programmer to extend a class that implements system security enables someone to create a security back door.sealed
Sealing a class prevents another program, possibly somewhere on the Internet, from using a modified version of your class. The remote program can use the class as is, or not, but it can’t inherit bits and pieces of your class while overriding the rest.