Team LiB
Previous Section Next Section

The Object Class

In addition to the String class, there are several other classes that deserve special mention in the C# language—one such class is the Object class. The Object class is at the very root of the .NET class hierarchy. Every other .NET type, from the simple predefined value types, to strings, to arrays, to predefined reference types, to user-defined types, ultimately derives from the Object class.

Inheritance from the Object class is implicit; there is no need to include the syntax

: Object

in a class definition; that is, the class definition syntax

public class Student { ... }

is equivalent to the more explicit

public class Student : Object { ... }

The Object class is contained in the System namespace, but as with the String class, there is an alias for the Object class—the keyword object (all lowercase)— which allows us to declare Object references without having to insert a using System; directive at the top of our program; the following two lines of code are thus equivalent:

System.Object x = y;

and

object x = y;

The Object class declares five public methods that are inherited by, and hence available to, any object of any type. We'll discuss the two most commonly used Object methods in detail in the following sections: Equals and ToString.

The Equals Method

The Equals method determines whether two object references are "equal": that is, whether the two references are referring to the exact same object in memory. There are two versions of this method:

  • public virtual bool Equals(object obj): This method can be called on any object, passing in a second object as an argument: if (x.Equals(y)) { }

  • public static bool Equals(object objA, object objB): This static method is called on the Object class, and the two references to be compared are both passed in as arguments to the method: if (Object.Equals(x, y)) { }

For example, let's create a Student object and maintain two handles on it:

Student s1 = new Student("Fred");
Student s2 = s1;

Reference variables s1 and s2 thus reference the same object in memory. Now, let's create a second Student object with the same data values as the first:

Student s3 = new Student("Fred");

Using the "object as helium balloon" analogy, we've created the situation portrayed in Figure 13-11.

Click To expand
Figure 13-11: Object #2 has the same data values as Object #1 but is a separate, distinct object in memory.

If we test for equality of s1 and s2 using the Equals method:

if (s1.Equals(s2)) {  // or, equivalently:   Object.Equals(s1, s2);
  Console.WriteLine("s1 equals s2");
}
else {
  Console.WriteLine("s1 does not equal s2");
}

the output of this code would be as follows:

  s1 equals s2

because s1 and s2 are indeed referencing the same object, whereas testing s1 and s3 for equality:

if (s1.Equals(s3)) {  // or, equivalently:   Object.Equals(s1, s3);
  Console.WriteLine("s1 equals s3");
}
else {
  Console.WriteLine("s1 does not equal s3");
}

would generate the following output:

  s1 does not equal s3

because despite the fact that s1 and s3 have the same data values (name is "Fred" in both cases), they are nonetheless references to distinct objects.

Overriding the Equals Method

The nonstatic version of the Object class's Equals method is declared to be virtual, allowing us to override its behavior in a derived class. We often override the Equals method to define a different interpretation of what it means for two objects to be equal: for example, in the Student class, we may wish to deem two physically distinct Student objects as nonetheless being "equal" if they have the same value for their respective student ID numbers.

Because the code involved in doing so is a bit complex, we'll present it in its entirety first, and will then explain it step by step:


using System;

public class Student
{
  // Field.
  private string studentId;

  // Property.
  public string StudentId {
    // Accessor details omitted.
  }

  // Overriding the Equals method.
  public override bool Equals(object obj) {
    // Initialize a flag.
    bool isEqual = false;

    try {
      // Start by attempting to cast the generic object reference,
      // "obj", as a Student; if an InvalidCastException arises
      // at run time, we KNOW that the object in question is NOT
      // a Student, and so by definition cannot be equal to THIS
      // Student!
      Student s = (Student) obj;

      // If we make it this far in the Equals method without
      // throwing an InvalidCastException, we know that we are
      // indeed dealing with two Student instances. Next,
      // we'll compare their ID numbers.
      if (this.StudentID == s.StudentID) {
        // Eureka! They're equal, according to our new
        // definition of Student equality.
        isEqual = true;
      }
      else {
        isEqual = false;
      }
    }
    catch (InvalidCastException e) {
      // As mentioned above, if "obj" cannot be cast as a Student,
      // we know it doesn't equal THIS Student!
      isEqual = false;
    }
    return isEqual;
  }
}

Let's now discuss key aspects of this code.

First, note that the Object class's version of the Equals method:

public virtual bool Equals(object obj)

takes a generic object reference as an argument. In our overridden Equals method for the Student class, however, we're interested in establishing the equality/inequality of two Student objects, so we first attempt to cast the obj argument into a Student object reference.

      Student s = (Student) obj;

Two possible outcomes can arise:

  • If the object reference being passed in is indeed a Student reference, the cast proceeds without exception at run time, and we then proceed to compare the two students' ID numbers. If they are found to be identical, we set the isEqual flag to true to indicate that they are "equal" according to our business rules for Student equality; otherwise, we set the flag to false.

                if (this.StudentID == s.StudentID) {
                  isEqual = true;
                }
                else {
                  isEqual = false;
                }
    
  • On the other hand, if the object being passed in is not a Student, an InvalidCastException is thrown, transferring control to the catch block of our example. Here, we automatically set the isEqual flag to false: if the object being compared to this Student isn't even a Student, then they are obviously unequal!

              catch (InvalidCastException e) {
                // If obj cannot be cast to a Student, we know it doesn't equal
                // THIS Student!
                isEqual = false;
              }
    

Let's put our new overridden method to work in our client code:


      public class Example
      {
        static void Main() {
          Professor p = new Professor();

          Student s1 = new Student(); // first object ...
          s1.StudentID = "123-45-6789";

          Student s2 = new Student(); // second object ...
          s2.StudentID = "123-45-6789"; // same ID as s1

          Student s3 = new Student(); // third object!
          s3.StudentID = "987-65-4321"; // different ID as s1 and s2

          Console.WriteLine("Is s1 equal to s2? " + s1.Equals(s2));
          Console.WriteLine("Is s1 equal to s3? " + s1.Equals(s3));
          Console.WriteLine("Is s1 equal to p? " + s1.Equals(p));
        }
      }

The preceding code would generate the following output:

Is s1 equal to s2? true
Is s1 equal to s3? false
Is s1 equal to p? false
Note?/td>

Note that when we instead use the == operator to test two references for equality

      if (x == y) { ... }

the nature of the equality test is again class-dependent; if x and y are generic object references, then == tests to see if the two references are referring to the same physical object in memory; if x and y are string references, then as we learned earlier, == tests to see if the two strings have the same values, regardless of whether they are referring to the same or different physical string objects.

For a user-defined class such as Student that has overridden the Equals method, there is also a way to override the == operator, but the means of doing so is beyond the scope of this book to address.

The ToString Method

The most commonly used (and most commonly overridden) Object class method is ToString. It's used to return a string representation of the object on which the method is called, and has the following header:

public virtual string ToString()

The Object class implementation of ToString simply returns the fully qualified name of the type of the object on which it's called. For example, if the Student class belongs in the SRS namespace and we were to call the ToString method on a Student object as it's inherited from the Object class:

Student s = new Student();
s.Name = "Dianne Bolden";
s.StudentId = "999999";
Console.WriteLine(s.ToString());

the following output would result:

SRS.Student

However, simply printing out the name of the class that an object belongs to isn't very informative. Fortunately, as was the case with the Equals method, the Object class version of the ToString method is declared to be virtual, which enables us to override its behavior for a derived class.

Overriding the ToString Method

In the preceding example, we'd prefer that the ToString method for a Student return a more informative result, such as perhaps the label "Student:" followed by a given student's name and student ID number, formatted as shown here:

Student: Dianne Bolden [999999]

To achieve this result, we'd simply need to override the ToString method in the Student class as follows:

public class Student {
  // Fields.
  private string name;
  private string studentId;
  // Other details omitted ...
  public override string ToString() {
    return "Student: " + Name + " [" + StudentId + "]";
  }

Now, the snippet shown earlier:

Student s = new Student();
s.Name = "Dianne Bolden";
s.StudentId = "999999";
Console.WriteLine(s.ToString());

would output the desired result when executed:

Student: Dianne Bolden [999999]

by virtue of our overridden ToString method.

"Behind the Scenes" Use of ToString

As it turns out, the ToString method is often called "behind the scenes"—for example, by the Console.WriteLine method. The Console.WriteLine method, which we normally think of as accepting string arguments, is overloaded such that an arbitrary object reference can be passed as an argument to the method. It's therefore perfectly acceptable to write code as follows:

Student s = new Student();
s.Name = "Cheryl Richter";
s.StudentId = "123456";
Console.WriteLine(s);

When an arbitrary object reference is passed to the WriteLine method, the WriteLine method automatically calls the object's ToString method to obtain its class-specific string representation, which is then printed to the console. Given the way in which we'd overridden the ToString method for the Student class earlier, the result of running the preceding code snippet would produce the following output:

Student: Cheryl Richter [123456]

Many of the predefined classes in the .NET Framework libraries have overridden the ToString method. What's more, it's a good idea to get into the habit of overriding the ToString method for all user-defined classes, to ensure that whenever ToString is called "behind the scenes" on an instance of such a class, a meaningful result is returned.

Other Object Class Methods

There are three other public methods defined by the Object class:

  • The GetType method allows us to determine the type of an arbitrary object at run time, and will be discussed in detail later in this chapter in the section titled "Object Identities."

  • GetHashCode and ReferenceEquals are less frequently used, and won't be covered in detail in this book. Please consult the FCL Reference on the MSDN web site if you'd like more information on these methods.


Team LiB
Previous Section Next Section