Overview of the Object Class
by Joe Mayo, 10/12/03
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.

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 from System.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 object class, 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 GetHashCode object 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 object
class 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 Equals
to 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 the InstanceEqual
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 object and
the second is assigned to a reference of type Employee. When run,
the program will print "Employee" as the result of the call to GetType.
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 the ToString 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 the GetHashCode
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 Employee class.
This is accomplished by using the ToString method that will return a
string uniquely representing the value of this instance. Then GetHashCode
is called on the string and returned to the caller. The benefits of using
the string class is that it already has a GetHashCode function
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 the Main 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 to ReferenceEquals 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 IDisposable interface 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 the IDisposable
(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.