Indexers & Operator Overloading

10 questions found

An indexer is a special property-like member defined with 'this[...]' syntax, letting instances of your class be accessed using square brackets (obj[key]) just like an array or dictionary — you define get/set accessors just as with a normal property.
public class WeekSchedule {
  private readonly string[] _days = new string[7];
  public string this[int dayIndex] {
    get => _days[dayIndex];
    set => _days[dayIndex] = value;
  }
}
var schedule = new WeekSchedule();
schedule[0] = "Meeting";
Console.WriteLine(schedule[0]); // "Meeting"
Real-world example Building a custom collection-like class (e.g., a Matrix or Grid) that should support natural array-style access syntax.

Common follow-ups: Can an indexer accept a non-integer parameter type, like a string, the way Dictionary does?

Collections
Declare a 'public static' method named 'operator +' taking two parameters of your type (or your type and another compatible type), returning the result — the compiler then lets you use the normal '+' syntax directly on instances of your type.
public struct Money {
  public decimal Amount;
  public static Money operator +(Money a, Money b) => new Money { Amount = a.Amount + b.Amount };
}
Money total = new Money { Amount = 10 } + new Money { Amount = 5 };
Console.WriteLine(total.Amount); // 15
Real-world example Allowing two Money, Vector, or Complex number instances to be combined naturally with the '+' operator instead of a named method.

Common follow-ups: Why must operator overloads always be declared as 'public static' members?

Structs Boxing & Unboxing
You can overload most unary and binary operators (+, -, *, /, ==, !=, <, >, etc.), true/false, and implicit/explicit conversion operators — but && and || specifically CANNOT be overloaded directly (they're derived automatically from overloading & and | plus true/false operators), and assignment operators like = can never be overloaded at all.
public static Vector operator +(Vector a, Vector b) => new(a.X + b.X, a.Y + b.Y); // OK
public static Vector operator *(Vector v, double scalar) => new(v.X * scalar, v.Y * scalar); // OK
// public static Vector operator &&(Vector a, Vector b) { } // NOT directly overloadable
Real-world example Deciding which operators genuinely make semantic sense to overload for a custom Vector, Money, or Complex number type.

Common follow-ups: How would you make a custom type work correctly with '&&' indirectly, given it can't be overloaded directly?

OOP
Overload '<' and '>' as static operator methods, and their logic must be CONSISTENT with any overloaded '==' and Equals() — specifically, if a.Equals(b) is true, then neither a < b nor a > b should also be true, maintaining a coherent total ordering that doesn't contradict equality.
public struct Money : IComparable<Money> {
  public decimal Amount;
  public static bool operator <(Money a, Money b) => a.Amount < b.Amount;
  public static bool operator >(Money a, Money b) => a.Amount > b.Amount;
  public int CompareTo(Money other) => Amount.CompareTo(other.Amount); // must agree with < and >
}
Real-world example Allowing natural comparison syntax (a < b) for a Money or custom numeric type used in sorting and range checks.

Common follow-ups: If you overload '<', does C# require you to also overload '>' — and what other operator pairing rules exist?

Equality: Equals GetHashCode & IEquatable
'implicit operator' lets the compiler AUTOMATICALLY convert between types without an explicit cast (used when the conversion is always safe and lossless); 'explicit operator' requires the caller to write an explicit cast, appropriate when the conversion could lose information or fail — this is exactly how int implicitly converts to double, but double requires an explicit cast down to int.
public struct Fahrenheit {
  public double Degrees;
  public static implicit operator Fahrenheit(double degrees) => new Fahrenheit { Degrees = degrees };
  public static explicit operator double(Fahrenheit f) => f.Degrees; // requires (double)fahrenheitValue
}
Fahrenheit temp = 98.6; // implicit conversion from double
double raw = (double)temp; // explicit conversion required
Real-world example Allowing a domain-specific value type (like Fahrenheit or Money) to convert naturally to/from a primitive type where appropriate.

Common follow-ups: Why should a LOSSY or potentially-failing conversion always be marked 'explicit' rather than 'implicit'?

Value vs Reference Types
Declare the indexer with multiple parameters separated by commas inside 'this[...]', exactly like a multi-parameter method — this lets your custom type support natural 2D (or higher-dimensional) index access syntax matching a built-in rectangular array's [,] syntax.
public class Matrix {
  private readonly double[,] _data;
  public Matrix(int rows, int cols) { _data = new double[rows, cols]; }
  public double this[int row, int col] {
    get => _data[row, col];
    set => _data[row, col] = value;
  }
}
var m = new Matrix(3, 3);
m[0, 1] = 5.0;
Real-world example Building a custom Matrix, Grid, or 2D game board class that supports natural [row, col] access syntax.

Common follow-ups: How would you additionally support a RANGE-based indexer, like matrix[0..2, ..] for slicing?

Arrays Span<T> & Memory<T>
Overloading 'true' and 'false' as static operators lets instances of your type be evaluated directly in boolean contexts (if, while, &&, ||) even though your type ISN'T actually 'bool' — the compiler calls your 'true' operator implementation wherever it needs to test the truthiness of your custom type.
public struct OptionalValue {
  public bool HasValue;
  public static bool operator true(OptionalValue v) => v.HasValue;
  public static bool operator false(OptionalValue v) => !v.HasValue;
}
OptionalValue opt = new OptionalValue { HasValue = true };
if (opt) { Console.WriteLine("Has a value!"); } // works, thanks to the true/false operators
Real-world example Building a custom nullable-like or Result-like value type that can be tested directly in conditional expressions without an explicit .HasValue check.

Common follow-ups: Why must you overload BOTH true and false together, and never just one alone?

Nullable Reference Types
Since Vector3 is typically implemented as a struct for value-semantics and performance, EVERY operator overload should return a NEW Vector3 instance (preserving immutability) rather than mutating either input — and because structs are copied by value, operator overloads on small structs are generally cheap, avoiding heap allocation entirely, unlike an equivalent class-based implementation.
public readonly struct Vector3 {
  public readonly double X, Y, Z;
  public Vector3(double x, double y, double z) { X = x; Y = y; Z = z; }
  public static Vector3 operator +(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
  public static Vector3 operator *(Vector3 v, double scalar) => new(v.X * scalar, v.Y * scalar, v.Z * scalar);
}
Vector3 result = (v1 + v2) * 2.0; // fully chainable, zero heap allocation
Real-world example Building a high-performance, allocation-free 3D vector math library for a game engine or physics simulation.

Common follow-ups: Why is 'readonly struct' specifically recommended here, beyond just 'struct'?

Structs Boxing & Unboxing
C# 11 lets you define a SEPARATE overload of an operator specifically for 'checked' contexts (using 'checked operator +' syntax) alongside the normal unchecked version — the compiler automatically calls the checked variant when the surrounding code is in a 'checked' block/expression, letting custom numeric types participate correctly in C#'s overflow-checking semantics.
public struct SafeInt {
  public int Value;
  public static SafeInt operator +(SafeInt a, SafeInt b) => new SafeInt { Value = a.Value + b.Value }; // unchecked
  public static SafeInt operator checked +(SafeInt a, SafeInt b) =>
    new SafeInt { Value = checked(a.Value + b.Value) }; // throws OverflowException if it overflows
}
Real-world example Building a custom numeric type that correctly respects C#'s checked/unchecked overflow-detection context, matching built-in numeric type behavior.

Common follow-ups: In what scenario would the compiler automatically choose the checked overload over the regular one?

Exception Handling
Combine an indexer (for direct positional access via []) with an implementation of IEnumerable<T> (via GetEnumerator(), often using 'yield return') to give your custom collection BOTH capabilities simultaneously — letting consumers use foreach AND direct index access naturally, just like a built-in List<T>.
public class CircularBuffer<T> : IEnumerable<T> {
  private readonly T[] _items;
  public T this[int index] => _items[index % _items.Length]; // wraps around
  public CircularBuffer(int size) { _items = new T[size]; }
  public IEnumerator<T> GetEnumerator() {
    foreach (var item in _items) yield return item;
  }
  IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Real-world example Building a custom circular buffer, sparse array, or specialized collection supporting both foreach iteration and direct indexed access.

Common follow-ups: What's the difference between implementing the generic IEnumerable<T> versus only the non-generic IEnumerable?

Iterators & yield return