C#
operator overloading
null check
recursion
programming tips

How do I check for nulls in an '' operator overload without infinite recursion?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When overloading == in C#, the easiest mistake is using == null inside the operator itself. That triggers the overloaded operator again, which calls itself again, and you end up with infinite recursion.

The safe pattern is to bypass the overloaded operator for null checks, usually with ReferenceEquals, and keep ==, !=, Equals, and GetHashCode aligned around one consistent equality definition.

Why Recursion Happens

This implementation looks innocent but is broken:

csharp
1public static bool operator ==(Money left, Money right)
2{
3    if (left == null && right == null) return true;
4    if (left == null || right == null) return false;
5    return left.Amount == right.Amount;
6}

The problem is that left == null calls the overloaded == operator again. That new call runs the same code, which performs the same check, and so on forever.

Use ReferenceEquals for Null Checks

ReferenceEquals checks object identity directly and does not route back through your overloaded operator:

csharp
1using System;
2
3public sealed class Money : IEquatable<Money>
4{
5    public decimal Amount { get; }
6
7    public Money(decimal amount)
8    {
9        Amount = amount;
10    }
11
12    public bool Equals(Money? other)
13    {
14        if (ReferenceEquals(other, null)) return false;
15        if (ReferenceEquals(this, other)) return true;
16        return Amount == other.Amount;
17    }
18
19    public override bool Equals(object? obj) => Equals(obj as Money);
20
21    public override int GetHashCode() => Amount.GetHashCode();
22
23    public static bool operator ==(Money? left, Money? right)
24    {
25        if (ReferenceEquals(left, right)) return true;
26        if (ReferenceEquals(left, null) || ReferenceEquals(right, null)) return false;
27        return left.Equals(right);
28    }
29
30    public static bool operator !=(Money? left, Money? right) => !(left == right);
31}

This handles all of the important cases safely:

  • both values are null
  • one is null
  • both references point to the same object
  • two different instances compare equal by value

Put the Real Equality Logic in Equals

The == operator should not invent a second equality system. A good design is:

  1. implement IEquatable<T>
  2. implement Equals(object?)
  3. implement GetHashCode()
  4. let == and != delegate to that logic

That keeps equality behavior consistent across operators, dictionaries, sets, and APIs that call Equals rather than ==.

If == says two values are equal but Equals disagrees, the type becomes very hard to reason about.

Prefer Modern Language Features When Possible

If the type is a straightforward value object, records can save you from writing most of this code by hand:

csharp
1public record Coordinate(int X, int Y);
2
3var a = new Coordinate(1, 2);
4var b = new Coordinate(1, 2);
5
6Console.WriteLine(a == b); // True

Manual operator overloads make sense only when you need custom semantics beyond what records or built-in value semantics already provide.

Test the Edge Cases Directly

Equality bugs hide at the edges, so test those edges explicitly:

csharp
1Money? a = null;
2Money? b = null;
3Money c = new Money(10m);
4Money d = new Money(10m);
5
6Console.WriteLine(a == b);   // True
7Console.WriteLine(c == d);   // True
8Console.WriteLine(c != null); // True

If your implementation is recursive or inconsistent, these cases usually expose it quickly.

Common Pitfalls

The biggest mistake is using == null or != null inside the overloaded operator itself. Those expressions call the operator again and create recursion.

Another common issue is overloading == without overriding Equals and GetHashCode. That leads to surprising behavior in collections and APIs that use object equality rules.

Developers also forget the identity shortcut. If both references point to the same object, you can return true immediately before doing any value comparison.

Finally, do not write manual equality code if a record or simpler type design already matches the use case. Less custom equality logic usually means fewer bugs.

Summary

  • Do not use == null inside an overloaded == operator.
  • Use ReferenceEquals to perform null and identity checks safely.
  • Keep ==, !=, Equals, and GetHashCode consistent with one equality definition.
  • Implement IEquatable<T> for strongly typed equality logic.
  • Prefer records or simpler built-in semantics when custom operator logic is unnecessary.

Course illustration
Course illustration

All Rights Reserved.