Indexers and operator overloading let your own custom classes behave like built-in C# types, supporting [] access and operators like +, ==, and < directly.
This collection of 15 C# exercises covers building indexers for custom collection-like classes and overloading operators to give your types clean, natural syntax.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you learn to design intuitive, expressive custom types.
+ Table Of Contents (15 Exercises)
Table of contents
- Exercise 1: Look Up a Day of the Week by Name
- Exercise 2: Build a Bounds-Checked Safe Array
- Exercise 3: Access Individual Bits with an Indexer
- Exercise 4: Build a Spreadsheet-Style Grid
- Exercise 5: Find an Employee by Multiple Keys
- Exercise 6: Overload Operators for 2D Vector Math
- Exercise 7: Build a Self-Reducing Fraction Calculator
- Exercise 8: Add and Subtract Time Durations
- Exercise 9: Compare Money Values Safely by Currency
- Exercise 10: Overload the Multiplication Operator for Strings
- Exercise 11: Multiply Two Matrices with a 2D Indexer
- Exercise 12: Convert Between Double and a Custom Distance Type
- Exercise 13: Build a Polynomial with an Indexer for Coefficients
- Exercise 14: Overload & and | for Short-Circuit Boolean Logic
- Exercise 15: Combine an Indexer and Operator Overloading in a Ledger
Exercise 1: Look Up a Day of the Week by Name
Practice Problem: Create a Week class that contains an array of the 7 days of the week. Implement a string-based indexer so that week["Monday"] returns 1, week["Tuesday"] returns 2, and so on. Return -1 if an invalid day name is passed.
Purpose: This exercise helps you practice writing a basic string-keyed indexer, letting instances of your own class be accessed with square bracket syntax just like a built-in array or dictionary.
Given Input: week["Monday"], week["Tuesday"], week["Funday"]
Expected Output:
week["Monday"] = 1 week["Tuesday"] = 2 week["Funday"] = -1
▼ Hint
- Store the days in an array starting with Sunday at index 0, so Monday naturally lands on index 1 and Tuesday on index 2.
- Declare the indexer as
public int this[string dayName], searching the array inside thegetaccessor and returning-1if no match is found.
▼ Solution & Explanation
Explanation:
public int this[string dayName]: Thethiskeyword followed by a parameter list in square brackets is what defines an indexer, lettingweek["Monday"]work just like accessing an array element.days[i] == dayName: Compares each stored day name against the requested one, returning its position the moment a match is found.return -1;: Falls through to this line only if the loop finishes without finding a match, signaling that the day name was not recognized.
Exercise 2: Build a Bounds-Checked Safe Array
Practice Problem: Build a SafeArray class that wraps a standard integer array. Implement a standard integer indexer, but add bounds-checking. If the user tries to access an index out of bounds (e.g., array[100]), instead of crashing with an exception, return a default value of 0 on get, and do nothing (or resize) on set.
Purpose: This exercise helps you practice writing both a get and a set accessor on the same indexer, each with their own bounds-checking logic instead of relying on the array’s own behavior.
Given Input: A SafeArray of size 5, with array[2] = 42, then access and write attempts at index 100.
Expected Output:
array[2] = 42 array[100] = 0 array[100] after out-of-bounds set = 0
▼ Hint
- In the
getaccessor, check if the index falls outside0toitems.Length - 1and return0immediately if so, before ever touching the underlying array. - In the
setaccessor, only write to the underlying array when the index is within bounds, silently ignoring the assignment otherwise.
▼ Solution & Explanation
Explanation:
if (index < 0 || index >= items.Length) return 0;: Guards thegetaccessor, sidestepping the underlying array entirely whenever the requested index would normally throw anIndexOutOfRangeException.valuekeyword inset: Refers to whatever value is being assigned, for example999inarray[100] = 999;, and here it is simply discarded when the index is invalid.- Silent failure by design: Both accessors deliberately avoid throwing, trading strict correctness for a class that can never crash the caller no matter what index is used.
Exercise 3: Access Individual Bits with an Indexer
Practice Problem: Create a BitCompact struct that wraps a single int value (32 bits). Implement an indexer public bool this[int bitIndex] that allows you to get or set individual bits (true for 1, false for 0) using bitwise operators.
Purpose: This exercise helps you practice combining an indexer with bitwise operators, letting individual bits of a packed integer be read and written as if they were separate boolean values.
Given Input: Starting from 0, set bit 0 and bit 3 to true.
Expected Output:
bits[0] = True bits[1] = False bits[3] = True Underlying Value = 9
▼ Hint
- Name the backing field something other than
value, since the indexer’ssetaccessor already usesvalueas its implicit parameter name. - To read a bit, use
(rawBits & (1 << bitIndex)) != 0. To set a bit on, userawBits |= (1 << bitIndex), and to set it off, userawBits &= ~(1 << bitIndex).
▼ Solution & Explanation
Explanation:
(rawBits & (1 << bitIndex)) != 0: Shifts a single 1 bit into the target position, then uses&to isolate that one bit fromrawBits, resulting in true only if it was set.rawBits |= (1 << bitIndex): Turns the target bit on, leaving every other bit unchanged, since OR-ing with 0 never alters a bit’s existing value.rawBits &= ~(1 << bitIndex): Turns the target bit off by AND-ing with a mask that has every bit set except the one being cleared.
Exercise 4: Build a Spreadsheet-Style Grid
Practice Problem: Create a Grid class that represents a 2D table of strings. Implement a two-dimensional indexer public string this[int row, int col] to get or set values. Add an overload that allows indexing via a string Excel-style coordinate, like grid["A", 5].
Purpose: This exercise helps you practice defining multiple indexers with different parameter signatures on the same class, similar to how methods can be overloaded.
Given Input: grid[0, 0] = "Name", then grid["A", 1] = "Alice"
Expected Output:
grid[0, 0] = Name grid["A", 1] = Alice grid[1, 0] via numeric indexer = Alice
▼ Hint
- Back the grid with a two-dimensional array,
string[,], and let the numeric indexer read and write directly into it. - For the string-based overload, convert the column letter to a numeric index with
columnLetter[0] - 'A', then delegate to the numeric indexer to avoid duplicating logic.
▼ Solution & Explanation
Explanation:
- Two indexer overloads:
this[int row, int col]andthis[string columnLetter, int row]coexist on the same class, and the compiler picks the correct one based on the argument types used at the call site. columnLetter[0] - 'A': Converts a letter like'A'into a zero based column number by subtracting the character code of'A'itself.return this[row, columnIndex];: The string-based overload delegates to the numeric indexer rather than duplicating the array access logic, keeping a single source of truth.
Exercise 5: Find an Employee by Multiple Keys
Practice Problem: Create an EmployeeRegistry class containing a list of Employee objects (each having an ID, Name, and Email). Implement overloaded indexers so an employee can be retrieved by their int ID, their string Name, or their string Email.
Purpose: This exercise helps you practice designing a class that can be looked up in several natural ways, using the parameter type alone to decide which lookup strategy applies.
Given Input: Two employees, Alice (ID 1) and Bob (ID 2), looked up by ID, name, and email.
Expected Output:
registry[1].Name = Alice registry["Bob"].Email = bob@example.com registry["alice@example.com"].Name = Alice
▼ Hint
- Add a
this[int id]indexer that filters onId, and a separatethis[string nameOrEmail]indexer that checks bothNameandEmailin a single condition. - Use LINQ’s
FirstOrDefault()inside each indexer to return the first matching employee, ornullif none is found.
▼ Solution & Explanation
Explanation:
this[int id]vsthis[string nameOrEmail]: The compiler chooses which indexer to use purely based on whether the caller passes anintor astringargument.e.Name == nameOrEmail || e.Email == nameOrEmail: A single condition covers both possible meanings of the string argument, whether it turns out to be a name or an email address.FirstOrDefault(): Returnsnullgracefully if no employee matches, rather than throwing when the requested key does not exist.
Exercise 6: Overload Operators for 2D Vector Math
Practice Problem: Create a Vector2D struct with X and Y properties. Overload the + and - operators to allow vector addition and subtraction. Also, overload the * operator to allow a vector to be multiplied by a scalar double value.
Purpose: This exercise helps you practice the basic syntax for operator overloading, defining what familiar symbols like +, -, and * mean for your own custom type.
Given Input: a = (2, 3), b = (4, 1)
Expected Output:
a + b = (6, 4) a - b = (-2, 2) a * 2.5 = (5, 7.5)
▼ Hint
- Operator overload declarations use the pattern
public static Vector2D operator +(Vector2D a, Vector2D b), always asstaticmethods. - For the scalar multiplication overload, the second parameter should be typed
doubleinstead ofVector2D, since it multiplies a vector by a plain number rather than another vector.
▼ Solution & Explanation
Explanation:
public static Vector2D operator +(Vector2D a, Vector2D b): Defines what the+symbol means whenever it appears between twoVector2Dvalues, here adding theirXandYcomponents separately.operator *(Vector2D vector, double scalar): The two operand types do not have to match, allowing a vector to be scaled by a plaindoublerather than another vector.override string ToString(): Provides a readable representation so that string interpolation like$"{sum}"prints the vector’s components instead of the type’s default name.
Exercise 7: Build a Self-Reducing Fraction Calculator
Practice Problem: Create a Fraction class with Numerator and Denominator properties. Overload +, -, *, and / to perform precise fractional arithmetic. Ensure the resulting fraction is automatically reduced to its simplest form (e.g., 2/4 becomes 1/2).
Purpose: This exercise helps you practice overloading all four arithmetic operators together, plus baking a normalization step directly into the constructor so every fraction is automatically kept in its simplest form.
Given Input: half = 1/2, twoFourths = 2/4, third = 1/3
Expected Output:
2/4 reduced automatically = 1/2 1/2 + 1/3 = 5/6 1/2 - 1/3 = 1/6 1/2 * 1/3 = 1/6 1/2 / 1/3 = 3/2
▼ Hint
- Write a private
GreatestCommonDivisor()helper using the Euclidean algorithm, then divide both the numerator and denominator by it inside the constructor. - Use the standard cross-multiplication formulas for each operator: addition and subtraction need a common denominator, multiplication multiplies straight across, and division multiplies by the reciprocal.
▼ Solution & Explanation
Explanation:
- Reduction in the constructor: Every
Fractionis divided by its greatest common divisor the moment it is created, so2/4is stored internally as1/2right from construction, never as the unreduced form. a.Numerator * b.Denominator + b.Numerator * a.Denominator: The standard cross-multiplication formula for adding fractions without needing a shared denominator up front.- Division as reciprocal multiplication:
a / bis implemented asa.Numerator * b.Denominatorovera.Denominator * b.Numerator, which is mathematically the same as multiplying byb‘s reciprocal.
Exercise 8: Add and Subtract Time Durations
Practice Problem: Create a Duration class representing hours and minutes. Overload the + and - operators to add or subtract durations. Ensure that if minutes exceed 59, they roll over correctly into hours (e.g., 1h 45m + 0h 30m = 2h 15m).
Purpose: This exercise helps you practice normalizing a value inside the constructor itself, so the roll-over logic only needs to be written once instead of being repeated in every operator.
Given Input: first = 1h 45m, second = 0h 30m
Expected Output: 1h 45m + 0h 30m = 2h 15m
▼ Hint
- In the constructor, compute
Hours = hours + minutes / 60andMinutes = minutes % 60, so any excess minutes automatically fold into whole hours. - Inside each operator, convert both durations entirely into total minutes first, combine them, and pass the result back into the constructor with
0hours so the normalization logic runs again automatically.
▼ Solution & Explanation
Explanation:
Hours = hours + minutes / 60; Minutes = minutes % 60;: Normalizes any raw minute count into a proper hours-and-minutes pair the moment aDurationis constructed.(a.Hours * 60 + a.Minutes) + (b.Hours * 60 + b.Minutes): Flattens both durations into a single total minute count before combining them, which sidesteps having to handle carrying manually inside the operator itself.new Duration(0, totalMinutes): Passing the combined total back through the constructor reuses the same normalization logic automatically, converting 105 total minutes into 1h 45m without any extra code.
Exercise 9: Compare Money Values Safely by Currency
Practice Problem: Create a Money struct with Amount (decimal) and Currency (string, e.g., “USD”). Overload the ==, !=, <, and > operators. The comparison operators should throw an exception if the two Money objects being compared have different currencies.
Purpose: This exercise helps you practice overloading comparison operators that include their own validation logic, guarding against a comparison that would otherwise be mathematically meaningless.
Given Input: tenDollars = 10 USD, twentyDollars = 20 USD, tenEuros = 10 EUR
Expected Output:
10 USD < 20 USD = True 10 USD == 10 USD = True Error: Cannot compare USD with EUR.
▼ Hint
- Write a small private helper method that throws an exception whenever the two
Currencyvalues differ, and call it at the start of every comparison operator. - Whenever
==and!=are overloaded, also overrideEquals()andGetHashCode()on the struct, since the compiler expects the two to stay consistent with each other.
▼ Solution & Explanation
Explanation:
EnsureSameCurrency(a, b): Called at the top of every comparison operator, throwing immediately if the two currencies do not match, before any actual amount comparison happens.- Operators must be paired: C# requires
==and!=to be overloaded together, and the same applies to<and>, since defining one without the other is a compile error. override bool Equals(object obj)andGetHashCode(): Kept consistent with the overloaded==operator, which the compiler expects whenever equality is customized on a type.
Exercise 10: Overload the Multiplication Operator for Strings
Practice Problem: Create a SmartString wrapper class. Overload the * operator between a SmartString and an int so that new SmartString("Hi") * 3 results in a new SmartString containing "HiHiHi".
Purpose: This exercise helps you practice giving an operator a meaning that has nothing to do with its usual mathematical interpretation, repurposing * as “repeat” for a text based type.
Given Input: greeting = new SmartString("Hi"), count = 3
Expected Output: Repeated = HiHiHi
▼ Hint
- Declare the operator as
public static SmartString operator *(SmartString text, int count), taking aSmartStringas the left operand and anintas the right operand. - Use a
StringBuilderinside a loop that runscounttimes, appending the original text on each pass, then wrap the final result in a newSmartString.
▼ Solution & Explanation
Explanation:
operator *(SmartString text, int count): Since the operand types areSmartStringandintrather than two of the same type, this overload only applies to multiplying aSmartStringby a whole number, in that specific order.StringBuilderinside a loop: Appends the original textcounttimes, which is more efficient than repeatedly concatenating strings directly in a loop.greeting * 3: Reads naturally at the call site, even though multiplying text by a number has no meaning in ordinary arithmetic, it is entirely up to the overload to define what makes sense here.
Exercise 11: Multiply Two Matrices with a 2D Indexer
Practice Problem: Create a Matrix class (a mathematical grid of numbers). Implement a 2D indexer to access elements at matrix[row, col]. Then, overload the * operator to implement true mathematical matrix multiplication between two Matrix objects.
Purpose: This exercise helps you practice combining a 2D indexer with a genuinely non-trivial overloaded operator, where * means real matrix multiplication rather than simple element-by-element multiplication.
Given Input: a = [[1, 2], [3, 4]], b = [[5, 6], [7, 8]]
Expected Output:
Result of a * b: 19 22 43 50
▼ Hint
- Back the class with a
double[,]array, exposed throughpublic double this[int row, int col]. - For multiplication, each resulting cell is the sum of products between a row from the first matrix and a column from the second, requiring three nested loops in total.
- Matrix multiplication only works when the first matrix’s column count matches the second matrix’s row count, worth checking before the loops even start.
▼ Solution & Explanation
Explanation:
- Three nested loops: The outer two loops walk through every cell of the result matrix, while the innermost loop sums the products needed to fill that one cell, following the standard row-times-column rule of matrix multiplication.
if (a.Columns != b.Rows): Validates the dimension rule for matrix multiplication before doing any work, since a mismatch would otherwise produce meaningless results or an index error.a[row, k] * b[k, col]: Both operands are read entirely through the 2D indexer, showing how indexers and operator overloading can work together inside the same method.
Exercise 12: Convert Between Double and a Custom Distance Type
Practice Problem: Create a Distance class that stores values in meters. Implement an implicit conversion operator from double to Distance (assuming the double is in meters). Then, implement an explicit conversion operator from Distance to string that formats the output nicely (e.g., "1500m").
Purpose: This exercise helps you practice the difference between implicit and explicit conversion operators, and when each is the appropriate choice for a custom type.
Given Input: double meters = 1500.0
Expected Output: Formatted = 1500m
▼ Hint
- Declare the double-to-Distance conversion as
public static implicit operator Distance(double meters), allowing a plaindoubleto be assigned directly to aDistancevariable with no cast needed. - Declare the Distance-to-string conversion as
public static explicit operator string(Distance distance), requiring the caller to write an explicit(string)cast.
▼ Solution & Explanation
Explanation:
Distance distance = 1500.0;: This assignment compiles with no cast at all, since the implicit operator tells the compiler exactly how to turn a rawdoubleinto aDistanceautomatically.(string)distance: The conversion tostringrequires an explicit cast, signaling to anyone reading the code that this conversion involves formatting or interpretation, not just a lossless type change.- When to choose which: Implicit conversions should be safe and lossless, like wrapping a raw number into a meaningful type, while explicit conversions are appropriate whenever the conversion could lose information or needs deliberate intent from the caller, like formatting into text.
Exercise 13: Build a Polynomial with an Indexer for Coefficients
Practice Problem: Create a Polynomial class that represents an algebraic polynomial (like 3x^2 + 2x + 5). Use an indexer where the index represents the power of x (so poly[2] = 3 sets the coefficient of x^2). Overload the + operator to add two polynomials together by summing their like-termed coefficients.
Purpose: This exercise helps you practice using an indexer where the index has a mathematical meaning rather than representing a physical storage position, backed internally by a dictionary instead of an array.
Given Input: first = 3x^2 + 2x + 5, second = 1x^2 + 4
Expected Output:
First = 3x^2 + 2x + 5 Second = 1x^2 + 4 Sum = 4x^2 + 2x + 9
▼ Hint
- Store coefficients in a
Dictionary<int, double>keyed by power, letting the indexer’sgetreturn0for any power that was never explicitly set. - For addition, collect every power that appears in either polynomial using
.Keys.Union(), then set the result’s coefficient at each power to the sum of both inputs at that same power.
▼ Solution & Explanation
Explanation:
coefficients.TryGetValue(power, out double value) ? value : 0: Treats any power that was never assigned as having a coefficient of0, which is exactly how a real polynomial behaves for missing terms.a.coefficients.Keys.Union(b.coefficients.Keys): Produces the full set of powers present in either polynomial, so the addition correctly accounts for powers that exist in only one of the two operands.result[power] = a[power] + b[power];: For each power, sums the coefficients from both polynomials, relying on the indexer’sgetto safely return 0 for any power missing from one side.
Exercise 14: Overload & and | for Short-Circuit Boolean Logic
Practice Problem: Create a Criteria class that holds a set of evaluation rules. Overload the conditional logical operators & and | (and consequently true and false operators, which C# requires for short-circuiting evaluation) so that users can combine multiple Criteria objects together seamlessly using && and ||.
Purpose: This exercise helps you practice one of the more obscure corners of operator overloading: to make && and || work on a custom type, you cannot overload them directly, instead you overload &, |, true, and false, and the compiler combines them to produce genuine short-circuiting behavior.
Given Input: isAdult = true, hasLicense = false, isBanned = false, hasValidPayment = true
Expected Output:
isAdult & hasLicense = (IsAdult AND HasLicense) = False isAdult | hasLicense = (IsAdult OR HasLicense) = True Access denied (short-circuited: IsBanned was already false, so HasValidPayment's AND combination was never evaluated).
▼ Hint
- Overload
operator &andoperator |as regular binary operators returning a new combinedCriteria. - Overload
operator trueandoperator falseas the two special unary operators the compiler needs, both taking a singleCriteriaand returning abool. - For
x && y, the compiler effectively evaluatesoperator false(x) ? x : (x & y), meaningyand the&operator are skipped entirely wheneverxis already false.
▼ Solution & Explanation
Explanation:
isAdult & hasLicense: The single-&operator always evaluates both operands and callsoperator &directly, which is why this line reliably produces the combined “(IsAdult AND HasLicense)” result.isBanned && hasValidPayment: SinceisBanned.IsSatisfiedis already false,operator false(isBanned)returns true, so the compiler short-circuits immediately, returningisBanneditself without ever callingoperator &or even touchinghasValidPayment.- Why
true/falseoperators are required: Theifstatement needs to know whether the resultingCriteriaobject should be treated as true or false, and sinceCriteriais not abool, the compiler relies onoperator trueto make that determination.
Exercise 15: Combine an Indexer and Operator Overloading in a Ledger
Practice Problem: Create a Ledger class that tracks financial transactions. Indexer: allow users to look up a transaction by date, ledger[DateTime.Today]. Operator overloading: overload the + operator to easily add a new Transaction object directly to the Ledger (e.g., myLedger += new Transaction(50.00);).
Purpose: This exercise helps you practice combining an indexer and an overloaded operator on the same class, and seeing how += works automatically for free once + is overloaded correctly.
Given Input: myLedger += new Transaction(50.00m), added with today’s date
Expected Output: Transaction on 2026-07-13 = 50.00
Note: since the transaction is recorded against DateTime.Today, the date shown will match whatever day the program is actually run on.
▼ Hint
- Declare the indexer as
public Transaction this[DateTime date], comparing only the.Dateportion of each stored transaction so the time component never causes a mismatch. - Declare the operator as
public static Ledger operator +(Ledger ledger, Transaction transaction), adding the transaction to the ledger’s internal list and then returning the same ledger instance. - Once
+is overloaded this way,myLedger += new Transaction(...)works automatically, since+=is always translated intomyLedger = myLedger + new Transaction(...)by the compiler.
▼ Solution & Explanation
Explanation:
myLedger += new Transaction(50.00m): The compiler rewrites this asmyLedger = myLedger + new Transaction(50.00m), so no separate+=overload is ever needed once+itself is defined.ledger.transactions.Add(transaction); return ledger;: Mutates the sameLedgerinstance and hands it straight back, which is why reassigningmyLedgerto the result of+still points at the exact same object with the new transaction included.t.Date.Date == date.Date: Compares only the calendar date portion of each transaction, so a transaction recorded at any time of day still matches a lookup made against midnight, likeDateTime.Today.

Leave a Reply