3.4. Object Lessons

In the preceding section we solved a perplexing storage and transmittal problem by introducing the abstract Query base class, from which the other query types inherit. For example, when declared as a Query object, the NotQuery operand is able to address all the derived query types—not only those we know about, but also any that are introduced in the future. This solves our transmittal problem as well. Each function parameter, if declared a Query type, can now correctly pass an object of any derived query type.

In this section we look at a similar problem—except that this one relates not to independent classes, but to independent class hierarchies. So far in this chapter we have defined two class hierarchies: the LibMat and the Query inheritance hierarchies. In Chapter 1 we saw portions of the Exception class hierarchy provided within the System namespace. Each class hierarchy represents an independent family of types.

Let's imagine that we need to write a function that can accept as a parameter any type defined within one of those three independent class hierarchies. One solution, of course, is to define a set of three overloaded functions:

class TheClassOfAllTypes
{
      public string ToString( LibMat m ){...}
      public string ToString( Query q ) {...}
      public string ToString( Exception e ){...}

      // ...
}

This set of three overloaded methods can accept every current and future class defined within the LibMat, Query, and Exception class hierarchies. The problem arises when a new class hierarchy or independent class is introduced that also wishes to invoke our ToString() method. Oops. Every time we have a new class or class hierarchy to support, we must go back to add a new function to our class. There has to be a better solution.

This is also a transport and storage problem, but one that cuts across independent type hierarchies such as Query. The various Query subtypes all represent a kind of specialized query. The shared commonality is the general set of operations of a query object.

There is no shared set of behaviorial operations upon which to define a relationship between the LibMat and Query class hierarchies, except that they both represent class types within the program's type system. The shared commonality is the general set of operations that we as programmers apply to the types in a runtime system.

The solution to this transport and storage problem is the same as our earlier solution: We introduce an abstract base class from which all the types within our program are derived. What should we call this base class, and what public operations, if any, should it provide?

I called it TheClassOfAllTypes because it represents what is common among all program types. Under .NET, it is called Object. Object is a class defined within the System namespace. It serves as the root of the type hierarchy under which all other types, such as Exception, string, int, LibMat, Query, and so on, are derived. The predefined object type in C# is an alias for this class.

Because all types are either directly or indirectly derived from object, an entity of type object can be initialized or assigned to an object of any other type—even literal values such as integer constants and strings. This solves our ToString() design dilemma: How can we define one function that can accept objects of all current and future types? By declaring it to take an object parameter:

class TheClassOfAllTypes
{

      static public string ToString( object o ){...}
      // ... what else?
}

An interesting question is, What member functions should the base class of all other classes provide? As you might guess, one operation the Object class provides is ToString(). A second public member function is Equals(). A third is GetType(). Each of these operations is inherited by every type in the system. We can invoke ToString(), Equals(), or GetType() on any object or literal value. An interesting question, of course, is what do these functions actually do?

One of the hard things about writing a function for all types is that it ends up doing very little of specific use to any particular type. This is especially true of both the ToString() and Equals() member functions. For example, how can we arbitrarily implement an equals operation that is meaningful for every possible type? The equality of two integers, for example, requires a different algorithm from that required by the equality of two JulianCalendar objects.

The solution is twofold. First, the default implementations within the Object class provide a least-common-denominator solution. For Equals(), the default implementation, called reference equality, returns true only if the two objects being compared are the same object. For ToString(), the default implementation prints out the type's fully qualified name. This is the functionality that each new class we define inherits.

The second aspect of the solution is to declare these two member functions as virtual functions. Doing so allows each derived class to optionally override the default implementation with an instance that provides more meaningful information about the data associated with the class object. For example, the arithmetic classes override ToString() to print out the value held by the object, and Equals() to implement value equality. As its name implies, value equality returns true if the two objects hold the same value.

A third function provided by the Object class is the nonvirtual GetType(). This function returns a Type object that encapsulates all the information about the actual type to which the object refers, such as a description of each of its properties, methods, constructors, and so on. It also contains a set of predicate properties—IsClass, IsArray, IsPublic, and so on—that answer questions about the type. It is nonvirtual because it provides a non-type-dependent operation that the derived class is not intended to override. The Type class serves as the gateway to runtime type reflection, the topic of Section 8.2.

Whenever we define a class without an explicit base class, that class implicitly inherits from the Object class. For example, let's look again at our Query class hierarchy. Here is how we might declare it:

class Query { ... }
class NameQuery : Query { ... }
class NotQuery  : Query { ... }
class OrQuery   : Query { ... }
class AndQuery  : Query { ... }

From this declaration it appears that Query is an independent class. Internally, however, its declaration is augmented to include Object as its base class:

// internally augmented to derive from System.Object
class Query : System.Object { ... }

Query serves as the immediate base class of each of the four derived query types. Object serves as an indirect base class of each of the derived query types as well. All five classes inherit the public methods defined within Object.

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

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