By Joe Mayo
In This Chapter
- Inheritance
- Encapsulating Object Internals
- Polymorphism
C# is a modern object-oriented programming language. As such it has many
new features to support object-oriented programming. The preceding chapter
covered the proper syntax of classes and their members. This chapter takes you a
step further. It builds upon what has already been presented to create
object-oriented programs.
This chapter discusses inheritance, the capability to derive new classes from
existing ones. It solidifies what has been presented about encapsulation. Then
it examines the nuances of polymorphism, allowing classes to dynamically modify
their runtime behavior. This chapter provides a basis for how to do detailed
design of object-oriented programs with C# as an implementation language.
Inheritance
Inheritance is an object-oriented term relating to how one class, a derived
class, can share the characteristics and behavior from another class, a base
class. There should be a natural parent/child relationship between the base
class and the derived class, respectively. This can be thought of as an "is
a" relationship, because the derived class can be identified by both its
class type and its base class type. Essentially, any base class members with
protected or greater access also belong to a derived class.
The benefits gained by this are the ability to reuse the base class members
and also to add members to the derived class. The derived class then becomes a
specialization of the parent. This specialization can continue for as many
levels as necessary, each new level derived from the base class above it. In the
opposite direction, going up the inheritance hierarchy, there is more
generalization at each new base class traversed. Regardless of how many levels
between classes, the "is a" relationship holds.
Base Classes
Normal base classes may be instantiated themselves, or inherited. Derived
classes inherit each base class member marked with protected or greater access.
The derived class is specialized to provide more functionality, in addition to
what its base class provides.
A derived class declares that it inherits from a base class by adding a colon
(:) and the base class name after the derived class name. Here's
an example:
public class Contact
{
string name;
string email;
string address;
public Contact()
{
// statements ...
}
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public string Email
{
get
{
return email;
}
set
{
email = value;
}
}
public string Address
{
get
{
return address;
}
set
{
address = value;
}
}
}
public class Customer : Contact
{
string gender;
decimal income;
public Customer()
{
// statements ...
}
}
In the example, the Contact class is inherited by the
Customer class. This means the Customer class possesses all
the same members as its base class (Contact) in addition to its own. In
this case, Customer has the properties Name, Email,
and Address.
Since Customer is a specialization of Contact, it has its own unique members:
gender and income.
Abstract Classes
Abstract classes are a special type of base classes. In addition to normal
class members, they have abstract class members. These Abstract class members
are methods and properties that are declared without an implementation. All
classes derived directly from abstract classes must implement these abstract
methods and properties.
Abstract classes can never be instantiated. This would be illogical, because
of the members without implementations. So what good is a class that can't
be instantiated? Lots! Abstract classes sit toward the top of a class hierarchy.
They establish structure and meaning to code. They make frameworks easier to
build. This is possible because abstract classes have information and behavior
common to all derived classes in a framework. Take a look at the following
example:
abstract public class Contact
{
protected string name;
public Contact()
{
// statements...
}
public abstract void generateReport();
abstract public string Name
{
get;
set;
}}
public class Customer : Contact
{
string gender;
decimal income;
int numberOfVisits;
public Customer()
{
// statements
}
public override void generateReport()
{
// unique report
}
public override string Name
{
get
{
numberOfVisits++;
return name;
}
set
{
name = value;
numberOfVisits = 0;
}
}
}
public class SiteOwner : Contact
{
int siteHits;
string mySite;
public SiteOwner()
{
// statements...
}
public override void generateReport()
{
// unique report
}
public override string Name
{
get
{
siteHits++;
return name;
}
set
{
name = value;
siteHits = 0;
}
}
}
This example has three classes. The first class, Contact, is now an
abstract class. This is shown as the first modifier of its class declaration.
Contact has two abstract members, and it has an abstract method named
generateReport(). This method is declared with the abstract modifier in
front of the method declaration. It has no implementation (no braces) and is
terminated with a semicolon. The Name property is also declared
abstract. The accessors of properties are terminated with semicolons.
The abstract base class Contact has two derived classes,
Customer and SiteOwner. Both of these derived classes
implement the abstract members of the Contact class. The
generateReport() method in each derived class has an override
modifier in its declaration. Likewise, the Name declaration contains an
override modifier in both Customer and SiteOwner.
The override modifier for the overridden generateReport()
method and Name property is mandatory. C# requires explicit declaration
of intent when overriding methods. This feature promotes safe code by avoiding
the accidental overriding of base class methods, which is what actually does
happen in other languages. Leaving out the override modifier generates
an error. Similarly, adding a new modifier also generates an error.
Abstract methods must be overridden and cannot be hidden, which the new
modifier or the lack of a modifier would be trying to do.
Notice the name field in the Contact class. It has a
protected modifier. Remember, a protected modifier allows
derived classes to access base class members. In this case, it enables the
overridden Name property to access the name field in the
Contact class.
The most famous of all abstract classes is the Object class. It may
be referred to as object or Object, but it's still the
same class. Object is the base class for all other classes in C#.
It's also the default base class when a base class is not specified. The
following class declarations produce the same exact results:
abstract public class Contact : Object
{
// class members
}
abstract public class Contact
{
// class members
}
Object is implicitly included as a base class if it is not already
declared. Besides providing the abstract glue to hold together the C# class
framework, object includes built-in functionality, some of which is
useful for derived classes to implement. Table 8.1 lists each object
method and its purpose.
Table 8.1 Object Class Methods
|
Method
|
Purpose
|
|
Equals()
|
Compares object references for equality.
|
|
GetHashCode()
|
Returns a hash code for an object.
|
|
GetType()
|
Returns the type of the object.
|
|
ToString()
|
Returns a string representation of an object.
|
|
Finalize()
|
Same as a destructor.
|
|
MemberwiseClone()
|
Performs shallow copy of an object.
|
All of the methods in Table 8.1 are public, except for
Finalize() and MemberwiseClone(), which are protected. The
GetType() and MemberwiseClone() methods may not be overridden,
but all others may. Listing 8.1 shows an example of using object methods.
Listing 8.1 Object Class Member Implementations in a Derived Class
using System;
public class WebSite
{
public string SiteName;
public string URL;
public string Description;
public WebSite()
{
}
public WebSite( string strSiteName, string strURL, string strDescription )
{
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
}
abstract public class Contact
{
protected string name;
public Contact()
{
// initialization code...
}
public abstract string generateReport();
abstract public string Name
{
get;
set;
}
}
public class SiteOwner : Contact
{
int siteHits;
WebSite mySite;
public SiteOwner()
{
mySite = new WebSite();
siteHits = 0;
}
public SiteOwner(string aName, WebSite aSite)
{
mySite = new WebSite(aSite.SiteName,
aSite.URL,
aSite.Description);
Name = aName;
}
public override string generateReport()
{
return this.ToString();
}
public override string Name
{
get
{
siteHits++;
return name;
}
set
{
name = value;
siteHits = 0;
}
}
public override string ToString()
{
return "[" +
Name +
", " +
siteHits.ToString() +
"]";
}
public override bool Equals(Object anOwner)
{
return this.ToString().Equals(anOwner.ToString());
}
public override int GetHashCode()
{
return this.ToString().GetHashCode();
}
public SiteOwner Clone()
{
return (SiteOwner) this.MemberwiseClone();
}
}
public class Test
{
public Test() {}
public static void Main()
{
WebSite mySite = new WebSite("Le Financier",
"http://www.LeFinancier.com",
"Fancy Financial Site");
SiteOwner firstOwner = new SiteOwner("Jack", mySite);
SiteOwner secondOwner = firstOwner.Clone();
Console.WriteLine("Report: {0}",
firstOwner.generateReport());
Console.WriteLine("To String: {0}",
firstOwner.ToString());
Console.WriteLine("Hash Code: {0}",
firstOwner.GetHashCode());
Console.WriteLine("Report: {0}",
secondOwner.generateReport());
Console.WriteLine("To String: {0}",
secondOwner.ToString());
Console.WriteLine("Hash Code: {0}",
secondOwner.GetHashCode());
Console.WriteLine(
"1stOwner: {0} equals: {1} 2ndOwner: {2}.",
firstOwner.Name,
firstOwner.Equals(secondOwner),
secondOwner.Name);
Console.WriteLine(
"2nd Equality Check: {0}",
firstOwner.Equals(secondOwner));
Console.WriteLine("Report: {0}",
firstOwner.generateReport());
Console.WriteLine("To String: {0}",
firstOwner.ToString());
Console.WriteLine("Hash Code: {0}",
firstOwner.GetHashCode());
Console.WriteLine("Report: {0}",
secondOwner.generateReport());
Console.WriteLine("To String: {0}",
secondOwner.ToString());
Console.WriteLine("Hash Code: {0}",
secondOwner.GetHashCode());
}
}
And here's its output:
Report: [Jack, 1]
To String: [Jack, 2]
Hash Code: 179554879
Report: [Jack, 1]
To String: [Jack, 2]
Hash Code: 179554879
1stOwner: Jack equals: False 2ndOwner: Jack.
2nd Equality Check: True
Report: [Jack, 7]
To String: [Jack, 8]
Hash Code: 179555189
Report: [Jack, 7]
To String: [Jack, 8]
Hash Code: 179555189
Listing 8.1 contains three classes to show implementation of object
methods. The Contact class is the base class for the SiteOwner
class. Although the Contact class possesses functionality and abstract
definitions, it does not contain overridden methods from the Object
class. The SiteOwner class does have the overridden methods from the
Object class. This shows that a class doesn't have to inherit
directly from a base class to override its members.
The SiteOwner class overrides three of Object's methods,
ToString(), Equals(), and GetHashCode(). Each of
these methods has the override modifier in its declaration. The
Object class method definitions for these members are not invoked
because the Object class method definitions are overridden by
SiteOwner.
The ToString() method returns a string representation of an
object's contents. This is often useful for debugging where the contents of
an object are dumped to an error log file or perhaps to the console. In this
case, ToString() concatenates Name and siteHits and
formats them into a string to be returned to the calling program. This example
uses the ToString() method extensively.
The Equals() method compares the current SiteOwner's
ToString() method to the value of the parameter's
ToString() method. This comparison takes advantage of the built-in
capabilities of the string type.
The GetHashCode() method executes the ToString() method of
the current SiteOwner and uses that to get a hash code. A hash value is
normally calculated from a key value that is not expected to change within an
object. In this case, the process is simplified by using the string type's
built-in GetHashCode() method. Hash codes are useful for any function
requiring a unique integer value from a class. The most common use of this is
with the HashTable collection class.
The Clone() method uses Object's
MemberwiseClone() method. The Object class's
MemberwiseClone() method can't be overridden and has protected
access. Therefore, other objects cannot call this method on an instance of
SiteOwner. This is why this method call is wrapped in the
Clone() method.
The output of this program is somewhat strange. The first comparison, where
firstOwner is compared to secondOwner, fails. However, the
second comparison, immediately after that, passes. What gives?
Fortunately, the ToString() printouts provide some clues. The second
parameter of the ToString() method output increments from 1 to 2 in the
first pair of printouts before the equality checks. After the equality checks,
the numbers increment from 7 to 8. Going back to the ToString() method
shows this second parameter is the siteHits field.
Further investigation reveals that the only place where the siteHits
field is modified is in the get accessor of the Name property.
This shows why the number is changing. Every time ToString() executes,
it uses the Name property. This invokes the get accessor of
Name, which increments siteName. Since, during processing of
the ToString() method, the get accessor of Name
executes before siteHits is read, the printout never shows
siteHits as being 0.
So, to explain the printouts: ToString() is called twice for each
object, firstOwner and secondOwner, incrementing
siteHits twice. This leaves siteHits at 2 on both objects. In
the equality check, firstOwner.Name is accessed, leaving
firstOwner.siteHits at 3. Now, secondOwner.siteHits is still 2
because its Name property has not been accessed. Therefore, when the
Equals() method is called, these two objects produce different strings
and are, in fact, not equal. Finally, secondOwner.Name is accessed,
incrementing its siteHits to 3. Now both objects produce the same
strings, so when Equals() is called again on the next line it returns
true. The rest of the printouts should be understandable after this
explanation.
Warning
Properties can have side
effects. In the ToString() method of this chapter, it seemed pretty
slick to update the siteHit every time the Name property was
read. Perhaps some motivation for this would be that every time a site was
visited, the Site Owner's name would be referenced. This was a narrow view
of how this class could be used. The choice to use the Name property in
the ToString() method seemed natural, but the side effect of
incrementing the siteHits field caused a potentially serious bug. When
building a class, think about how properties will be used.
A couple of Object methods not shown here are GetType() and
Finalize(). The GetType() method is shown later in the
chapter, when polymorphism is discussed. The Finalize() method is
normally never used in a class declaration. The destructor syntax is used
instead. Destructors and Finalizers are the same thing. During compilation, C#
converts all destructors to the Finalize() method, for compatibility
with other languages conforming to Common Language Infrastructure (CLI)
standards. This enables the garbage collector to work with Finalize()
methods instead of language specific syntax.
Calling Base Class Members
Derived classes can access the members of their base class if those members
have protected or greater access. Simply use the member name in the appropriate
context, just as if that member were a part of the derived class itself.
Here's an example:
abstract public class Contact
{
private string address;
private string city;
private string state;
private string zip;
public string FullAddress()
{
string fullAddress =
address + '\n' +
city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class Customer : Contact
{
public string GenerateReport()
{
string fullAddress = FullAddress();
// do some other stuff...
return fullAddress;
}
}
In this example, the GenerateReport() method of the
Customer class calls the FullAddress() method in its base
class, Contact. All classes have full access to their own members
without qualification. Qualification refers to using a class name with the dot
operator to access a class memberMyObject.SomeMethod(), for
instance. This shows that a derived class can access its base class members in
the same manner as its own.
Base class constructors can be called from derived classes. To call a base
class constructor, use the base() constructor reference. This is
desirable when it's necessary to initialize a base class appropriately.
Here's an example that shows the derived class constructor with an
address parameter:
abstract public class Contact
{
private string address;
public Contact(string address)
{
this.address = address;
}
}
public class Customer : Contact
{
public Customer(string address) : base(address)
{
}
}
In this code, the Customer class does not have an address, so it
passes the parameter to its base class constructor by adding a colon and the
base keyword with the parameter to its declaration. This calls the
Contact constructor with the address parameter, where the
address field in Contact is initialized.
Warning
Depending on the design of a class hierarchy, failure to initialize base
class constructors may leave code in an inconsistent state.
The following example will not compile. It illustrates the effects of not
including a default constructor in a class definition:
abstract public class Contact
{
private string address;
public Contact(string address)
{
this.address = address;
}
}
public class Customer : Contact
{
public Customer(string address)
{
}
}
In this example, the Customer constructor does not call the base
class constructor. This is obviously a bug, since the address field
will never be initialized.
When a class has no explicit constructor, the system assigns a default
constructor. The default constructor automatically calls a default or
parameterless base constructor. Here's an example of automatic default
constructor generation that would occur for the preceding example:
public Customer() : Contact()
{
}
When a class does not declare any constructors, the code in this example is
automatically generated. The default base class constructor is called implicitly
when no derived class constructors are defined. Once a derived class constructor
is defined, whether or not it has parameters, a default constructor will not be
automatically defined, as the preceding code showed.
Hiding Base Class Members
Sometimes derived class members have the same name as a corresponding base
class member. In this case, the derived member is said to be "hiding"
the base class member. When hiding occurs, the derived member is masking the
functionality of the base class member. Users of the derived class won't
be able to see the hidden member; they'll see only the derived class member.
The following code shows how hiding a base class member works. If you're
compiling this example now, please disregard the compiler warning, which I explain
at the start of the next section, "Versioning."
abstract public class Contact
{
private string address;
private string city;
private string state;
private string zip;
public string FullAddress()
{
string fullAddress =
address + '\n' +
city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact
{
public string FullAddress()
{
string fullAddress;
// create an address...
return fullAddress;
}
}
In this example, both SiteOwner and its base class,
Contact, have a method named FullAddress(). The
FullAddress() method in the SiteOwner class hides the
FullAddress() method in the Contact class. This means that
when an instance of a SiteOwner class is invoked with a call to the
FullAddress() method, it is the SiteOwner class
FullAddress() method that is called, not the FullAddress()
method of the Contact class.
Although a base class member may be hidden, the derived class can still access
it. It does this through the base identifier. Sometimes this is desirable.
It is often useful to take advantage of the base class functionality and then
add to it with the derived class code. The next example shows how to refer to
a base class method from the derived class. If compiling this code now, please
disregard the warnings, which I explain at the start of the next section, "Versioning."
abstract public class Contact
{
private string address;
private string city;
private string state;
private string zip;
public string FullAddress()
{
string fullAddress =
address + '\n' +
city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact
{
public string FullAddress()
{
string fullAddress = base.FullAddress();
// do some other stuff...
return fullAddress;
}
}
In this particular example, the FullAddress() method of the
Contact class is called from within the FullAddress() method
of the SiteOwner class. This is accomplished with a base class
reference. This provides another way to reuse code and add on to it with
customized behavior.
Versioning
Versioning, in the context of inheritance, is a C# mechanism that allows
modification of classes (creating new versions) without accidentally changing
the meaning of the code. Hiding a base class member with the methods previously
described generates a warning message from the compiler. This is because of the
C# versioning policy. It's designed to eliminate a class of problems
associated with modifications to base classes.
Warning
Often these warning messages
scroll off the screen or are overlooked during compilation in an IDE. These
overlooked warnings could be early indications of a bug.
Here's the scenario: A developer creates a class that inherits from a
third-party library. For the purposes of this discussion, we assume that the
Contact class represents the third-party library. Here's the
example:
public class Contact
{
// does not include FullAddress() method
}
public class SiteOwner : Contact
{
public string FullAddress()
{
string fullAddress = mySite.ToString();
return fullAddress;
}
}
In this example, the FullAddress() method does not exist in the base
class. There is no problem yet. Later on, the creators of the third-party
library update their code. Part of this update includes a new member in a base
class with the exact same name as the derived class:
public class Contact
{
private string address;
private string city;
private string state;
private string zip;
public string FullAddress()
{
string fullAddress =
address + '\n' +
city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact
{
public string FullAddress()
{
string fullAddress = mySite.ToString();
return fullAddress;
}
}
In this code, the base class method FullAddress() contains different
functionality than the derived class method. In other languages, this scenario
would break the code because of implicit polymorphism. (Polymorphism is
discussed later in this chapter.) However, this does not break any code in C#
because when the FullAddress() method is called on SiteOwner,
it is still the SiteOwner class method that gets called.
This scenario generates a warning message. One way to eliminate the warning
message is to place a new modifier in front of the derived class method
name, as the following example shows:
using System;
public class WebSite
{
public string SiteName;
public string URL;
public string Description;
public WebSite()
{
}
public WebSite( string strSiteName, string strURL, string strDescription )
{
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
public override string ToString()
{
return SiteName + ", " +
URL + ", " +
Description;
}
}
public class Contact
{
public string address;
public string city;
public string state;
public string zip;
public string FullAddress()
{
string fullAddress =
address + '\n' +
city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact
{
int siteHits;
string name;
WebSite mySite;
public SiteOwner()
{
mySite = new WebSite();
siteHits = 0;
}
public SiteOwner(string aName, WebSite aSite)
{
mySite = new WebSite(aSite.SiteName,
aSite.URL,
aSite.Description);
Name = aName;
}
new public string FullAddress()
{
string fullAddress = mySite.ToString();
return fullAddress;
}
public string Name
{
get
{
siteHits++;
return name;
}
set
{
name = value;
siteHits = 0;
}
}
}
public class Test
{
public static void Main()
{
WebSite mySite = new WebSite("Le Financier",
"http://www.LeFinancier.com",
"Fancy Financial Site");
SiteOwner anOwner = new SiteOwner("John Doe", mySite);
string address;
anOwner.address = "123 Lane Lane";
anOwner.city = "Some Town";
anOwner.state = "HI";
anOwner.zip = "45678";
address = anOwner.FullAddress(); // Different Results
Console.WriteLine("Address: \n{0}\n", address);
}
}
Here's the output:
Address:
Le Financier, http://www.LeFinancier.com, Fancy Financial Site
This has the effect of explicitly letting the compiler know the
developer's intent. Placing the new modifier in front of the
derived class member states that the developers know there is a base class
method with the same name, and they definitely want to hide that member. This
prevents breakage of existing code that depends on the implementation of the
derived class member. With C#, the method in the derived class is called when an
object of the derived class type is used. Likewise, the method in the base class
is called when an object of the Base class type is called. Another problem this
presents is that the base class may present some desirable new features that
wouldn't be available through the derived class.
To use these new features requires one of a few different workarounds. One
option would be to rename the derived class member, which would allow programs
to use a base class method through a derived class member. The drawback to this
option would be if there were other classes relying upon the implementation of
the derived class member with the same name. This scenario will break code and,
for this reason, is considered extremely bad form.
Another option is to define a new method in the derived class that called the
base class method. This allows users of the derived class to have the new
functionality of the base class, yet retain their existing functionality with
the derived class. While this would work, there are maintainability concerns for
the derived class.
Sealed Classes
Sealed classes are classes that can't be derived from. To prevent other
classes from inheriting from a class, make it a sealed class. There are a couple
good reasons to create sealed classes, including optimization and security.
Sealing a class avoids the system overhead associated with virtual methods.
(The "Polymorphism" section later in this
chapter has in-depth discussion of virtual methods.) This allows the compiler
to perform certain optimizations that are otherwise unavailable with normal
classes.
Another good reason to seal a class is for security. Inheritance, by its very
nature, dictates a certain amount of protected access to the internals of a
potential base class. Sealing a class does away with the possibility of
corruption by derived classes. A good example of a sealed class is the String
class. The following example shows how to create a sealed class:
public sealed class CustomerStats
{
string gender;
decimal income;
int numberOfVisits;
public CustomerStats()
{
}
}
public class CustomerInfo : CustomerStats // error
{
}
public class Customer
{
CustomerStats myStats; // okay
}
This example generates a compiler error. Since the CustomerStats class is
sealed, it can't be inherited by the CustomerInfo class. The CustomerStats
class was meant to be used as an encapsulated object in another class. This is
shown by the declaration of a CustomerStats object in the Customer class.
Encapsulating Object Internals
Encapsulation is an object-oriented concept associated with hiding the
internals of a class from the outside world. C# has several mechanisms for
supporting encapsulation. Some, such as properties and indexers, are new
concepts we haven't seen implemented in languages before. There are several
reasons to take advantage of C#'s built-in mechanisms for managing
encapsulation:
Good encapsulation reduces coupling. By using only those class members
exposed, users can write code with less dependency on that class.
Internal implementation of a class can freely change. This reduces the
possibility of breaking someone else's code.
A class has a much cleaner interface. Users only see those members that
are exposed, which reduces the amount of understanding they need to use a class.
It simplifies reuse.
Data Hiding
One of the most useful forms of encapsulation is data hiding. Most of the
time, users shouldn't have access to the internal data of a class. Class
data represents the state of an object. A class normally has full control of its
own state to guarantee its consistent behavior. Anytime access to data is
opened, the potential of someone else wreaking havoc with the operation of that
class increases.
There are times when it's logical and necessary to expose class
dataespecially if it's necessary to expose constants, enumerations,
and read-only fields. Perhaps a design goal is to increase the efficiency of
data access for a field that's accessed frequently. The decisions made
depend on the requirements. However, give serious consideration to proper
encapsulation of class information.
Modifiers Supporting Encapsulation
Manage class encapsulation with appropriate use of C# access modifiers, which
specify who can access class members. They also control the method of
access:
Private access is the most restrictive. This allows members, only within
a class, to access another member marked as private. Anyone outside the class
cannot access this member. They won't even know it's there without
source code or documentation telling them otherwise. Private access is useful
because it allows modification of a private member implementation without anyone
knowing.
Protected access is a little less restrictive than private. Users may
know the member is in a class, but they can't access protected members
directly. The only way to use a protected member is through inheritance. A
derived class has full access to protected base class members. This is
regardless of the depth of the hierarchy. The protected member need not be in
the derived class's immediate base class. Protected access is good for
optimization when a derived class needs frequent access to base class
information.
Internal access is for use only in the program or project where the data
resides. If data only has particular relevancy in the context of a single
program, this access is useful. This type of modifier would be used for in-house
projects where a given class member was used by other teams on the same project.
Other programs or user code would have no idea that this internal class member
existed.
Protected Internal is a combination of protected and internal modifiers.
It's a little bit more open than straight internal, allowing all members of
a program to access the member. Additionally, derived classes of base classes in
a program with protected internal members can access those members if they are
either other program members or external user code. This access is useful for
third-party libraries where users need access to protected members, with the
added convenience that in-house developers would have free utilization of that
class member without restriction.
Public access is the least restrictive of all. It lets anyone and
everyone have access to class members without restriction. Public access is
necessary to publish the interface of a class. It is through these members that
communication with a class is accomplished. Great care should be taken to ensure
that only those members contributing to effective use of an interface to a class
are made public.
Other Encapsulation Strategies
The purpose of properties and indexers is to encapsulate the details of a
class and provide a public interface to users of the class. See Chapter 7,
"Working with Classes," for a detailed description of properties and
indexers. Since one of their purposes is encapsulation, it's wise to use
them as much as practical.
Relationship of Encapsulation to
Inheritance
Encapsulation implies containment, where one object is inside of another.
This is the "has a" relationship. An object inside another object is a
field of its containing object.
When speaking of inheritance, it's useful to think of the "is
a" relationship, where a class is a part of the classification hierarchy
associated with its parent class.
Inheritance and containment are two different concepts, but one can be used
improperly in place of the other. This text has repeatedly spoken of the
"natural" inheritance hierarchy that is implemented between objects.
Studies have shown inheritance is sometimes used where it doesn't
necessarily make sense. For a good discussion, see C++ Programming Style,
Tom Cargill, 1992, Addison-Wesley. Inheritance is good when applied naturally
and is a good fit for the problem.
An alternative to inheritance is containment. By encapsulating one object
within another, a class can control what behavior is used by derived classes. If
need be, it can provide access to each member of the contained object through
its own methods. In contrast, all class members in a base class, accessible to
a derived class, are also accessible to further derivation. The efforts required
to restrict base class access through a derived class would be tedious and error
prone. Containment helps encapsulate the contained object's members.
Another factor to consider is that C# has only single inheritance. This means
it can inherit functionality only from a single base class. Therefore, if a
class already inherits from a base class, containment is the only way to reuse
pre-canned functionality.
For C++ Programmers
C++ has multiple
inheritance, whereas C# allows only single inheritance.
Polymorphism
Earlier sections of this chapter covered abstract classes, including the
ultimate abstract class, object. It showed how to implement overrides
of virtual classes in the object class. This section goes further by
explaining how virtual classes are overridden, why, and what good it is. This
capability enables an object-oriented programming concept known as
polymorphism.
Implementing Polymorphism
To begin, it's useful to get an appreciation of the problem polymorphism
solves. The key factor is the ability to dynamically invoke methods in a class
based on their type. Essentially, a program would have a group of objects,
examine the type of each one, and execute the appropriate method. Here's an
example:
using System;
public class WebSite
{
public string SiteName;
public string URL;
public string Description;
public WebSite()
{
}
public WebSite( string strSiteName, string strURL, string strDescription )
{
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
public override string ToString()
{
return SiteName + ", " +
URL + ", " +
Description;
}
}
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public new string UpdateNotify()
{
return @"
This is to let you know your
favorite site, Financial Times,
has been updated with new links";
}
}
public class SiteOwner : Contact
{
WebSite mySite;
public SiteOwner(string aName, WebSite aSite)
{
mySite = new WebSite(aSite.SiteName,
aSite.URL,
aSite.Description);
}
public new string UpdateNotify()
{
return @"
This is to let you know your site, " + "\n" +
mySite.SiteName + @", has been added as
a link to Financial Times.";
}
}
public class Test
{
public static void Main()
{
WebSite leFin = new WebSite("Le Financier",
"http://www.LeFinancier.com",
"Fancy Financial Site");
Contact[] Contacts = new Contact[2];
Contacts[0] = new SiteOwner("Pierre Doe", leFin);
Contacts[1] = new Customer();
foreach (Contact poc in Contacts)
{
if (poc is SiteOwner)
{
Console.WriteLine("Message: {0}\n",
((SiteOwner)poc).UpdateNotify());
}
else
{
Console.WriteLine("Message: {0}\n",
((Customer)poc).UpdateNotify());
}
}
}
}
In this example, the Main() method of the Test class
creates an array of Contact objects. It puts a SiteOwner
object and a Customer object in the array. Each of these classes has an
UpdateNotify() method, and the point of this program is to call the
UpdateNotify() method belonging to each object.
The foreach loop checks the type of each object with the is
operator. Depending on the type, the poc object is cast to that type
and used in the Console.WriteLine() method. Here's another
technique that could be used in the preceding foreach loop:
foreach (Contact poc in Contacts)
{
SiteOwner anOwner = poc as SiteOwner;
if (anOwner != null)
{
Console.WriteLine("Message: {0}\n",
anOwner.UpdateNotify());
}
else
{
Console.WriteLine("Message: {0}\n",
((Customer)poc).UpdateNotify());
}
}
This example uses the as operator. The as operator does an
assignment of one object to another object when the type on the right side of
the as operator is the same as the object on its left. Otherwise, the
as operator returns null. This is more efficient than the
is operator because the is operator required a type check and
an assignment in two separate steps. In that last code example, the if
statement only needs to check whether the value is not null and to
execute the SiteOwner class UpdateNotify() method when this
condition is true. Otherwise, the UpdateNotify() method of the
Customer class is executed. Although the cast is necessary for
Customer objects, using as is still more efficient because
half of the objects don't need a cast.
Tip
Use the as operator for
greater efficiency when iterating through a list of objects requiring type
checks and casting. Use the is operator when a single object is being
type checked or when casting is not necessary.
The preceding examples accomplish the task of dynamically invoking object
methods. However, there is a more efficient and elegant way to accomplish the
same thing. This method is called polymorphism. Polymorphism is efficient
because C# rather than explicit coding is managing this process. It's also
more elegant because there is less code, which makes for a simpler
implementation.
Polymorphism is the capability of a program to carry out dynamic operations
by implementing methods of multiple derived classes through a common base class
reference. Another definition of polymorphism is the ability to treat different
objects the same way. This means that the runtime type of an object determines
its behavior rather than the compile-time type of its reference. Chapter 6,
"Object and Component Concepts," discussed polymorphic behavior at a
simplified and abstract level. It may help to review Chapter 6 and visualize
those concepts before proceeding.
It's sometimes necessary to manipulate a collection of objects with
multiple object types. A common task is to iterate through these objects
performing some type of similar operation. Since the object types are different,
it usually isn't possible to perform the same operation on each one.
However, it would be convenient to request the same type of operation with
specialized behavior for each object type. This is accomplished through
polymorphism in a very efficient manner.
Imagine a scenario where a Web site creates notifications to multiple
contacts about updates. There are different types of Contacts that
require different types of notifications, but they are all Contacts.
This example makes the assumption that Contact is a well-defined and
natural abstraction for this purpose.
There are two types of Contacts interested in Web site updates:
Customer and SiteOwner. While both types of Contacts
are interested in updates, the actual message generated to each will be
different, because each of their particular interests is different. Poly-
morphism is a useful tool to solve this problem. Take a look at the following
example:
using System;
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public override string UpdateNotify()
{
return @"
This is to let you know your
favorite site, Financial Times,
has been updated with new links";
}
}
public class SiteOwner : Contact
{
string siteName;
public SiteOwner(string sName)
{
siteName = sName;
}
public override string UpdateNotify()
{
return @"
This is to let you know your site, " + "\n" +
siteName + @", has been added as
a link to Financial Times.";
}
}
This example shows three primary classes: Contact,
Customer, and SiteOwner. Contact is the abstract base
class for the other two, providing a virtual UpdateNotify() method.
Both Customer and SiteOwner override the Contact
class UpdateNotify() method.
Virtual methods are those base class methods that enable polymorphism to
work. They use the virtual modifier to indicate that they can be
overridden by derived classes. The difference between abstract methods and
virtual methods is that virtual methods have implementations, and abstract
methods don't. Abstract methods are implicitly virtual, and they must be
overridden. Virtual methods don't have to be overridden.
The override keyword indicates that a derived class method can be
invoked at runtime, instead of the virtual base class method. The key points
are
The object reference is a base class type, declaring the virtual
method.
The runtime object is of the derived type with the overriding
method.
The following code snippet shows polymorphism at work:
public class Test
{
public static void Main()
{
Contact[] Contacts = new Contact[2];
Contacts[0] = new SiteOwner("Le Financier");
Contacts[1] = new Customer();
foreach (Contact poc in Contacts)
{
Console.WriteLine("Message: {0}\n",
poc.UpdateNotify());
}
}
}
And here's the output:
Message:
This is to let you know your site,
Le Financier, has been added as
a link to Financial Times.
Message:
This is to let you know your
favorite site, Financial Times,
has been updated with new links
This example shows a simple implementation using polymorphism. The program
declares the array Contacts (plural) of type Contact. This is
the first key point, the fact that the Contacts array possesses base
class references to a virtual method. Also, Contact is the compile-time
type of each Contacts array object.
Next, the program assigns objects of type SiteOwner and
Customer to the Contacts array elements. This is the second
key point, the fact that the runtime type of the object is a derived class with
an override on a base class virtual method.
At runtime, the foreach loop uses the UpdateNotify() method
of each Contacts array object. Although the compile-time type of each
object is Contact, the Contact class virtual
UpdateNotify() method is not executed. Instead, the overridden
UpdateNotify() method of each derived class is executed.
Hiding Again
Now let's look at some scenarios with polymorphism-related modifiers and
versioning. Using an override modifier in a derived class where there
is no corresponding virtual method in a base class yields an error as in the
following example:
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public override string SendMail() {}// error
public override string UpdateNotify(int number) {}// error
}
This code produces an error during compilation. This is because the
SendMail() method is declared with an override modifier, and
there is not a corresponding virtual method to be overridden.
The same error occurs with the UpdateNotify() method in the
Customer class. However, the reason is somewhat different. The
UpdateNotify() method in the Customer class has a parameter,
but the UpdateNotify() method of the Contact class
doesn't have any parameters. Since there is a signature mismatch,
polymorphism can't occur, and compilation generates an error. Remember, a
method's signature consists of its name, number of parameters, and type of
each parameter.
A virtual modifier by itself presents no problem at all. It's normal to
label a method with a virtual modifier to indicate its availability for
polymorphism to potential derived classes. This way any future classes may
inherit from the class and override its virtual method.
When a derived class adds a normal method, with no modifiers, with the same
signature of a base class virtual method, it generates a compile-time warning.
This is the same behavior as described earlier with hiding. If you compile the
following code, it generates a compiler warning:
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public string UpdateNotify()
{
return @"
This is to let you know your
favorite site, Financial Times,
has been updated with new links";
}
}
There are two ways to correct this example. One is to add an
override modifier to the derived class method:
public override string UpdateNotify() {...}
The other way is to add the new modifier to the derived class
method. This hides the base class virtual method. Since the derived class hides
the base class virtual method, any further derivations from the original derived
class are not able to see the original base class virtual method. Here's an
example:
public new string UpdateNotify() {...}
Earlier, there was an example of the UpdateNotify() method where
each derived class overrode the virtual UpdateNotify() method in the
Contact class. Here's an example of what happens when a virtual
method is not overridden:
using System;
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public new string UpdateNotify()
{
return @"
This is to let you know your
favorite site, Financial Times,
has been updated with new links";
}
}
public class SiteOwner : Contact
{
string siteName;
public SiteOwner(string sName)
{
siteName = sName;
}
public override string UpdateNotify()
{
return @"
This is to let you know your site, " + "\n" +
siteName + @", has been added as
a link to Financial Times.";
}
}
public class Test
{
public static void Main()
{
Contact[] Contacts = new Contact[2];
Contacts[0] = new SiteOwner("Le Financier");
Contacts[1] = new Customer();
foreach (Contact poc in Contacts)
{
Console.WriteLine("Message: {0}\n",
poc.UpdateNotify());
}
}
}
And here's the output:
Message:
This is to let you know your site,
Le Financier, has been added as
a link to Financial Times.
Message: Web Site Change Notification
This example shows what happens when virtual methods are not overridden. The
UpdateNotify() method of the Customer class has a new
modifier but does not have an override modifier. When the
foreach loop of the Main() method of the Test class
executes, it operates on Contact references to objects of type
Customer and SiteOwner.
Viewing the output, the UpdateNotify() method of the
SiteOwner class executes first. Since it overrides the virtual
UpdateNotify() method of the Contact class, its method is
executed. Next, the UpdateNotify() method of the Contact class
executes. This time the UpdateNotify() method of the Customer
class isn't executed, because the Customer class does not override
the virtual UpdateNotify() method of the Contact class. When
the runtime type of an object does not override a method of a virtual base
class, the virtual method in the base class executes.
Most-Derived Implementations
The most derived implementation of a method is the lowest class in a
hierarchy, down to the current class, that holds an implementation of a virtual
method. The examples presented thus far have a base class and a derived class.
To determine the most derived implementation, see whether the current object
being referred to has an overridden implementation of a virtual method. If so,
it is the most derived implementation. Otherwise, check the immediate base class
of the current class, continuing up the hierarchy until an overriding method is
found or the original virtual method itself is found. When there is only a
single virtual method with no overrides in derived classes, then that virtual
method is the most derived implementation. Here's an example that helps
demonstrate how this works:
using System;
abstract public class Contact
{
public virtual string UpdateNotify()
{
return "Web Site Change Notification";
}
}
public class Customer : Contact
{
public new string UpdateNotify()
{
return @"
This is to let you know your
favorite site, Financial Times,
has been updated with new links";
}
}
public class SiteOwner : Contact
{
string siteName;
public SiteOwner(string sName)
{
siteName = sName;
}
public override string UpdateNotify()
{
return @"
This is to let you know your site, " + "\n" +
siteName + @", has been added as
a link to Financial Times.";
}
}
public class PayingSiteOwner : SiteOwner
{
public PayingSiteOwner(string ownerName)
: base(ownerName)
{
// Initializers
}
public new string UpdateNotify()
{
return @"
This is to let you know your bill
is coming due. We award early
payment with a 5% discount.";
}
}
public class Test
{
public static void Main()
{
Contact[] Contacts = new Contact[3];
Contacts[0] = new SiteOwner("Le Financier");
Contacts[1] = new Customer();
Contacts[2] = new PayingSiteOwner("Rip Uoff");
foreach (Contact poc in Contacts)
{
Console.WriteLine("Message: {0}\n",
poc.UpdateNotify());
}
}
}
And here's the output:
Message:
This is to let you know your site,
Le Financier, has been added as
a link to Financial Times.
Message: Web Site Change Notification
Message:
This is to let you know your site,
Rip Uoff, has been added as
a link to Financial Times.
The PayingSiteOwner class inherits SiteOwner, which in turn
inherits Contact. The PayingSiteOwner class has an
UpdateNotify() method that hides inherited UpdateNotify()
methods. The SiteOwner class has an UpdateNotify() method that
overrides the virtual UpdateNotify() method in the Contact
class.
In the Main() method of the Test class is the declaration
of both the SiteOwner and PayingSiteOwner classes. They are
assigned to a Contact class reference. When the foreach loop
executes, it calls the UpdateNotify() methods of each object in the
array. Looking at the output, there are three outputs from
UpdateNotify() methods. The first is from the overriding method in
SiteOwner. The second is from the Contact class, which
isn't overridden by the derived Customer class. The third entry is
also from the SiteOwner class.
The reason for the third entry is because the UpdateNotify() method
of the SiteOwner class is the most derived implementation of the
UpdateNotify() method. Although the runtime object of the third entry
is of the PayingSiteOwner class type, its UpdateNotify()
method does not override the parent class UpdateNotify() method.
Since the object reference is a Contact class type, it searches for
the most derived implementation of the virtual UpdateNotify() method.
The search begins with the PayingSiteOwner class, where it doesn't
find an override. Next, the base class of PayingSiteOwner,
SiteOwner, is searched. A valid override exists there, so that is the
method that gets executed.
If the example code was changed to
public class PayingSiteOwner : SiteOwner
{
public PayingSiteOwner(string ownerName)
: base(ownerName)
{
// Initializers
}
public override string UpdateNotify()
{
return @"
This is to let you know your bill
is coming due. We award early
payment with a 5% discount.";
}
}
The output would be
Message:
This is to let you know your site,
Le Financier, has been added as
a link to Financial Times.
Message: Web Site Change Notification
Message:
This is to let you know your bill
is coming due. We award early
payment with a 5% discount.
The modifier on the UpdateNotify() method of the
PayingSiteOwner class was changed from new to
override. This made the UpdateNotify() method of the
PayingSiteOwner class the most derived implementation, resulting in it
being executed as the third entry of the output.
Polymorphic Properties
C# permits polymorphism with property accessors. The same rules applied to
methods also apply to properties. Here's an example.
using System;
public class SiteStats
{
public int numberOfVisits = 0;
}
abstract public class Contact
{
protected string name;
public virtual string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}
public class Customer : Contact
{
SiteStats myStats = new SiteStats();
public override string Name
{
get
{
myStats.numberOfVisits++;
Console.WriteLine("Number of visits: {0}",
myStats.numberOfVisits);
return name;
}
set
{
base.Name = value;
myStats.numberOfVisits = 0;
Console.WriteLine("Name: {0}", Name);
}
}
}
public class Test
{
public static void Main()
{
Contact myContact = new Customer();
myContact.Name = "George";
}
}
And here's the output:
Number of visits: 1
Name: George
In this example, the Contact class declares the Name
property with a virtual modifier. The Customer class overrides each of
the Name property accessors. The set accessor of the
Customer class Name property calls the set accessor
of the Contact class Name property by using the base
keyword.
The reason the output reflects access to both the get and
set accessors can be seen in the set accessor of the
Customer class Name property. It uses the Name
property as an argument to the Console.WriteLine() method call. This
causes a get to be performed using that class Name property.
The get does its own Console.WriteLine() method, which results
in the first line of output. The Console.WriteLine() method of the
set accessor executes, producing the second line in the output.
Warning
C# allows both the get
and set accessors of a property to reference the same property. Beware
of creating circularities where the get accessor causes the
set accessor to be called and vice versa. This results in an endless
loop.
Polymorphic Indexers
C# permits polymorphism with indexer accessors. The same rules applied to
methods and properties also apply to indexers. Here's an example:
using System;
using System.Collections;
public class SiteList
{
protected SortedList sites;
public SiteList()
{
sites = new SortedList();
}
public int NextIndex
{
get {
return sites.Count;
}
}
public virtual string this[int index]
{
get
{
return (string) sites.GetByIndex(index);
}
set
{
sites[index] = value;
}
}
}
public class FinancialSiteList : SiteList
{
public override string this[int index]
{
get
{
Console.WriteLine("FinancialSiteList Indexer Get");
if (index > sites.Count)
return (string)null;
return base[index];
}
set
{
Console.WriteLine("FinancialSiteList Indexer Set");
base[index] = value;
}
}
}
class SiteManager
{
SiteList sites = new SiteList();
public static void Main()
{
SiteManager mgr = new SiteManager();
mgr.sites = new FinancialSiteList();
mgr.sites[mgr.sites.NextIndex] = "Great Site!";
Console.WriteLine("Site: {0}",
mgr.sites[0].ToString());
}
}
And here's the output:
FinancialSiteList Indexer Set
FinancialSiteList Indexer Get
Site: Great Site!
In this example, the SiteList class declares its indexer as virtual.
The FinancialSiteList indexer overrides the indexer of its base class,
SiteList. The FinancialSiteList indexer accessors call the
SiteList indexer accessors by using the base keyword with the
index value.
The Main() method of the SiteManager class creates an
object of type FinancialSiteList and assigns it to the sites
field of the mgr object. The sites field is a
SiteList class type. Then it assigns a string to the sites
object. Because the FinancialSiteList indexer accessors override the
SiteList indexer, the FinancialSiteList indexer set
accessor is executed.
Viewing the output shows that the Console.WriteLine() method in the
FinancialSiteList set accessor executed first. After the
string is assigned to sites, the Main() method of
SiteManager executes a Console.WriteLine() call. Because of
polymorphism, this calls the get accessor of the
FinancialSiteList class, which prints out the second line of output.
Finally, the last line is printed from the Main() method of the
SiteManager class.
Summary
In the first part of this chapter, I discussed inheritance. Issues associated
with inheritance include base classes, abstract base classes, accessing base
class members, hiding base class members, versioning, and sealed classes.
The next part covered encapsulation. Relevant encapsulation topics included
data hiding, modifiers supporting encapsulation, encapsulation strategies using
indexers and properties, and the relationship of encapsulation to
inheritance.
Finally, the subject of polymorphism was explained. This section included
strategies on how to implement polymorphism, the use of hiding in a polymorphic
context, determining the most derived implementation of a virtual method,
polymorphism with properties, and polymorphism with indexers.
This chapter touched upon the ability of classes to have multiple members
with the same name when it presented constructor overloading. This is not all
that C# can do with overloading, and you'll see why in the next chapter,
"Overloading Class Members and Operators."
© Copyright Pearson Education. All rights reserved.