How to Overload the == Operator in C#
c# overload == operator: Learn how to overload the == operator in C# for value and reference types, and understand the rules, pitfalls, and performance considerations.
c# overload == operator requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Overloading the == operator in C# is one of the most common ways to customize equality for your own types. By default, == performs reference equality for classes and value equality for structs. But in many cases, you need a different rule—for example, two Person objects might be equal when their Id values match, regardless of whether they point to the same memory location. To make that happen, you must overload == and usually also override Equals and GetHashCode.
This article focuses on the correct syntax, the required companion methods, and the runtime behavior that makes overloading == work reliably. It covers when to use it, when to avoid it, and the subtle performance and compatibility implications that often surprise developers.
The Basic Syntax for Overloading ==
To overload ==, you define a static operator method inside the type. The method must take two parameters of the containing type and return a bool.
public struct Money { public decimal Amount { get; } public string Currency { get; } public Money(decimal amount, string currency) { Amount = amount; Currency = currency; } public static bool operator ==(Money left, Money right) { return left.Amount == right.Amount && left.Currency == right.Currency; } public static bool operator !=(Money left, Money right) { return !(left == right); } }
This code lets you compare Money instances with == and != in a natural way. The != operator is not automatically derived from ==; you must define it explicitly. Notice that the == overload here is an instance method, not an extension method—you cannot overload operators with extension methods.
Why You Must Override Equals and GetHashCode
When you overload ==, you introduce a new definition of equality that may conflict with the base implementation of Object.Equals. In C#, == is not virtual, but Equals is. Many framework code paths, such as Dictionary<TKey, TValue>, HashSet<T>, and List<T>.Contains, rely on Equals rather than ==. If your == says two objects are equal but Equals says they are not, the behavior becomes inconsistent and confusing.
For value types (structs), the default ValueType.Equals uses reflection and can be slow. By overriding Equals, you can provide faster, semantically correct equality. For reference types, the default object.Equals checks reference equality. If your == overload defines value equality, then Equals should match that same logic.
The compiler does not force you to override these methods, but omitting them leads to subtle bugs. For example, a HashSet<T> will not treat two objects as equal if Equals is not overridden, even if == returns true. That inconsistency can break collection behavior, so the rule is: overload == only if you also override Equals and GetHashCode.
Here is a corrected version for a reference type:
public class Person { public string Id { get; } public string Name { get; } public Person(string id, string name) { Id = id; Name = name; } public override bool Equals(object obj) { return obj is Person other && Id == other.Id; } public override int GetHashCode() { return Id.GetHashCode(); } 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) { return !(left == right); } }
The == operator here delegates to Equals to ensure consistency. It also handles null correctly. Notice that we use is null to avoid the overloaded == inside the operator itself, which would otherwise cause infinite recursion.
Handling Nulls in the == Operator
Null handling is the most error-prone part of overloading ==. If you write a naive comparison that dereferences a parameter without checking for null, you get a NullReferenceException when one side is null. Worse, if you call the overloaded == inside the operator itself, you trigger infinite recursion.
// Dangerous: calls itself recursively public static bool operator ==(Person left, Person right) { return left == right; }
Always compare with ReferenceEquals or use pattern matching with is null for null checks. For value types, nullability is trickier: Nullable<T> automatically lifts operators, but if you overload == on a struct, you should still think about how it behaves when wrapped in Nullable<T>.
If you are working with a class, the safest pattern is:
public static bool operator ==(Person left, Person right) { if (left is null) return right is null; return left.Equals(right); }
This returns true when both are null, and false when exactly one is null. For equality, null equals null—that is a sensible convention in most C# code.
When to Overload == for Value Types vs Reference Types
For value types (structs), overloading == is a strong choice because the default ValueType.Equals is slow and often not semantically correct. For example, a struct representing a Money amount should compare by numeric value and currency, not by the bitwise representation of the struct fields.
For reference types, the decision is less clear-cut. If your type is an entity with an identity (like a database row with an Id), overriding == to compare by that Id can be convenient, but it changes the semantics of equality for the entire lifetime of the object. In many designs, reference equality is the correct default for mutable classes. Using value equality for mutable objects can cause collections to break if the object changes after being added to a dictionary.
| Type Kind | Default == behavior | When overloading makes sense |
|---|---|---|
class | Reference equality | Immutable value-like objects (e.g., DateRange, Coordinates) |
struct | Value equality via reflection | Any struct where equality should be fast or semantic |
record (C# 9+) | Value equality (compiler-generated) | Usually not needed unless custom logic is required |
The table clarifies the typical use cases. For records, the compiler already implements value equality, so adding your own operator overload is only necessary when you want to change the default behavior, such as ignoring certain properties.
Performance and Runtime Considerations
Overloading == can improve performance by avoiding reflection-based equality checks, but it can also introduce hidden costs. When you write left == right inside the operator, be wary of recursion. Also, if your operator performs complex calculations (like normalizing strings), it may be slower than a simple reference comparison.
For value types, the JIT can often inline simple operator methods, making == nearly as fast as a direct field comparison. For reference types, the cost is primarily the method call overhead, which is negligible compared to the actual comparison work.
One important performance concern is consistency with GetHashCode. If two objects are equal via ==, they must produce the same hash code. If not, hashtable-based collections become incorrect. A common mistake is to compare all fields in == but use only one field in GetHashCode. That is acceptable if that field determines equality, but if equality can be true for objects with different hash codes, you have a bug.
Another operational consideration is the interaction with the Nullable<T> wrapper. For a struct S, S? (i.e., Nullable<S>) promotes == to handle null and lifts the operator. However, if your overloaded operator is not careful with null handling, you may get unexpected results. The language specification says that lifted operators are only used when the operand types are non-nullable value types. If you define == on a non-nullable struct, the lifted operator automatically handles nullability for S?. That is convenient, but it means you must ensure your operator works when the underlying values are default (e.g., zero).
Common Pitfalls and How to Avoid Them
The most common pitfalls when overloading == are:
- Forgetting to overload
!=– The compiler does not infer!=from==. Without it, you get a compile-time error if you try to use!=. - Using the overloaded operator inside itself – This causes infinite recursion. Use
ReferenceEqualsoris nullfor null checks. - Inconsistent equality helpers – If
==says equal butEqualssays not equal, collections break. - Bad
GetHashCode– IfGetHashCodeis not consistent with==,DictionaryandHashSetwill misbehave. - Overloading for mutable reference types – If the object changes after being added to a collection, equality may change unexpectedly.
For example, this code contains a subtle bug:
public static bool operator ==(Person left, Person right) { return left.Id == right.Id; // No null check }
If left is null, this throws. The corrected version should handle null explicitly. Also, if Id is a string, the == on strings already handles nulls, but left itself could be null.
Best Practices for Overloading == in Public APIs
When you design a public API, overloading == affects how consumers perceive your type. Always document the equality semantics. If the type is intended to be used as a key in a dictionary, make sure the hash code is stable and does not depend on mutable state.
Consider implementing IEquatable<T> as well. The generic Equals(T other) overrides the non-generic Equals by routing through the generic version, which avoids type casts and is faster. Here is an example:
public class Money : IEquatable<Money> { public decimal Amount { get; } public string Currency { get; } public bool Equals(Money other) { if (other is null) return false; return Amount == other.Amount && Currency == other.Currency; } public override bool Equals(object obj) => Equals(obj as Money); public override int GetHashCode() => HashCode.Combine(Amount, Currency); public static bool operator ==(Money left, Money right) { return left?.Equals(right) ?? (right is null); } public static bool operator !=(Money left, Money right) => !(left == right); }
Note the use of HashCode.Combine for producing a good hash code, and the null-conditional ?. to avoid explicit null checks. This pattern is concise and correct.
Compatibility and Language Version Considerations
In older C# versions, you had to write your own GetHashCode implementation, often using unchecked arithmetic. Starting with C# 9, HashCode.Combine is the standard, but it requires .NET Core 2.1+ or .NET Standard 2.1. If you target .NET Framework, you may need to use a manual combination.
Also, whether you are using record types changes the picture. In C# 9 and later, records automatically generate equality members that respect the overloaded == if you define one, but the compiler synthesizes its own Equals and GetHashCode based on the positional parameters. If you define a custom ==, you must ensure it is consistent with the synthesized members, or the compiler may warn.
If you are working with older code that does not use records, the same rules apply: overload ==, override Equals and GetHashCode, and implement IEquatable<T> when appropriate. These rules are stable across C# 6 through C# 13.
Advanced: Operator Overloading and Inheritance
Operator overloads are static methods and are not virtual. This means that if you have a base class with an overloaded == and a derived class, the base class operator is invoked unless the derived class also declares its own. This can be surprising.
public class Base { public static bool operator ==(Base a, Base b) { ... } } public class Derived : Base { // If derived doesn't declare != , it inherits the base operator. }
If you compare two Derived objects with ==, the base operator runs, which may ignore derived-specific fields. To handle derived types properly, you must either not overload == on the base, or you must cast inside the operator and handle type mismatches. The recommended approach is to avoid overloading == in a non-sealed class unless you are prepared for that complexity.
Instead, prefer to implement IEquatable<T> on the derived types and let the base class provide a virtual Equals that derived classes can override. The == operator should delegate to Equals, as shown earlier, so that polymorphism works correctly.
Choosing the Right Equality Approach
Overloading == is not always the best solution. If you only need to compare objects for hashing, implementing IEquatable<T> and GetHashCode without overloading == may be sufficient. If you need to support == in generic code, you must overload the operator, because IEquatable<T> does not provide ==.
The decision criteria are:
- Use
==overload when you want natural syntax for equality comparisons in your domain. - Use
IEquatable<T>when you need fast equality checks in collections and avoid boxing. - Use
recordtypes when you want value equality with minimal code for immutable data.
In practice, many developers combine these: they overload == and implement IEquatable<T> to keep all equality paths consistent.
By following the rules outlined in this article, you can overload == safely and efficiently, avoiding the common bugs that arise from inconsistent equality implementations.