Overview of the Object Class

Introduction

The object class is the ultimate base class of every type. Because of its lofty position and significant influence on the code you write, you should have a good understanding of the object class and its members. You may also be interested in knowing why object exists in the first place. In this article, we look at these things with the goal of making you more aware of the rationale and use of the object class in your code.

Why an Object Class?

As the ultimate base class that all other other types directly or indirectly derive from, the object class allows you to build generic routines, provides Type System Unification, and enables working with groups of types as collections. Common practice for implementing generic routines has been to write a method that accepts arguments of type object and allow users to pass in any type they want. This approach is difficult to maintain because of the lack of type safety and inefficiency for value types. I believe the practice of using the object class for generic routines will be replaced in many future scenarios by the use of Generics in C# v2.0.

Reference types inherit the object class either directly or through other reference types. Value types inherit implicitly from the object class through System.ValueType. In C#, the object class is a language specific alias for the .NET Base Class Library (BCL) type with the fully qualified name of System.Object. Figure 1 shows the the relationships between the object class and other types.

ObjectHierarchy

Figure 1. The object class hierarchy has the System.Object type at the top. Reference types inherit System.Object directly or through another reference type. Value types implicitly inherit System.Object through the System.ValueType type.

The ability of both reference types and value types to be treated as objects supports Type System Unification. In many languages, built-in types such as int and float have no object-oriented properties and must be explicitly wrapped in objects to simulate object-oriented behavior. However, C# eliminates the requirement to wrap built-in types through the concept of value types that inherit fromSystem.ValueType, which inherits from System.Object. This allows C# built-in types to be worked with in a manner similar to reference types. From an object-oriented perspective, under Type System Unification both reference types and value types are objects.

Of course you don’t get Type System Unification for free. When assigning a value type to an object, behind the scenes, the system performs an extra operation referred to as Boxing. After a boxing operation has completed, a value type is considered boxed. Similarly, when copying a boxed value type back to a normal value type, behind the scenes, the system performs another operation referred to as Unboxing. While any value type can be boxed, it is not logical to assume that any reference type can be unboxed. Only objects that have been previously boxed can be unboxed. When a value type is boxed, a new object for that type is created in memory and the value of the value type is copied into that object. During unboxing the value in the object is copied into the value of a value type. Listing 1 shows a simple boxing/unboxing operation.

Listing 1.  Boxing and Unboxing

int myValType1 = 5;
object myRefType = myValType1; // myValType is boxed
int myValType2 = (int) myRefType; // myValType is unboxed

Although, for performance reasons, you need to be aware of the boxing/unboxing operations, the point being made here is that the objectclass, as the ultimate base class, promotes Type System Unification. This makes it very simple to work with all types in the same manner for those times when such behavior in your application is necessary.

The object class also facilitates using any type in a collection. It contains methods that can be reused or overridden in base types. Since collections, such as Hashtable and ArrayList accept object types, you can add any type to them. The Equals, GetType, and GetHashCodeobject class methods are called directly by the collection classes. Other object class methods offer general capabilities that types have access to and can expose in their interface.

The Equals Method

A common operation, especially for searching and sorting in collections, is testing two objects for equality. The Equals method of the objectclass provides a default implementation that compares two reference type objects for reference equality. Reference equality occurs when two reference type objects refer to the same object. Sometimes reference types need to define value equality instead of reference equality. Fortunately, the Equals method is virtual, so derived reference types may override it. An example is the string class, which overrides Equalsto ensure that two strings are compared by the value of their strings. Value types are compared for bitwise equality. Listing 2 demonstrates how to use the Equals method with reference types.

Listing 2. Using the Equals Method

using System;

class Employee
{
   string m_name;
   public Employee(string name)
   {
       m_name = name;
   }
}

class EqualsDemo
{
   static void Main()
   {
       EqualsDemo eqDemo = new EqualsDemo();
       eqDemo.InstanceEqual();
       Console.ReadLine();
   }

   public void InstanceEqual()
   {
       string name = "Joe";
       Employee employee1 = new Employee(name);
       Employee employee2 = new Employee(name);

       // comparing references to separate instances
       bool isEqual = employee1.Equals(employee2);

       Console.WriteLine("employee1 == employee2 = {0}", isEqual);

       employee2 = employee1;

       // comparing references to the same instance
       isEqual = employee1.Equals(employee2);

       Console.WriteLine("employee1 == employee2 = {0}", isEqual);
   }
}

The demo in Listing 2 shows how the default implementation of Equals in the object class performs comparison on reference types. In theInstanceEqual method the first comparison is of two separate instances of Employee, so the result of the call to Equals will be false. Next, the reference to the object that employee1 refers to is assigned to employee2, making both references refer to the same instance. This makes the next call to the Equals method return true. To change the way Equals works, the Employee class could override Equals and provide an implementation based on equality of the m_name member.

There is also a static Equals method that performs the same task: Object.Equals(object obj1, object obj2).

The ReferenceEquals Method

In the object class, the Equals and ReferenceEquals methods are semantically equivalent, except that the ReferenceEquals works only on object instances. The ReferenceEquals method is static. Listing 3 demonstrates how to use the ReferenceEquals method.

Listing 3. Using the ReferenceEquals Method.

using System;

class Employee
{
   string m_name;

   public Employee(string name)
   {
       m_name = name;
   }
}

class ReferenceEqualsDemo
{
   static void Main()
   {
       ReferenceEqualsDemo refEqDemo = new ReferenceEqualsDemo();

       refEqDemo.InstanceEqual();

       Console.ReadLine();
   }

   public void InstanceEqual()
   {
       string name = "Joe";

       Employee employee1 = new Employee(name);
       Employee employee2 = new Employee(name);

       // comparing references to separate instances
       bool isEqual = Object.ReferenceEquals(employee1, employee2);

       Console.WriteLine("employee1 == employee2 = {0}", isEqual);

       employee2 = employee1;

       // comparing references to the same instance
       isEqual = Object.ReferenceEquals(employee1, employee2);
       Console.WriteLine("employee1 == employee2 = {0}", isEqual);
   }
}

Notice that the code in Listing 2 and Listing 3 are the same except that Listing 3 uses the ReferenceEquals method. The results are also the same.

The ToString Method

The purpose of the ToString method is to return a human readable representation of a type. The default implementation in the object class returns a string with the name of the runtime type of the object. Listing 4 demonstrates how to implement the ToString method in your own type.

Listing 4. Implementing the ToString method.

using System;

class Employee
{
   string m_name;

   public Employee(string name)
   {
       m_name = name;
   }

   public override string ToString()
   {
       return String.format("[Employee: {0}]", m_name);
   }
}

class ToStringDemo
{
   static void Main()
   {
       Employee emp = new Employee("Joe");

       Console.WriteLine(emp.ToString());

       Console.ReadLine();
   }
}

The ToString method in Listing 4 overrides ToString in the object class. You can use ToString to facilitate debugging by providing human readable information about your type that can be viewed by developers inspecting program output emitted by calls to the ToString method.

The GetType Method

GetType is the basis for using reflection in .NET. It returns a Type object, describing the object it was called on. It can then be used to extract type member data like methods and fields that may subsequently be used for late-bound method invocations. The GetType method is also useful if you get an object at runtime and you don’t know what it’s type is. You can use the returned Type object to figure out what to do with the object. Listing 5 demonstrates how to use the GetType method.

Listing 5. Using the GetType Method.

using System;

class Employee
{
}

class GetTypeDemo
{
   static void Main()
   {
       object emp1 = new Employee();
       Employee emp2 = new Employee();

       Console.WriteLine(emp1.GetType());
       Console.WriteLine(emp2.GetType());

       Console.ReadLine();
   }
}

There are two instances of the Employee class created in the Main method of Listing 5. The first is assigned to a reference of type objectand the second is assigned to a reference of type Employee. When run, the program will print “Employee” as the result of the call toGetType. This shows that GetType returns the run-time type of the object it is called on, rather than the compile-time reference it is assigned to. The reason the program could get a string out of the call to GetType is because it returns a Type object, which overrides theToString method used by Console.WriteLine.

The GetHashCode Method

The GetHashCode method makes any object usable in a Hashtable or any hashing algorithm. Since the default algorithm supplied by theGetHashCode method of the object class is not guaranteed to be unique, you should override GetHashCode in your custom types. Listing 6 demonstrates how to implement a custom GetHashCode method.

Listing 6. Implementing a GetHashCode method.

using System;

class Employee
{
   string m_name;

   public Employee(string name)
   {
       m_name = name;
   }

   public override int GetHashCode()
   {
       string uniqueString = ToString();

       return uniqueString.GetHashCode();
   }

   public override string ToString()
   {
       return String.format("[Employee: {0}]", m_name);
   }
}

class GetHashCodeDemo
{
   static void Main()
   {
       Employee emp = new Employee("Joe");

       Console.WriteLine(emp.GetHashCode());

       Console.ReadLine();
   }
}

The Employee class in Listing 6 overrides the GetHashCode method to provide a unique number for each unique instance of the Employeeclass. This is accomplished by using the ToString method that will return a string uniquely representing the value of this instance. ThenGetHashCode is called on the string and returned to the caller. The benefits of using the string class is that it already has a GetHashCodefunction that can be used. The GetHashCode method of the string class returns a random distribution of hash codes, guarantees codes are unique for different strings, and ensures codes are identical for the same strings. Although not shown in this example for the sake of brevity, types that implement GetHashCode should also implement Equals for Hashtable support.

The MemberwiseClone Method

Whenever you need to create a shallow copy of your type, use the MemberwiseClone method. A shallow copy is a bitwise copy of your type. As such, if you perform a MemberwiseClone on your class, it will make a copy of the type and all contained value types and references types. However, it will not copy the objects that the reference type members in your type refer to. This behavior of only making a copy of the first level of your type demonstrates the reason why a MemberwiseClone is called a shallow copy. Since the MemberwiseClone method is not virtual, you can not override it in derived classes. You should implement the IClonable interface if you need a deep copy. Listing 7 shows how to use MemberwiseClone.

Listing 7.  Using the MemberwiseClone Method.

using System;

public class Address
{
}

class Employee
{
   Address m_address = new Address();
   string m_name;

   public Employee(string name)
   {
       m_name = name;
   }

   public Employee ShallowCopy()
   {
       return (Employee)MemberwiseClone();
   }

   public Address EmployeeAddress
   {
       get
       {
            return m_address;
       }
   }
}

class MemberwiseCloneDemo
{
   static void Main()
   {
       Employee emp1 = new Employee("Joe");
       Employee emp2 = emp1.ShallowCopy();

       // compare Employee references
       bool isEqual = Object.ReferenceEquals(emp1, emp2);

       Console.WriteLine("emp1 == emp2: {0}", isEqual);

       // compare references of Address object in each Employee object
       isEqual = Object.ReferenceEquals(emp1.EmployeeAddress, emp2.EmployeeAddress);

       Console.WriteLine("emp1.EmployeeAddress == emp2.EmployeeAddress: {0}", isEqual);

       Console.ReadLine();
   }
}

Since MemberwiseClone has protected visibility, Listing 7 wraps the call to it in the ShallowCopy method of the Employee class. When theMain method calls ShallowCopy, the emp2 variable holds a reference to a copy of emp1. However, this is a shallow copy, as subsequent statements prove. The first call to ReferenceEquals shows that the emp1 and emp2 variables refer to separate instances. The second call toReferenceEquals performs a comparison on the Address object within the emp1 and emp2 instances. The comparison demonstrates that both of the Address references point to the same object, proving that only a shallow copy has been performed.

The Finalize Method

Although the object class has a Finalize method, it is not available to C# programs in that form. Instead, you would use what is called a destructor in C#, which is synonymous with the Finalize method. In further discussion, I’ll refer to the Finalize method as a destructor. The original purpose of the destructor was to serve as a place where you can release unmanaged resources such as network connections, operating system resources, or file streams. However, in practice you never want to use the destructor. The reason is that a destructor is executed in non-deterministic time, meaning that you have no way of knowing when it will be run and no way to know when your resources will be released. Developers familiar with C++ will notice that this is a distinct difference in destructor behavior because in C++ a destructor is executed deterministically, as soon as an object is freed. The solution to this problem in C# is to implement the IDisposableinterface on all your types that need to release unmanaged resources. Listing 8 shows a class that implements a destructor.

Listing 8.  Implementing a Destructor

using System;

class Employee
{
   string m_name;

   public Employee(string name)
   {
       m_name = name;
   }

   ~Employee()
   {
       Console.WriteLine("Employee destructor executed.");
   }
}

class DestructorDemo
{
   static void Main()
   {
       Employee emp = new Employee("Joe");

       Console.ReadLine();
   }
}

The Employee class in Listing 8 implements a destructor. Destructors are prefixed with a tilde and named the same as their containing class. Additionally, they do not have parameters. Value types do not have destructors. If you run this program it will not print anything to the console and will wait for you to press the Enter key. Since it is so small, it will not consume enough memory resources to force a garbage collection, which in turn would call the destructor. In fact, the program will set there forever and the destructor may never be called if you don’t ever press the Enter key. If the program was holding an unmanaged resource, the unmanaged resource would not be released. When you do press the enter key, the CLR will initiate a garbage collection and the program will print “Employee destructor executed.” to the screen when the garbage collector calls the destructor, prior to final program shutableown. If you are using an IDE, this message will flash on the screen quickly as the console goes away. To see the message, open a console window and run the executable program on the command line.

There are a couple arguments that are for and against using destructors — both agree that the Dispose pattern should be implemented. One of these arguments states that a destructor should be implemented as a fall-back in case code that uses your class doesn’t implement the Dispose pattern properly (or at all). They view the destructor as a safety net. Another argument states that destructors should never be implemented. This point of view regards destructors as dangerous because they give the implementer a false sense of safety. For instance, if there were a problem with the code in the destructor you wouldn’t know it until the destructor was executed, which may never happen at all. Because destructor execution is non-deterministic, you have a non-deterministic problem to solve, which may be very difficult and nearly impossible to reproduce and fix. Having worked on many mission critical systems and having to overcome the perils of fixing production problems, I support the second argument that states you should not use destructors. You should learn how to implement theIDisposable (The Dispose Pattern) interface properly.

Conclusion

As the ultimate base class of all .NET types, the object class is very important. It contains several methods that you need to be aware of. You need to know how to use and implement overrides of some of these methods as appropriate for building well-behaved custom types. Most of the discussion about object class methods begs for more information, which I’ll follow up with in subsequent articles. This has been an overview that illuminates key points that should be useful in your development endeavors.

Follow Joe Mayo on Twitter.

Feedback

Loading...