Chapter 5. Inheritance and Polymorphism

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.

Tip

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.

Specialization and Generalization

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 Controls you indicate that there are characteristics and behaviors of Controls 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.

An is-a relationship

Figure 5-1. An is-a relationship

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.

Deriving from Control

Figure 5-2. Deriving from Control

After working with RadioButtons, CheckBoxes, 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.

A more factored hierarchy

Figure 5-3. A more factored hierarchy

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 Buttons 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.

Tip

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.

Inheritance

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.

Implementing Inheritance

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.”

Tip

C++ programmers take note: C# has no private or protected inheritance.

The derived class inherits all the members of the base class, both member variables and methods.

Polymorphism

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.

Creating Polymorphic Types

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.

Creating Polymorphic Methods

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.

Tip

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;

Note

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.

Tip

C++ programmers take note: you must explicitly mark the declaration of any method that overrides a virtual method with the keyword override.

Calling Base Class Constructors

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.

Tip

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.

Controlling Access

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.

Versioning with the new and override Keywords

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() {...}

Warning

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.

Abstract Classes

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).

Limitations of 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.

Tip

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.

Sealed Class

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.

Tip

Java programmers take note: a sealed class in C# is the equivalent of a final class in Java.

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).

The Root of All Classes: Object

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.

Tip

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 Types

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.

Tip

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 Implicit

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 reference types

Figure 5-4. Boxing reference types

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.

Unboxing Must Be Explicit

To return the boxed object back to a value type, you must explicitly unbox it. You should accomplish this in two steps:

  1. Make sure the object instance is a boxed value of the given value type.

  2. Copy the value from the instance to the value-type variable.

Figure 5-5 illustrates.

Boxing and then unboxing

Figure 5-5. Boxing and then unboxing

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.

Nesting Classes

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.

Tip

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.

Tip

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.20.231