Indexers & Operator Overloading
10 questions found
What is an indexer, and how does it let a custom class support square-bracket syntax like an array?
Beginner
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
How do you overload the '+' operator to support adding two custom objects together?
Beginner
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
Which operators can be overloaded in C#, and which ones (like && and ||) generally CANNOT be overloaded directly?
Intermediate
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
How do you overload comparison operators like < and >, and what consistency rule must they maintain relative to Equals()?
Intermediate
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
How do you define a custom implicit or explicit conversion operator between your type and another type?
Intermediate
'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
How would you implement a multi-dimensional indexer, like a Matrix class supporting matrix[row, col] syntax?
Advanced
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>
How do you overload the true/false operators to make a custom type usable directly in an 'if' statement or as a boolean condition?
Advanced
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
How would you implement a fluent, chainable custom '+' operator overload for building an immutable Vector3 math library, and what performance consideration applies for structs?
Advanced
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
How do you implement the 'checked' operator overload variant (introduced in C# 11) to provide overflow-checking behavior for a custom numeric type?
Advanced
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
How would you use an indexer together with a custom enumerator (implementing IEnumerable) to build a fully iterable, indexable custom collection class?
Advanced
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