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 overridingEquals()
. - 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.