3.12. Member Access: The new and base Modifiers

The inherited members of the base class appear as if they are members of the derived class. This is why we are able to directly refer to them without qualification. However, they are not members of the derived class. Rather they maintain their membership within the declaration space of their base class.

The derived-class declaration space is searched first for an occurrence of a name within the derived class. If the name is not found there, the declaration space of the base class is searched. This is why a derived class can reuse the name of a base-class member without penalty. The derived instance is entered into the derived-class declaration space. All unqualified uses of the name within the derived class resolve to the derived-class instance. The derived-class instance is said to hide the inherited base-class instance—for example,

abstract public class Query
{
      public void func1( int i ) { ... }
      public virtual void func2( int ival )
             { func1( ival ); }

      // ...
}

public class OrQuery : Query
{
      // necessary because this definition
      // hides the inherited base-class member
      new public void func1( int i ) { ... }

      public override void func2( int ival )
      {
            // OK: we want the hidden base-class member
            //     invoked here
            base.func1( ival );

            // unqualified reference invokes the
            // OrQuery instance ...
            func1( ival );
      }

      // ...
}

Hopefully what is going on here is clear. The two func1() instances defined in both the base and the derived classes are nonvirtual. They have the same signature, so the derived-class instance hides that of the base class. To indicate to both the compiler and the readers of our program that the renaming is intentional, we modify the derived-class instance with the new keyword. In this context, the new keyword identifies the member definition as hiding the unqualified access of an inherited member with the same name. If we don't specify new, we get a warning because the compiler is not sure we are aware of the hidden base-class member.

To access the base-class member within the derived class, we must qualify its name with the base keyword:

base.func1( ival );

This statement invokes the func1() method defined as a member of the base class. In this case it invokes the func1() class member defined within Query. If Query has not defined a func1() member, the statement is flagged as an error.

Outside the derived class, the base keyword cannot be used. There is no syntax to support accessing a hidden base-class member through a derived-class object. If absolutely necessary, we can resort to an explicit cast:

static public void Main()
{
    OrQuery or = new OrQuery();

    or.func1( 1024 );         // OrQuery.func1( int )
    ((Query)or).func1(1024);  // Query.func1( int )

    // ...
}

Because func1() is not a virtual function, an invocation of func1() through a Query object always resolves to the member defined within Query, even if the Query object actually addresses an object of type OrQuery, as in the following example:

Query q = new OrQuery();

q.func1( 1024 ); // nonvirtual: Query.func1()
q.func2( 1024 ); // virtual: OrQuery.func2();

In general, you should be suspicious of a class hierarchy design in which nonvirtual methods reuse the same name. The potential for users to become confused as to which instance is being invoked is quite real.

A virtual function that provides a definition of an abstract method and the derived-class override method does not need to specify the new keyword because there is no hiding in such instances. The virtual mechanism invokes the appropriate method transparently according to the type of the runtime object through which the function is invoked.

3.12.1. Accessibility versus Visibility

Consider the following class hierarchy, with a member s defined in both the base and derived classes:

class aBase { public string s; }
class aDerived : aBase { new private string s; }

Within the base class, all references to s refer to its public string member. Similarly, within the derived class all unqualified references to s refer to its private string member. Within the derived class, the base-class member can be referenced through the base keyword:

base.s; // refers to aBase.s

The new keyword alerts the compiler (and readers of the program) that the designer of the derived class intended to hide the base-class member s within the declaration space of the derived class.

Now let's introduce another level of derivation:

class aMostDerived : aDerived
{
    // OK: which member 's' is assigned?
    public void foo( string str ) { s = str; }
}

Our new class inherits both member instances of s. Which of the two instances does an unqualified use of s within the aMostDerived class resolve to—that of aBase or of aDerived?

In C++ the member being referred to is resolved before its accessibility is considered. First the immediate scope of the method is examined, then the enclosing scope of the method's class, then the enclosing base-class scope. In C++ the aDerived class instance of s is selected. An error is subsequently generated because s is a private inherited member and is therefore not accessible. The intention is not to change the meaning of a program when the access level of a member is changed.

In C#, the member being referred to is resolved only after the accessible candidates are considered. That is, although the aDerived instance of s is the immediate derived-class instance, because it is private and therefore inaccessible, it is not considered an accessible candidate for resolving the reference. Rather the aBase instance is selected. If the class designer were later to redeclare the aDerived instance as protected, it would be selected instead.

3.12.2. Encapsulating Base-Class Access

Consider the following simplified Point/Point3D class hierarchy. The coordinate members span the base and derived classes:

class Point
{
    protected float x_, y_;
    public Point( float x, float y ){ x_ = x; y_ = y; }

    public virtual void display()
         { Console.Write( "{0}, {1}", x_, y_ ); }

      // ...
}

class Point3D : Point
{
    protected float z_;
    public Point3D( float x, float y, float z )...
    public override void display(){ ... }
}

First, how should we implement the Point3D constructor? Basically, we have two choices:

  1. Pass the x and y coordinates to the Point constructor using the base keyword:

    public Point3D( float x, float y, float z )
           : base( x, y ) { z_ = z; }
    
  2. Directly initialize the three members within the derived-class constructor:

    public Point3D( float x, float y, float z )
           { x_ = x; y_ = y; z_ = z; }
    

The members are correctly initialized under both implementations. However, we should always prefer the first solution, in which the base-class constructor is given the responsibility of initializing the members of its class.

The primary reason is to maintain a loose coupling between the base class and the classes that inherit from it. This way, if the implementation of the base class were to change, the derived classes would not mysteriously then fail to compile. For example, point of contention among graphics programmers is whether to store coordinate members individually or as an array. I've actually seen implementations flip-flop between the two approaches as different programmers assumed responsibility to maintain the classes.

How, then, should we implement the Point3D instance of display()? The choices are pretty much the same: (1) directly print out all three members, or (2) invoke the base-class display() function to handle the base-class members and print out only the z coordinate member in the derived-class instance.

The preferred solution is to localize the responsibility to the class whose members are being displayed. Our derived instance should be concerned with only the display of its member(s):

class Point3D : Point
{
   public override void display()
   {
         base.display();
         Console.Write( ", {0}", z_ );
   }
   // ...
}

The use of the base keyword is necessary to invoke the Point instance of display(). An unqualified invocation of display() resolves to the Point3D instance, which results in an infinite recursion. Because the base keyword specifies which instance of display() to invoke, the invocation is performed at compile time rather than through the virtual mechanism.

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

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