Object Equality - Comparing Instances Correctly in C#

Vaibhav • September 10, 2025

In the previous article, we explored the differences between struct and class - how they behave in memory, how they are copied, and how they affect performance and design. Now, we turn to a topic that often causes confusion for beginners and even experienced developers: object equality.

In C#, comparing objects is not always straightforward. Two variables may refer to the same object, or they may refer to different objects with identical data. Understanding how equality works - both for value types and reference types - is essential for writing correct and predictable code. In this article, we’ll explore the different kinds of equality, how to override equality behavior, and how to avoid common pitfalls.

Reference Equality vs Value Equality

C# supports two main types of equality:

  • Reference equality - whether two variables refer to the same object in memory.
  • Value equality - whether two objects contain the same data.

Reference equality is the default for classes. Value equality is the default for structs.

class Person
{
    public string Name;
}

Person p1 = new Person { Name = "Alice" };
Person p2 = new Person { Name = "Alice" };

Console.WriteLine(p1 == p2); // False - different references
Console.WriteLine(object.ReferenceEquals(p1, p2)); // False

Even though p1 and p2 contain the same data, they are different objects. The == operator and ReferenceEquals() both check reference equality.

For reference types, == compares references unless the operator is overloaded. For value types, == compares values.

Value Equality for Structs

Structs are value types, so equality is based on their data. Two structs with the same field values are considered equal.

struct Point
{
    public int X;
    public int Y;
}

Point a = new Point { X = 1, Y = 2 };
Point b = new Point { X = 1, Y = 2 };

Console.WriteLine(a == b); // True
Console.WriteLine(a.Equals(b)); // True

The == operator and Equals() both compare the values of the fields. This behavior is built into the .NET runtime for structs.

Overriding Equality in Classes

If you want value equality for classes, you must override Equals() and GetHashCode(). You can also overload the == and != operators.

class Product
{
    public string Name;
    public decimal Price;

    public override bool Equals(object obj)
    {
        if (obj is Product other)
            return Name == other.Name && Price == other.Price;

        return false;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Price);
    }

    public static bool operator ==(Product a, Product b)
    {
        if (ReferenceEquals(a, b)) return true;
        if (a is null || b is null) return false;
        return a.Equals(b);
    }

    public static bool operator !=(Product a, Product b) => !(a == b);
}

This implementation allows you to compare Product objects by value:

Product p1 = new Product { Name = "Laptop", Price = 999.99m };
Product p2 = new Product { Name = "Laptop", Price = 999.99m };

Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.Equals(p2)); // True

Without this override, == would compare references, and Equals() would use the default implementation from object.

Using object.Equals() Safely

The static method object.Equals(a, b) is null-safe and works for both value and reference types. It calls a.Equals(b) if a is not null.

Product p1 = null;
Product p2 = new Product { Name = "Phone", Price = 499.99m };

Console.WriteLine(object.Equals(p1, p2)); // False
Console.WriteLine(object.Equals(p1, null)); // True

This method avoids null reference exceptions and is useful in defensive code.

Comparing Strings

Strings are reference types, but they behave like value types because they are immutable. The == operator compares string contents.

string s1 = "hello";
string s2 = "he" + "llo";

Console.WriteLine(s1 == s2); // True
Console.WriteLine(s1.Equals(s2)); // True

Even though s2 is built dynamically, it has the same content as s1. Both comparisons return true.

You can use StringComparison.OrdinalIgnoreCase to compare strings without case sensitivity.

Reference Equality with ReferenceEquals()

If you want to check whether two variables refer to the same object, use object.ReferenceEquals().

Person p1 = new Person { Name = "Alice" };
Person p2 = p1;

Console.WriteLine(object.ReferenceEquals(p1, p2)); // True

This method is useful for identity checks, caching, and avoiding duplicate processing.

Equality in Collections

Collections like Dictionary and HashSet use Equals() and GetHashCode() to determine uniqueness. If you override equality, you must also override GetHashCode() consistently.

HashSet<Product> products = new HashSet<Product>();
products.Add(new Product { Name = "Tablet", Price = 299.99m });
products.Add(new Product { Name = "Tablet", Price = 299.99m });

Console.WriteLine(products.Count); // Output: 1

Without a proper GetHashCode(), the collection may treat equal objects as different.

Avoiding Equality Pitfalls

Comparing objects incorrectly can lead to subtle bugs. Here are some common mistakes:

  • Using == for classes without overriding it.
  • Forgetting to override GetHashCode() when overriding Equals().
  • Comparing structs in collections without reassigning modified copies.
  • Assuming == means value equality for all types.

Always understand what kind of equality you need - reference or value - and implement it accordingly.

Summary

Object equality in C# is a nuanced topic that affects how your code behaves and how your data is compared. Value types use value equality by default. Reference types use reference equality unless overridden. You can customize equality by overriding Equals(), GetHashCode(), and the == operator.

We explored how equality works for structs, classes, strings, and collections. We discussed how to implement custom equality logic and how to avoid common mistakes. We also covered reference identity checks and null-safe comparisons.

As you continue building applications, make sure your types compare correctly. Whether you're checking identity, uniqueness, or equivalence, understanding object equality will help you write safer and more reliable code.

In the next article, we’ll explore OOP Best Practices - how to apply object-oriented principles effectively in real-world C# projects.