The previous chapter demonstrated how to create new types by
declaring classes. The current chapter explores the relationship
between objects in the real world and how to model these
relationships in your code. This chapter focuses on
specialization, which is implemented in C#
through inheritance. This chapter also explains
how instances of more specialized classes can be treated as if they
were instances of more general classes, a process known as
polymorphism
.
This chapter ends with a consideration of sealed
classes, which can’t be specialized;
abstract classes, which exist only to be
specialized; and a discussion of the root of all classes, the class
Object
.
VB6 programmers take note: like VB.NET, C# provides full object-oriented technology, including inheritance, polymorphism, and encapsulation. These are relatively new topics for VB6 programmers. You should study them carefully; they affect your class and application design.
Classes and their instances (objects) don’t exist in a vacuum, but rather, in a network of interdependencies and relationships, just as we, as social animals, live in a world of relationships and categories.
The is-a relationship is one of specialization. When we say that a dog is-a mammal, we mean that the dog is a specialized kind of mammal. It has all the characteristics of any mammal (it bears live young, nurses with milk, has hair), but it specializes these characteristics to the familiar characteristics of canine domesticus. A cat is also a mammal. As such, we expect it to share certain characteristics with the dog that are generalized in mammals, but to differ in those characteristics that are specialized in cats.
The specialization and generalization relationships are both reciprocal and hierarchical. They are reciprocal because specialization is the obverse side of the coin from generalization. Thus, dog and cat specialize mammal, and mammal generalizes from dog and cat.
These relationships are hierarchical because they create a relationship tree, with specialized types branching off from more generalized types. As you move up the hierarchy, you achieve greater generalization. You move up toward mammal to generalize that dogs and cats and horses all bear live young. As you move down the hierarchy, you specialize. Thus, the cat specializes mammal in having claws (a characteristic) and purring (a behavior).
Similarly, when you say that ListBox
and
Button
are
Control
s you indicate that there are
characteristics and behaviors of Control
s that you
expect to find in both of these types. In other words,
Control
generalizes the shared characteristics of
both ListBox
and Button
, while
each specializes its own particular characteristics and behaviors.
It is common to note that two classes share functionality, and then to factor out these commonalities into a shared base class. This provides you with easier-to-maintain code and greater reuse of common code. For example, suppose you started out creating a series of objects as illustrated in Figure 5-2.
After working with RadioButton
s,
CheckBox
es, and Command
buttons
for a while, you realize that they share certain characteristics and
behaviors that are more specialized than Control
but more general than any of the three. You might factor these common
traits and behaviors into a common base class,
Button
, and rearrange your inheritance hierarchy
as shown in Figure 5-3. This is an example of how
generalization is used in object-oriented development.
This UML diagram depicts the relationship between the factored
classes and shows that both ListBox
and
Button
derive from Control
, and
that Button
is in turn specialized into
CheckBox
and Command
. Finally,
RadioButton
derives from
CheckBox
. You can thus say that
RadioButton
is a CheckBox
,
which in turn is a Button
, and that
Button
s are Controls
.
This is not the only, or even necessarily the best, organization for these objects, but it is a reasonable starting point for understanding how these types (classes) relate to one another.
Actually, although this might reflect how some widget hierarchies are
organized, I’m very skeptical of any system in which
the model doesn’t reflect how I perceive reality.
When I find myself saying that a RadioButton
is a
CheckBox
, I have to think long and hard about
whether that makes sense. I suppose a RadioButton
is a kind of checkbox. It is a checkbox that
supports the idiom of mutually exclusive choices. That said, it is a
bit of a stretch and might be a sign of a shaky design.
In C#, the specialization relationship is typically implemented using inheritance. This is not the only way to implement specialization, but it is the most common and most natural way to implement this relationship.
Saying that ListBox
inherits from (or derives
from) Control
indicates that it specializes
Control
. Control
is referred to
as the base
class, and ListBox
is
referred to as the
derived
class. That is,
ListBox
derives its characteristics and behaviors
from Control
and then specializes to its own
particular needs.
In C#, you create a derived class by adding a colon after the name of the derived class, followed by the name of the base class:
public class ListBox : Control
This code declares a new class, ListBox
, that
derives from Control
. You can read the colon as
“derives from.”
The derived class inherits all the members of the base class, both member variables and methods.
There
are two powerful aspects to inheritance. One is
code reuse. When
you create a ListBox
class,
you’re able to reuse some of the logic in the base
(Control
) class.
What is arguably more powerful, however, is the second aspect of inheritance: polymorphism. Poly means many and morph means form. Thus, polymorphism refers to being able to use many forms of a type without regard to the details.
When the phone company sends your phone a ring signal, it doesn’t know what type of phone is on the other end of the line. You might have an old-fashioned Western Electric phone that energizes a motor to ring a bell, or you might have an electronic phone that plays digital music.
As far as the phone company is concerned, it knows only about the
“base type”
Phone
and expects that any
“instance” of this type knows how
to ring. When the phone company tells your phone to
ring, it simply expects the phone to
“do the right thing.” Thus, the
phone company treats your phone polymorphically.
Because a
ListBox
is-a
Control
and a Button
is-a
Control
, we expect to be
able to use either of these types in situations that call for a
Control
. For example, a form might want to keep a
collection of all the instances of Control
it
manages so that when the form is opened, it can tell each of its
Controls
to draw itself. For this operation, the
form doesn’t want to know which elements are
listboxes and which are buttons; it just wants to tick through its
collection and tell each to “draw.”
In short, the form wants to treat all its Control
objects polymorphically.
To create a method that supports
polymorphism, you need only mark it as virtual
in
its base class. For example, to indicate that the method
DrawWindow( )
of class
Control
in Example 5-1 is
polymorphic, simply add the keyword
virtual
to its declaration as follows:
public virtual void DrawWindow()
Now each derived class is free to implement its
own version of DrawWindow( )
. To do so, simply
override the base class virtual method by using
the keyword override
in the derived class method
definition, and then add the new code for that overridden method.
In the following excerpt from Example 5-1 (which
appears later in this section), ListBox
derives
from Control
and implements its own version of
DrawWindow( )
:
publicoverride void DrawWindow()
{
base.DrawWindow(); // invoke the base method
Console.WriteLine ("Writing string to the listbox: {0}",
listBoxContents);
}
The keyword
override
tells the
compiler that this class has intentionally overridden how
DrawWindow( )
works. Similarly,
you’ll override this method in another class,
Button
, also derived from
Control
.
In the body of Example 5-1, you’ll
first create three objects: a Control
, a
ListBox
, and a Button
.
You’ll then call DrawWindow( )
on
each:
Control win = new Control(1,2); ListBox lb = new ListBox(3,4,"Stand alone list box"); Button b = new Button(5,6); win.DrawWindow( ); lb.DrawWindow( ); b.DrawWindow( );
This works much as you might expect. The correct
DrawWindow( )
object is called for each. So far,
nothing polymorphic has been done. The real magic starts when you
create an
array of
Control
objects. Because a
ListBox
is-a
Control
, you are free to place a
ListBox
into a Control
array.
You can also place a Button
into an array of
Control
objects because a
Button
is also a Control
:
Control[] winArray = new Control[3]; winArray[0] = new Control(1,2); winArray[1] = new ListBox(3,4,"List box in array"); winArray[2] = new Button(5,6);
What happens when you call
DrawWindow( )
on each object?
for (int i = 0;i < 3; i++) { winArray[i].DrawWindow(); }
All the compiler knows is that it has three
Control
objects and that you’ve
called DrawWindow()
on each. If you had not marked
DrawWindow
as virtual
,
Control
’s
DrawWindow( )
method would be called three times.
However, because you did mark DrawWindow()
as
virtual
, and because the derived classes override
that method, when you call DrawWindow()
on the
array, the compiler determines the runtime type of the actual objects
(a Control
, a ListBox
, and a
Button
) and calls the right method on each. This
is the essence of polymorphism. The complete code for this example is
shown in Example 5-1.
This listing uses an array, which is a collection of objects of the same type. Access the members of the array with the index operator:
// set the value of the element // at offset 5 MyArray[5] = 7;
The first element in any array is at index 0. The use of the array in this example should be fairly intuitive. Arrays are explained in detail in Chapter 9.
Example 5-1. Using virtual methods
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace VirtualMethods { public class Control { // these members are protected and thus visible // to derived class methods. We'll examine this // later in the chapter protected int top; protected int left; // constructor takes two integers to // fix location on the console public Control( int top, int left ) { this.top = top; this.left = left; } // simulates drawing the window public virtual void DrawWindow( ) { Console.WriteLine( "Control: drawing Control at {0}, {1}", top, left ); } } // ListBox derives from Control public class ListBox : Control { private string listBoxContents; // new member variable // constructor adds a parameter public ListBox( int top, int left, string contents ): base(top, left) // call base constructor { listBoxContents = contents; } // an overridden version (note keyword) because in the // derived method we change the behavior public override void DrawWindow( ) { base.DrawWindow( ); // invoke the base method Console.WriteLine( "Writing string to the listbox: {0}", listBoxContents ); } } public class Button : Control { public Button( int top, int left ): base(top, left) { } // an overridden version (note keyword) because in the // derived method we change the behavior public override void DrawWindow( ) { Console.WriteLine( "Drawing a button at {0}, {1} ", top, left ); } } public class Tester { static void Main( ) { Control win = new Control( 1, 2 ); ListBox lb = new ListBox( 3, 4, "Stand alone list box" ); Button b = new Button( 5, 6 ); win.DrawWindow( ); lb.DrawWindow( ); b.DrawWindow( ); Control[] winArray = new Control[3]; winArray[0] = new Control( 1, 2 ); winArray[1] = new ListBox( 3, 4, "List box in array" ); winArray[2] = new Button( 5, 6 ); for ( int i = 0; i < 3; i++ ) { winArray[i].DrawWindow( ); } } } } Output: Control: drawing Control at 1, 2 Control: drawing Control at 3, 4 Writing string to the listbox: Stand alone list box Drawing a button at 5, 6 Control: drawing Control at 1, 2 Control: drawing Control at 3, 4 Writing string to the listbox: List box in array Drawing a button at 5, 6
Note that throughout this
example we’ve marked
the new overridden methods with the keyword
override
:
public override void DrawWindow()
The compiler now knows to use the overridden method
when treating these objects polymorphically. The compiler is
responsible for tracking the real type of the object and for handling
the "late binding” so that
it is ListBox.DrawWindow( )
that is called when the
Control
reference really points to a
ListBox
object.
In Example 5-1, the
new class ListBox
derives from
Control
and has its own constructor, which takes
three parameters. The ListBox
constructor invokes
the constructor of its parent (Control
) by placing
a colon (:) after the parameter list and then
invoking the base class with the keyword base
:
public ListBox(
int theTop,
int theLeft,
string theContents):base(theTop, theLeft) // call base constructor
Because classes can’t inherit constructors, a derived class must implement its own constructor and can only make use of the constructor of its base class by calling it explicitly.
If the base class has an accessible default constructor, the derived
constructor is not required to invoke the base constructor
explicitly; instead, the default constructor is called implicitly.
However, if the base class doesn’t have a default
constructor, every derived constructor must
explicitly invoke one of the base class constructors using the
base
keyword.
As discussed in Chapter 4, if you don’t declare a constructor of any kind, the compiler will create a default constructor for you. Whether you write it yourself or you use the one provided “by default” by the compiler, a default constructor is one that takes no parameters. Note, however, that once you do create a constructor of any kind (with or without parameters), the compiler doesn’t create a default constructor for you.
The
visibility of
a class and its members can be restricted through the use of access
modifiers, such as public
,
private
, protected
,
internal
, and protected
internal
. (See Chapter 4 for
a discussion of access modifiers.)
As you’ve seen,
public
allows a
member to be accessed by the member methods of other classes, while
private
indicates
that the member is visible only to member methods of its own class.
The protected
keyword extends visibility to
methods of derived classes, while
internal
extends
visibility to methods of any class in the same
assembly.[1]
The
internal
protected
keyword pair allows access to members of
the same assembly (internal) or derived classes
(protected). You can think of this designation as
internal
or
protected
.
Classes as well as their members can be designated with any of these
accessibility levels. If a class member has an access designation
that is different from that of the class, the more restricted access
applies. Thus, if you define a class, myClass
, as
follows:
public class myClass { // ... protected int myValue; }
the accessibility for myValue
is protected even
though the class itself is public. A public
class
is one that is visible to any other class that wishes to interact
with it. Often, classes are created that exist only to help other
classes in an
assembly, and
these classes might be marked internal
rather than
public
.
In C#, the
programmer’s
decision to override a
virtual
method is made explicit with the override
keyword.
This helps you release new versions of your code; changes to the base
class will not break existing code in the derived classes. The
requirement to use the keyword override
helps
prevent that problem.
Here’s how: assume for a moment that the
Control
base class of the previous example was
written by Company A. Suppose also that the
ListBox
and RadioButton
classes
were written by programmers from Company B using a purchased copy of
the Company A Control
class as a base. The
programmers in Company B have little or no control over the design of
the Control
class, including future changes that
Company A might choose to make.
Now suppose that one of the programmers for Company B decides to add
a Sort()
method to ListBox
:
public class ListBox : Control { public virtual void Sort( ) {...} }
This presents no problems until Company A, the author of
Control
, releases Version 2 of its
Control
class, and it turns out that the
programmers in Company A have also added a Sort( )
method to their public class Control
:
public class Control { // ... public virtual void Sort( ) {...} }
In other object-oriented languages (such as C++), the new virtual
Sort( )
method in Control
would
now act as a base method for the virtual Sort()
method in ListBox
. The compiler would call the
Sort()
method in ListBox
when
you intend to call the Sort( )
in
Control
. In Java, if the Sort()
in Control
has a different return type, the class
loader would consider the Sort()
in
ListBox
to be an invalid override and would fail
to load.
C# prevents this confusion. In C#, a virtual function is always
considered to be the root of virtual dispatch; that is, once C# finds
a virtual method, it looks no further up the inheritance hierarchy.
If a new virtual Sort( )
function is introduced
into Control
, the runtime behavior of
ListBox
is unchanged.
When ListBox
is compiled again, however, the
compiler generates a warning:
...class1.cs(54,24): warning CS0114: 'ListBox.Sort( )' hides inherited member 'Control.Sort( )'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
To remove the warning, the programmer must indicate what he intends.
He can mark the ListBox
Sort( )
method new
, to indicate that it is
not an override of the virtual method in
Control
:
public class ListBox : Control { public new virtual void Sort() {...}
This action removes the warning. If, on the other hand, the
programmer does want to override the method in
Control
, he need only use the
override
keyword to make that intention explicit:
public class ListBox : Control { public override void Sort() {...}
To avoid this warning, it might be tempting to add the keyword
new
to all your virtual methods. This is a bad
idea. When new
appears in the code, it ought to
document the versioning of code. It points a potential client to the
base class to see what you aren’t overriding. Using
new
scattershot undermines this documentation.
Further, the warning exists to help identify a real issue.
Every subclass of
Control
should implement its
own DrawWindow()
method—but nothing requires
that it do so. To require subclasses to implement a method of their
base, you need to designate that method as
abstract.
An abstract method has no implementation. It creates a method name and signature that must be implemented in all derived classes. Furthermore, making one or more methods of any class abstract has the side effect of making the class abstract.
Abstract classes establish a base for derived classes, but it is not legal to instantiate an object of an abstract class. Once you declare a method to be abstract, you prohibit the creation of any instances of that class.
Thus, if you were to designate DrawWindow()
as
abstract
in the Control
class,
you could derive from Control
, but you could not
create any Control
objects. Each derived class
would have to implement DrawWindow()
. If the
derived class failed to implement the abstract method, that class
would also be abstract, and again no instances would be possible.
Designating a method as abstract
is accomplished
by placing the keyword abstract
at the beginning
of the method definition, as follows:
abstract public void DrawWindow();
(Because the method can have no implementation, there are no braces; only a semicolon.)
If one or more methods are abstract, the class definition must also
be marked abstract
, as in the following:
abstract public class Control
Example 5-2 illustrates the creation of an
abstract
Control
class and an abstract
DrawWindow( )
method.
Example 5-2. Using an abstract method and class
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace abstractmethods { using System; abstract public class Control { protected int top; protected int left; // constructor takes two integers to // fix location on the console publicControl( int top, int left ) { this.top = top; this.left = left; } // simulates drawing the window // notice: no implementation abstract public void DrawWindow( ); } // ListBox derives from Control public class ListBox : Control { private string listBoxContents; // new member variable // constructor adds a parameter public ListBox( int top, int left, string contents ): base(top, left) // call base constructor { listBoxContents = contents; } // an overridden version implementing the // abstract method public override void DrawWindow( ) { Console.WriteLine( "Writing string to the listbox: {0}", listBoxContents ); } } public class Button : Control { public Button( int top, int left ): base(top, left) { } // implement the abstract method public override void DrawWindow( ) { Console.WriteLine( "Drawing a button at {0}, {1} ", top, left ); } } public class Tester { static void Main( ) { Control[] winArray = new Control[3]; winArray[0] = new ListBox( 1, 2, "First List Box" ); winArray[1] = new ListBox( 3, 4, "Second List Box" ); winArray[2] = new Button( 5, 6 ); for ( int i = 0; i < 3; i++ ) { winArray[i].DrawWindow( ); } } } }
In Example 5-2, the Control
class
has been declared abstract and therefore can’t be
instantiated. If you replace the first array member:
winArray[0] = new ListBox(1,2,"First List Box");
with this code:
winArray[0] = new Control(1,2);
the program generates the following error:
Cannot create an instance of the abstract class or interface 'abstractmethods.Control'
You can instantiate the ListBox
and
Button
objects because these classes override the
abstract method, thus making the classes
concrete (i.e., not abstract).
Although designating
DrawWindow( )
as abstract does force all the
derived classes to implement the method, this is a very limited
solution to the problem. If we derive a class from
ListBox
(e.g.,
DropDownListBox
), nothing forces that derived
class to implement its own DrawWindow( )
method.
C++ programmers take note: in C#, it is not possible
for Control.DrawWindow( )
to provide an
implementation, so we can’t take advantage of the
common DrawWindow( )
routines that might otherwise
be shared by the derived classes.
Finally, abstract classes should not just be an implementation trick; they should represent the idea of an abstraction that establishes a “contract” for all derived classes. In other words, abstract classes describe the public methods of the classes that will implement the abstraction.
The idea of an abstract Control
class ought to lay
out the common characteristics and behaviors of all
Controls
, even if we never intend to instantiate
the abstraction Control
itself.
The idea of an abstract class is implied in the word
“abstract.” It serves to implement
the abstraction “control” that will
be manifest in the various concrete instances of
Control
, such as browser window, frame, button,
listbox, or drop-down menu. The abstract class establishes what a
Control
is, even though we never intend to create
a control per se. An alternative to using
abstract
is to define an interface, as described
in Chapter 8.
The obverse side of the design
coin from abstract is
sealed.
Although an abstract class is intended to be derived from and to
provide a template for its subclasses to follow, a sealed class
doesn’t allow classes to derive from it at all.
Placed before the class declaration, the sealed
keyword precludes derivation. Classes are most often marked
sealed
to prevent accidental inheritance.
If the declaration of Control
in Example 5-2 is changed from abstract
to
sealed
(eliminating the
abstract
keyword from the
DrawWindow( )
declaration as well), the program
will fail to compile. If you try to build this project, the compiler
will return the following error message:
'ListBox' cannot inherit from sealed class 'Control'
among many other complaints (such as that you can’t create a new protected member in a sealed class).
All C# classes, of any
type, are treated as if they ultimately derive from
System.Object
. Interestingly, this includes value
types.
A base class is the
immediate “parent” of a derived
class. A derived class can be the base to further
derived classes, creating an inheritance
“tree” or hierarchy. A
root class is the
topmost class in an inheritance hierarchy. In C#, the root class is
Object
. The nomenclature is a bit confusing until
you imagine an upside-down tree, with the root on top and the derived
classes below. Thus, the base class is considered to be
“above” the derived class.
C++
programmers take note: C# uses single inheritance with a
monolithic class hierarchy: every class inherits from a base class of
Object
, and multiple inheritance is not possible.
However, C# interfaces provide many of the benefits of multiple
inheritance. (See Chapter 8 for more
information.)
Object
provides a number of
virtual methods that subclasses can and
do override. These include Equals( )
to determine
if two objects are the same; GetType()
, which
returns the type of the object (discussed in Chapter 8); and ToString( )
, which
returns a string to represent the current object (discussed in Chapter 10). Table 5-1 summarizes
the methods of Object
.
Table 5-1. The methods of Object
Method |
What it does |
---|---|
Equals( ) |
Evaluates whether two objects are equivalent. |
GetHashCode( ) |
Allows objects to provide their own hash function for use in collections (see Chapter 9). |
GetType( ) |
Provides access to the type object (see Chapter 18). |
ToString( ) |
Provides a string representation of the object. |
Finalize( ) |
Cleans up nonmemory resources; implemented by a destructor (see Chapter 4). |
MemberwiseClone ( ) |
Creates copies of the object; should never be implemented by your type. |
ReferenceEquals( ) |
Evaluates whether two objects refer to the same instance. |
Example 5-3 illustrates the
use of the
ToString( )
method inherited from
Object
, as well as the fact that primitive
datatypes such as int
can be treated as if they
inherit from
Object
. Note that the
DisplayValue
method expects an object, but works
perfectly fine if you pass in an integer.
Example 5-3. Inheriting from Object
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace InheritingFromObject { public classSomeClass { private int val; public SomeClass( int someVal ) { val = someVal; } public override string ToString( ) { return val.ToString( ); } } public class Tester { static void DisplayValue( object o ) { Console.WriteLine( "The value of the object passed in is {0}", o.ToString( ) ); } static void Main( ) { int i = 5; Console.WriteLine( "The value of i is: {0}", i.ToString( ) ); DisplayValue( i ); SomeClass s = new SomeClass( 7 ); Console.WriteLine( "The value of s is {0}", s.ToString( ) ); DisplayValue( s ); } } } Output: The value of i is: 5 The value of the object passed in is 5 The value of s is 7 The value of the object passed in is 7
The documentation for Object.ToString( )
reveals
its signature:
public virtual string ToString();
It is a public virtual method that returns a string and that takes no
parameters. All the built-in types, such as int
,
derive from Object
and so can invoke
Object
’s methods.
Example 5-3
overrides the virtual function for
SomeClass
, which is the usual case, so that the
class’ ToString()
method will
return a meaningful value. If you comment out the overridden
function, the base method will be invoked, which will change the
output to:
The value of s is SomeClass
Thus, the default behavior is to return a string with the name of the class itself.
Classes don’t need to explicitly declare that they
derive from Object
; the inheritance is
implicit.
Boxing and
unboxing are the processes that enable
value types (e.g., integers) to be
treated as reference types (objects). The value is
“boxed” inside an
Object
, and subsequently
“unboxed” back to a value type.
Java
programmers take note: in Java, wrapping basic types in
objects requires the explicit use of wrapper types like
Integer
and Float
. In C#, the
boxing mechanism takes care of all of this for you automatically;
wrapper types are unnecessary.
Boxing is an implicit
conversion of a value type to the type Object
.
Boxing a value allocates an instance of the
boxed
type
and copies the value
into the new object instance, as shown in Figure 5-4.
Boxing is implicit when you provide a value type where a reference is
expected. For example, if you assign a primitive type, such as an
integer to a variable of type Object
(which is
legal because int
derives from
Object
), the value is boxed, as shown here:
using System; class Boxing { public static void Main( ) { int i = 123; Console.WriteLine("The object value = {0}", i); } }
Console.WriteLine( )
expects an object, not an
integer. To accommodate the method, the integer type is automatically
boxed by the CLR, and ToString( )
is called on the
resulting object. This feature allows you to create methods that take
an object as a parameter; no matter what is passed in (reference or
value type), the method will work.
To return the boxed object back to a value type, you must explicitly unbox it. You should accomplish this in two steps:
Make sure the object instance is a boxed value of the given value type.
Copy the value from the instance to the value-type variable.
Figure 5-5 illustrates.
For the unboxing to succeed, the object being unboxed must be of the appropriate type for the variable you are assigning it to. Boxing and unboxing are illustrated in Example 5-4.
Example 5-4. Boxing and unboxing
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace boxing { public classUnboxingTest { public static void Main( ) { int i = 123; //Boxing object o = i; // unboxing (must be explicit) int j = ( int ) o; Console.WriteLine( "j: {0}", j ); } } }
Example 5-4 creates an integer i
and implicitly boxes it when it is assigned to the object
o
. The value is then explicitly unboxed and
assigned to a new int
whose value is displayed.
Typically, you will wrap an unbox operation in a try block, as
explained in Chapter 11. If the object being
unboxed is null or a reference to an object of a different type, an
InvalidCastException
is thrown.
Classes
have members, and it is entirely possible for the member of a class
to be another user-defined type. Thus, a Button
class might have a member of type Location
, and a
Location
class might contain members of type
Point
. Finally, Point
might
contain members of type int
.
At times, the contained class might exist only to serve the outer class, and there might be no reason for it to be otherwise visible. (In short, the contained class acts as a helper class.) You can define the helper class within the definition of the outer class. The contained, inner class is called a nested class, and the class that contains it is called, simply, the outer class.
Nested classes have the advantage of access to all the members of the outer class. A method of a nested class can access private members of the outer class.
In addition, the nested class can be hidden from all other classes—that is, it can be private to the outer class.
Finally, a nested class that is public is accessed within the scope
of the outer class. If Outer
is the outer class,
and Nested
is the (public) inner class, refer to
Nested
as Outer.Nested
, with
the outer class acting (more or less) as a namespace or scope.
Java programmers take note: nested classes are roughly equivalent to static inner classes; there is no C# equivalent to Java’s nonstatic inner classes.
Example 5-5 features a nested class of
Fraction
named FractionArtist
.
The job of FractionArtist
is to render the
fraction on the console. In this example, the rendering is handled by
a pair of simple WriteLine( )
statements.
Example 5-5. Using a nested class
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace NestedClasses { public class Fraction { private int numerator; private int denominator; public Fraction( int numerator, int denominator ) { this.numerator = numerator; this.denominator = denominator; } public override string ToString( ) { return String.Format( "{0}/{1}", numerator, denominator ); } internal class FractionArtist { public void Draw( Fraction f ) { Console.WriteLine( "Drawing the numerator: {0}", f.numerator ); Console.WriteLine( "Drawing the denominator: {0}", f.denominator ); } } } public class Tester { static void Main( ) { Fraction f1 = new Fraction( 3, 4 ); Console.WriteLine( "f1: {0}", f1.ToString( ) ); Fraction.FractionArtist fa = new Fraction.FractionArtist( ); fa.Draw( f1 ); } } }
The nested class is shown in bold. The
FractionArtist
class provides only a single
member, the Draw()
method. What is particularly
interesting is that Draw( )
has access to the
private data members f.numerator
and
f.denominator
, to which it
wouldn’t have had access if it
weren’t a nested class.
Notice in Main()
that to declare an instance of
this nested class, you must specify the type name of the outer class:
Fraction.FractionArtist fa = new Fraction.FractionArtist( );
FractionArtist
is scoped to within the
Fraction
class.
[1] An assembly (discussed in Chapter 1) is the unit of sharing and reuse in the CLR (a logical DLL). Typically, an assembly is created from a collection of physical files, held in a single directory that includes all the resources (bitmaps, .gif files, etc.) required for an executable, along with the IL and metadata for that program.
18.118.20.231