📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How does operator overloading work in C#? Explain the rules for declaring operators, which operators come in pairs, equality operators and Equals, conversion operators, checked operators, generic math interfaces, and C# 14 user-defined compound assignment operators.

C# operator overloading: syntax and rules, required pairs, == with Equals/GetHashCode, implicit and explicit conversions, checked operators, generic math, C# 14 compound assignment and design guidelines.

Mid-Level Theory Intermediate conversions csharp csharp-14 equality generic-math oop operator-overloading

🎯 Key points

  • Operators are public static methods named operator X with at least one parameter of the containing type
  • ==/!=, </>, <=/>= and true/false must be overloaded in pairs
  • Overloading == requires consistent Equals, `IEquatable<T>` and GetHashCode; use `is null` inside operators
  • Implicit conversions must never fail or lose data; otherwise use explicit
  • C# 11 added checked operators and static abstract operators for generic math interfaces
  • C# 14 adds instance compound assignment (+=) and ++/-- operators that update in place without allocating
  • Some operators cannot be overloaded: =, ., ?:, ??, new, is, as, and && / || directly
  • Overload only where the meaning is obvious, and provide named methods for discoverability

⏱ 30-second answer

You overload an operator with a public static method named operator + (or ==, <, implicit operator and so on), where at least one parameter is the containing type. Some operators must be overloaded in pairs: ==/!=, </>, <=/>=, true/false. If you overload ==, also override Equals and GetHashCode so all equality paths agree. Conversion operators are implicit (safe, no data loss) or explicit (may fail or lose data). C# 11 added checked operators and static abstract operators in interfaces for generic math (IAdditionOperators<,,>). C# 14 lets you declare compound assignment (+=) and ++/-- as instance operators that update the object in place. Overload only when the meaning is obvious, as with Money, Vector or BigInteger.

🗣 Full answer (senior)

How I would answer this in an interview

Operator overloading lets a type define what operators like +, == or < mean for its instances. It's valuable for value-like types where the operator meaning is obvious (Money + Money, Vector * scalar, Duration < Duration) and harmful anywhere the meaning would surprise. I'd cover the declaration rules, paired operators, equality, conversions, checked operators, generic math, and C# 14's compound assignment operators.

1. Declaration rules

public readonly record struct Money(decimal Amount, string Currency)
{
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency) throw new InvalidOperationException("Currency mismatch");
        return new(a.Amount + b.Amount, a.Currency);
    }
    public static Money operator -(Money a) => a with { Amount = -a.Amount };   // unary
    public static Money operator *(Money a, decimal factor) => a with { Amount = a.Amount * factor };
    public static bool operator <(Money a, Money b) => Compare(a, b) < 0;
    public static bool operator >(Money a, Money b) => Compare(a, b) > 0;
    private static int Compare(Money a, Money b) { /* currency check */ return a.Amount.CompareTo(b.Amount); }
}
  • Operators are public static methods using the operator keyword.
  • At least one parameter must be the containing type (you can't redefine int + int).
  • You can overload unary (+ - ! ~ ++ -- true false), binary arithmetic/bitwise/shift (+ - * / % & | ^ << >> >>>), comparison (== != < > <= >=) and conversion operators.
  • You can't overload &&, || (derived from &/| plus true/false), =, ., ?:, ??, =>, new, is, as, typeof; and compound assignment like += historically was derived from + (until C# 14 — see below).
  • Overloads are resolved at compile time from static types (operators are not virtual).

2. Operators that must come in pairs

The compiler requires these pairs: ==/!=, </>, <=/>=, and true/false. Defining one without its partner is an error (CS0216).

3. Equality operators and Equals

If you overload ==/!=, you must keep them consistent with Equals(object), IEquatable<T>.Equals and GetHashCode (the compiler warns CS0660/CS0661). Typical pattern:

public static bool operator ==(Sku? a, Sku? b) => a?.Equals(b) ?? b is null;
public static bool operator !=(Sku? a, Sku? b) => !(a == b);

Records generate all of these consistently, which is one reason to use them for value types. Overloading == on mutable reference types is usually a mistake — developers expect reference equality from == on classes.

4. Conversion operators

public readonly record struct Abn(string Value)
{
    public static explicit operator Abn(string s) => Parse(s);     // may fail -> explicit
    public static implicit operator string(Abn abn) => abn.Value;  // always safe -> implicit
}

Rules: implicit only when the conversion can't fail and loses no information; otherwise explicit. Too many implicit conversions make overload resolution confusing and hide bugs (silently converting an OrderId to a Guid defeats strongly typed IDs).

5. Checked operators (C# 11)

public static Quantity operator +(Quantity a, Quantity b) => new(unchecked(a.Value + b.Value));
public static Quantity operator checked +(Quantity a, Quantity b) => new(checked(a.Value + b.Value));

In a checked context the checked version is used, so user types can participate in overflow checking like built-in numeric types.

6. Generic math interfaces (.NET 7)

Operators can be static abstract interface members, so generic code can use them:

public readonly record struct Money(decimal Amount) :
    IAdditionOperators<Money, Money, Money>, IAdditiveIdentity<Money, Money>
{
    public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
    public static Money AdditiveIdentity => new(0m);
}

static T Sum<T>(IEnumerable<T> items) where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    var total = T.AdditiveIdentity;
    foreach (var i in items) total += i;
    return total;
}

7. C# 14 user-defined compound assignment

Previously a += b always compiled to a = a + b — for reference types that means allocating a new object. C# 14 lets types define instance compound assignment operators (and ++/--) that mutate in place:

public sealed class Accumulator
{
    private decimal _total;
    public void operator +=(decimal value) => _total += value;   // in-place, no new object
}

Useful for large mutable types (big-number buffers, matrices, builders) where creating a new instance per operation is wasteful. If not defined, the old a = a + b lowering still applies.

8. A production example

A finance module used decimal everywhere for amounts, and a bug added an AUD amount to a USD amount in a multi-currency report. Introducing a Money record struct with +/- operators that throw on currency mismatch, comparison operators and no implicit conversion from decimal made such mixing a run-time failure in tests rather than a silent wrong total — and the code still read naturally (subtotal + shipping).

🔍 What the interviewer is checking

Checks rules, consistency and design judgement.

Green flags: public static syntax; required pairs; equality consistency and is null; implicit versus explicit; checked operators; generic math interfaces; C# 14 compound assignment and why it reduces allocations; restraint in choosing when to overload.

Red flags: overloading + on entities; == without GetHashCode; implicit conversions everywhere; unaware that && cannot be overloaded directly.

📚 Learn in depth

1. What operators compile to

a + b with a user-defined operator compiles to a static call Money.op_Addition(a, b). Names: op_Addition, op_Subtraction, op_Multiply, op_Equality, op_Implicit, op_Explicit, op_CheckedAddition (C# 11), op_AdditionAssignment (C# 14 compound), etc. Other .NET languages call these methods by name; tools see them in metadata as specialname methods.

2. Overload resolution for operators

The compiler collects candidate operators from both operand types (and built-in operators), applies normal overload resolution, and picks the best. Because it uses static types:

object a = new Money(1, "AUD"), b = new Money(1, "AUD");
a == b;   // reference comparison on object - user == NOT used (and for structs, boxes)

This is why == overloads don't help when values are typed as object or in unconstrained generic code — use Equals or EqualityComparer<T>.Default.

3. Lifted operators and nullable

User-defined operators on structs are automatically lifted to Nullable<T>: Money? a + Money? b returns null if either is null. Comparison operators lift to return false when either side is null. Keep this in mind for optional amounts.

4. true/false operators and short-circuiting

Defining operator true, operator false and operator &/| allows a type to participate in &&/||: x && y becomes T.false(x) ? x : T.&(x, y). Rarely needed today (historically used for three-valued logic types like SqlBoolean).

5. Generic math hierarchy

Key interfaces in System.Numerics: IAdditionOperators<TSelf, TOther, TResult>, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IComparisonOperators, IEqualityOperators, IUnaryNegationOperators, IIncrementOperators, identity interfaces (IAdditiveIdentity, IMultiplicativeIdentity), and aggregates like INumber<TSelf>. Implementing only what your type supports is better than claiming INumber<T> (which requires many members that may not make sense, e.g. multiplying money by money).

6. Compound assignment lowering (before and after C# 14)

Expression Without user += With C# 14 user += (reference type)
a += b a = a + b (new instance) a.op_AdditionAssignment(b) (mutates)
a++ a = a + 1 via ++ operator returning new value Instance operator ++() mutating in place

For value types, compound operators can also be instance members, mutating the variable in place. Semantic caution: in-place mutation means aliases see changes — appropriate for builder-like types, surprising for value-like types.

7. Design guidelines

  • Overload operators only where meaning is unambiguous and matches mathematical/domain intuition.
  • Provide named method equivalents (Add, Multiply, CompareTo) for languages without operator support and for discoverability (CA2225).
  • Keep operators side-effect free (except compound assignment by design).
  • Ensure ==, Equals and GetHashCode agree; prefer records.
  • Avoid implicit conversions that lose type safety or information.

Try it yourself

  1. Implement Money with +, -, comparison operators and currency checks; write tests mixing currencies.
  2. Compare two Money values typed as object with == and with Equals.
  3. Implement IAdditionOperators and IAdditiveIdentity and use a generic Sum<T>.
  4. Add a C# 14 += operator to a mutable accumulator and confirm no allocations with BenchmarkDotNet.

💻 Code

using System.Numerics;

public readonly record struct Money(decimal Amount, string Currency)
    : IAdditionOperators<Money, Money, Money>,
      IComparisonOperators<Money, Money, bool>
{
    public static Money operator +(Money a, Money b) => new(a.Amount + Same(a, b).Amount, a.Currency);
    public static Money operator -(Money a, Money b) => new(a.Amount - Same(a, b).Amount, a.Currency);
    public static Money operator *(Money a, decimal f) => new(decimal.Round(a.Amount * f, 2), a.Currency);

    public static bool operator <(Money a, Money b)  => a.Amount < Same(a, b).Amount;
    public static bool operator >(Money a, Money b)  => a.Amount > Same(a, b).Amount;
    public static bool operator <=(Money a, Money b) => a.Amount <= Same(a, b).Amount;
    public static bool operator >=(Money a, Money b) => a.Amount >= Same(a, b).Amount;
    // == and != are generated by the record struct

    public static explicit operator decimal(Money m) => m.Amount;   // cast required

    private static Money Same(Money a, Money b) =>
        a.Currency == b.Currency ? b : throw new InvalidOperationException($"{a.Currency} vs {b.Currency}");
}

// C# 14: in-place compound assignment for a large mutable type
public sealed class Histogram(int buckets)
{
    private readonly long[] _counts = new long[buckets];
    public static Histogram operator +(Histogram a, Histogram b)      // still offered: returns new
    {
        var r = new Histogram(a._counts.Length);
        for (int i = 0; i < r._counts.Length; i++) r._counts[i] = a._counts[i] + b._counts[i];
        return r;
    }
    public void operator +=(Histogram other)                          // used by h += other
    {
        for (int i = 0; i < _counts.Length; i++) _counts[i] += other._counts[i];
    }
}

var total = new Money(10m, "AUD") + new Money(5m, "AUD");   // 15 AUD
decimal raw = (decimal)total;                                  // explicit conversion

⚠ Common mistakes

1. Overloading operators with surprising meaning

What happens: + on Order adds a line, << writes to a log. Why it is wrong: readers can't guess the meaning; code becomes cryptic. Do instead: use named methods unless the meaning is obvious and conventional.

2. Inconsistent == and Equals

What happens: == overloaded but Equals/GetHashCode not (or vice versa). Why it is wrong: collections and comparisons disagree. Do instead: implement all consistently or use records.

3. Overloading == on mutable classes

What happens: value equality operators on entity classes. Why it is wrong: developers expect reference semantics; hash codes change with mutation. Do instead: keep reference equality for mutable classes; compare IDs explicitly.

4. Implicit conversions that lose safety

What happens: implicit operator Guid(OrderId id) and implicit operator OrderId(Guid g). Why it is wrong: strongly typed IDs silently convert, allowing mix-ups. Do instead: make conversions explicit (or provide only safe one-way implicit conversions).

5. Implicit conversions that can fail

What happens: implicit operator Abn(string s) that throws on invalid input. Why it is wrong: exceptions from innocent-looking assignments. Do instead: use explicit conversions or Parse/TryParse.

6. Forgetting operators are static

What happens: expecting polymorphic dispatch from operators on base types. Why it is wrong: operators are chosen at compile time from static types. Do instead: delegate operators to virtual methods if polymorphism is needed.

7. Ignoring overflow semantics

What happens: custom numeric types wrap silently in checked contexts. Why it is wrong: inconsistent with built-in numeric behaviour. Do instead: provide checked operators (C# 11) where overflow matters.

8. Mutating compound operators on value-like types

What happens: C# 14 += defined to mutate a type used as a value. Why it is wrong: aliased references see unexpected changes. Do instead: reserve in-place compound operators for builder/buffer types.

🏛 Senior insight: trade-offs

Seniors overload operators to make domain rules impossible to bypass, and only where the meaning is obvious.

Situation Senior choice
Currency-safe arithmetic Money value type with checked operators and explicit conversion to decimal
Generic numeric algorithms Implement generic math interfaces
Large mutable aggregates in hot loops C# 14 instance += plus a binary + for immutable use
Equality on value objects Records (generated) rather than hand-written operators
Domain entities No operators; named methods

They also check overflow intent: provide checked operators for numeric types where overflow must not be silent.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate overloads == Why: Checks Equals and GetHashCode consistency
    How to answer

    System.Object gives every type Equals, GetHashCode, ToString, GetType, the protected MemberwiseClone and Finalize, plus static ReferenceEquals. The review problem is the Equals/GetHashCode contract: equal objects must return equal hash codes. If Equals compares Email but GetHashCode is still identity-based, two "equal" customers land in different hash buckets, so Dictionary, HashSet, Distinct and GroupBy treat them as different. I'd implement IEquatable<Customer>, override both methods using the same immutable fields, compare emails case-insensitively in both, overload ==/!=, and seal the class. For value-like data a record generates all of this.

  2. When: Candidate mentions generic math interfaces Why: Probes static abstract interface members
    How to answer

    An interface is a contract: a set of members a type promises to provide, with no instance state. A class or struct can implement many interfaces. Since C# 8 an interface can carry default implementations (used only through the interface, and they need .NET Core 3.0+), plus static, private and protected members. Since C# 11 it can declare static abstract / static virtual members, which power generic math (INumber<T>, T.Zero). Explicit implementation (void IDisposable.Dispose()) hides a member unless you cast. Calls through an interface use interface dispatch, which the JIT can often devirtualise. Casting a struct to an interface boxes it; a generic constraint (where T : IComparable<T>) avoids the box.

  3. When: Candidate notes records generate == and != Why: Checks what records generate
    How to answer

    A record is a type whose identity is its data. public record Money(decimal Amount, string Currency); makes the compiler generate init-only properties, a constructor, Deconstruct, value-based Equals/GetHashCode/==, a readable ToString, and a hidden clone method used by with expressions for non-destructive mutation. record (or record class, C# 9) is a reference type; record struct (C# 10) is a value type whose positional properties are mutable unless you write readonly record struct. Equality includes an EqualityContract so a base and derived record are never equal. Watch out: with is a shallow copy, collection members compare by reference, and records are a poor fit for EF Core entities, which need identity-based equality.

🛠 Real-world problems: how you handle them

Currency mix-up fixed by a Money type with operators
Situation
A billing engine represented amounts as plain `decimal` with a separate currency string. Multi-currency support was being added for [three] new markets.
Signals
Finance found invoices where USD tax had been added to AUD totals; reconciliation was off by [several thousand] dollars a month.
Investigation
Code search showed amounts and currencies passed separately through [40+] methods; the bug was a mix-up in one discount path.
Resolution
Introduced a `readonly record struct Money` with `+`, `-`, `*` and comparison operators that throw on currency mismatch, and an explicit conversion to `decimal`. Converted the billing code module by module; the compiler flagged [140] mixed uses.
Prevention
Banned raw `decimal` amounts in the billing domain with an architecture test, and added property-based tests (FsCheck) for Money arithmetic.

How to tell it (STAR)

**Situation:** our billing engine sometimes added USD amounts to AUD totals because money was a bare decimal. **Task:** make currency mistakes impossible rather than just fixing one bug. **Action:** I introduced a `Money` value type whose operators check currency, made conversion to decimal explicit, and migrated the billing code with the compiler pointing out every mixed use. **Result:** we found [two] more real currency bugs during migration, and there have been no currency reconciliation errors since.

↩ Usually asked after

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me