Back to Blog
C#

C# Reference Equality vs Value Equality

c# reference equality vs value equality: Understand the difference between reference equality and value equality in C#, how to implement custom equality, and when reco...

EqualityIEquatableRecordsStructsOperator Overloading
Two overlapping object diagrams with a comparison symbol, illustrating reference equality versus value equality in C#.

When you compare two objects in C#, the result depends on whether you're dealing with a reference type or a value type, and on whether equality has been explicitly defined. The distinction between c# reference equality vs value equality is one of the most common sources of subtle bugs, especially when objects are stored in collections or used as dictionary keys. This article explains the default behavior, how to override it correctly, and when the built-in features of records make manual implementation unnecessary.

Default Equality Behavior for Classes

For a class, the default Equals(object) method uses reference equality. Two variables are equal only if they point to the exact same instance in memory. The static object.ReferenceEquals method works the same way and cannot be overridden.

public class Person { public string Name { get; set; } } var p1 = new Person { Name = "Alice" }; var p2 = new Person { Name = "Alice" }; Console.WriteLine(p1 == p2); // False Console.WriteLine(p1.Equals(p2)); // False Console.WriteLine(ReferenceEquals(p1, p2)); // False

Even though p1 and p2 have identical property values, they are different objects. This is the expected behavior for most reference types because identity matters more than content in many scenarios. However, when you need two separate objects to be considered equal based on their data, you must override the equality members.

Default Equality Behavior for Structs

Value types, such as struct, use a default value equality implementation. The compiler generates an Equals method that compares each field of the struct using EqualityComparer<T>.Default. This means two struct instances with identical field values are equal.

public struct Point { public int X; public int Y; } var a = new Point { X = 1, Y = 2 }; var b = new Point { X = 1, Y = 2 }; Console.WriteLine(a == b); // False (no operator overload) Console.WriteLine(a.Equals(b)); // True

Note that the == operator is not automatically defined for structs. The default Equals works, but it uses reflection to compare fields, which is slow. For structs used in performance-critical paths, implementing IEquatable<T> and overriding Equals is recommended.

Overriding Equals and GetHashCode

When you override Equals for a class, you must also override GetHashCode. The hash code must be consistent with equality: if two objects are equal, they must produce the same hash code. Otherwise, Dictionary and HashSet will behave incorrectly.

public class Person : IEquatable<Person> { public string Name { get; set; } public bool Equals(Person? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Name == other.Name; } public override bool Equals(object? obj) => Equals(obj as Person); public override int GetHashCode() => Name?.GetHashCode() ?? 0; }

The typed Equals(Person?) method comes from IEquatable<T> and avoids boxing when the type is known. The object.Equals override calls into the typed version. The GetHashCode implementation uses the same field that determines equality, so the contract holds.

Operator Overloading for == and !=

Defining Equals does not automatically make == work. You must overload the operators explicitly.

public class Person : IEquatable<Person> { public string Name { get; set; } public static bool operator ==(Person? left, Person? right) { if (left is null) return right is null; return left.Equals(right); } public static bool operator !=(Person? left, Person? right) => !(left == right); }

Now p1 == p2 uses the custom logic. Be careful with null handling: the == operator must treat two null references as equal, and a null left operand must not throw.

Records: Value Equality Without Boilerplate

C# 9 introduced records, which are reference types but use value equality by default. The compiler generates Equals, GetHashCode, and the == and != operators based on the record's positional properties or init-only properties.

public record Person(string Name, int Age); var p1 = new Person("Alice", 30); var p2 = new Person("Alice", 30); Console.WriteLine(p1 == p2); // True Console.WriteLine(p1.Equals(p2)); // True

Records are the easiest way to get value equality for reference types without writing repetitive code. The generated equality compares each property in order. If you need to exclude a property from equality, you can use the [property: IgnoreEquality] attribute (in .NET 7+), or you can manually override the equality members inside the record.

Performance and Allocation Considerations

The default Equals for structs uses reflection, which is significantly slower than a hand-written comparison. For structs that are frequently compared, such as keys in a dictionary, implementing IEquatable<T> avoids boxing and uses direct field comparisons. For classes, the cost of a custom Equals is usually negligible unless the comparison involves complex logic or deep object graphs.

Hash code computation also matters. A poor GetHashCode that returns a constant for all instances will degrade dictionary lookups to O(n). Use a hash function that distributes values well, such as combining hash codes with a multiplier.

public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 23 + Name.GetHashCode(); hash = hash * 23 + Age.GetHashCode(); return hash; } }

This pattern is common and works well for most scenarios. If performance measurements show that equality is a bottleneck, profile before optimizing further.

Common Pitfalls and How to Avoid Them

One frequent mistake is overriding Equals but not GetHashCode, or using a mutable field in GetHashCode. If an object's hash code changes after it is added to a HashSet or Dictionary, the object becomes unreachable. Avoid using mutable properties in equality and hash code calculations.

Another issue is inconsistent equality between the Equals method and the == operator. If you override one but not the other, code that uses == may behave differently from code that uses Equals. Always keep them aligned.

Inheritance adds complexity. A base class that overrides equality must ensure that derived types are handled correctly. The Equals method should check the runtime type, or you should seal the class to prevent unexpected behavior. Records handle this well because the compiler generates equality that respects the actual type.

Choosing the Right Equality Strategy

Use reference equality when identity is what matters, such as with entity objects that have a database ID and are managed by a service. Use value equality when the content defines the object, such as with value objects, DTOs, or configuration options. For simple data containers, records are the best choice because they provide value equality with minimal code. For structs, implement IEquatable<T> and override Equals and GetHashCode to avoid reflection overhead. For classes that must behave like values, manually implement the full equality contract or use a record if the shape is immutable.

c# reference equality vs value equality: Practical Usage and | RYUSLOG DEV