The Object Class - Root of All Types in C#
Vaibhav • September 10, 2025
In the previous article, we explored how casting and type checking allow us to work safely with polymorphic
objects. Now we turn our attention to the foundation of all types in C#: the object
class. Every type in C# - whether it’s a class, struct, array, or even a
primitive like int
- ultimately derives from object
. This makes it the universal base class in the .NET type system. In this
article, we’ll explore what that means, what members object
provides, and how
to override them effectively in your own types.
Why does everything derive from object?
In C#, all types - including user-defined classes, built-in types like int
, and
even arrays - inherit from System.Object
. This design gives the language a
unified type system, where any value can be treated as an object
. This is what
allows you to write code like:
object x = 42;
object y = "hello";
object z = new List<int>();
Here, x
, y
, and z
are all declared as object
, but they hold
values of different types. This is the essence of polymorphism - treating different types through a common base.
In .NET, object
is an alias for System.Object
. You can use either name interchangeably.
What members does object provide?
The object
class defines a small set of methods that are inherited by all
types. These include:
public class Object
{
public virtual bool Equals(object obj);
public static bool Equals(object objA, object objB);
public virtual int GetHashCode();
public Type GetType();
public virtual string ToString();
}
Let’s explore each of these methods in detail and see how they’re used in real-world code.
ToString - converting objects to text
The ToString()
method returns a string representation of an object. By default,
it returns the fully qualified type name:
object obj = new object();
Console.WriteLine(obj.ToString()); // Output: System.Object
Most types override ToString()
to provide more meaningful output. For example:
int number = 42;
Console.WriteLine(number.ToString()); // Output: 42
DateTime now = DateTime.Now;
Console.WriteLine(now.ToString()); // Output: 9/10/2025 10:30:00 AM (or similar)
You can override ToString()
in your own classes to customize how they appear in
logs, debugging, or UI.
class Person
{
public string Name;
public int Age;
public override string ToString()
{
return $"{Name} ({Age} years old)";
}
}
Now, printing a Person
object will show a friendly summary instead of just the
type name.
Equals - comparing object equality
The Equals()
method checks whether two objects are considered equal. The
default implementation compares references - two objects are equal only if they refer to the same instance.
object a = new object();
object b = new object();
Console.WriteLine(a.Equals(b)); // False
object c = a;
Console.WriteLine(a.Equals(c)); // True
Many types override Equals()
to compare values instead of references. For
example:
string s1 = "hello";
string s2 = "hello";
Console.WriteLine(s1.Equals(s2)); // True - same content
You can override Equals()
in your own types to define what equality means:
class Point
{
public int X, Y;
public override bool Equals(object obj)
{
if (obj is Point other)
return X == other.X && Y == other.Y;
return false;
}
}
Now two Point
objects with the same coordinates will be considered equal.
Always override GetHashCode()
when you override Equals()
. This ensures consistent behavior in hash-based collections like
Dictionary
and HashSet
.
GetHashCode - generating hash codes
The GetHashCode()
method returns an integer that represents the object’s hash
code. This is used in hash-based collections to quickly locate items.
The default implementation returns a unique value per instance, but if you override Equals()
, you must also override GetHashCode()
to ensure that equal objects produce the same hash code.
class Point
{
public int X, Y;
public override bool Equals(object obj)
{
if (obj is Point other)
return X == other.X && Y == other.Y;
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
}
This implementation uses HashCode.Combine()
to generate a stable hash based on
the fields. This ensures that two equal Point
objects will have the same hash
code.
GetType - inspecting runtime type
The GetType()
method returns a Type
object
that describes the actual runtime type of the object. This is useful for reflection, diagnostics, and debugging.
object obj = new List<int>();
Console.WriteLine(obj.GetType()); // Output: System.Collections.Generic.List`1[System.Int32]
You can use GetType()
to compare types or inspect metadata:
if (obj.GetType() == typeof(List<int>))
{
Console.WriteLine("It's a list of integers");
}
This is more precise than using is
or as
,
which also consider inheritance.
Object in collections and APIs
Because all types derive from object
, many APIs use object
as a generic placeholder. For example:
void Print(object value)
{
Console.WriteLine(value.ToString());
}
This method can accept any type - string, number, list, or even a custom class - and print its string
representation. This is possible because all types support ToString()
.
Similarly, collections like ArrayList
(non-generic) store items as object
, allowing mixed types:
ArrayList list = new ArrayList();
list.Add(42);
list.Add("hello");
list.Add(new DateTime(2025, 9, 10));
While this is flexible, it requires casting when retrieving items. That’s why generic collections like List<T>
are preferred in modern C#.
Boxing and unboxing
When a value type (like int
or bool
) is
assigned to an object
variable, it is boxed - wrapped in a
reference type. When it’s cast back, it is unboxed.
int x = 10;
object obj = x; // boxing
int y = (int)obj; // unboxing
Boxing incurs a performance cost and should be avoided in performance-critical code. Generics help eliminate the need for boxing by preserving type information.
Boxing only applies to value types. Reference types are already stored as objects.
Summary
The object
class is the root of the C# type system. Every type inherits from
it, which means every object in C# supports methods like ToString()
, Equals()
, GetHashCode()
, and GetType()
. Understanding how and when to override these methods is essential for
writing clean, correct, and idiomatic C# code. Whether you're customizing how your objects print, defining
equality, or working with polymorphic APIs, the object
class is always at the
core.
In the next article, we’ll explore Inheritance Best Practices - how to design class hierarchies that are flexible, maintainable, and easy to reason about.