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.