PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
    • C# Exercises
  • Quizzes
  • Code Editor ▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
    • Online C# Compiler
Home » C# Exercises » C# Indexers and Operator Overloading Exercises: 15 Coding Problems with Solutions

C# Indexers and Operator Overloading Exercises: 15 Coding Problems with Solutions

Updated on: July 15, 2026 | Leave a Comment

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 the get accessor and returning -1 if no match is found.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    class Week
    {
        private readonly string[] days = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

        public int this[string dayName]
        {
            get
            {
                for (int i = 0; i < days.Length; i++)
                {
                    if (days[i] == dayName)
                    {
                        return i;
                    }
                }

                return -1;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Week week = new Week();

            Console.WriteLine($"week[\"Monday\"] = {week["Monday"]}");
            Console.WriteLine($"week[\"Tuesday\"] = {week["Tuesday"]}");
            Console.WriteLine($"week[\"Funday\"] = {week["Funday"]}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public int this[string dayName]: The this keyword followed by a parameter list in square brackets is what defines an indexer, letting week["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 get accessor, check if the index falls outside 0 to items.Length - 1 and return 0 immediately if so, before ever touching the underlying array.
  • In the set accessor, only write to the underlying array when the index is within bounds, silently ignoring the assignment otherwise.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    class SafeArray
    {
        private readonly int[] items;

        public SafeArray(int size)
        {
            items = new int[size];
        }

        public int this[int index]
        {
            get
            {
                if (index < 0 || index >= items.Length)
                {
                    return 0;
                }

                return items[index];
            }
            set
            {
                if (index >= 0 && index < items.Length)
                {
                    items[index] = value;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SafeArray array = new SafeArray(5);
            array[2] = 42;

            Console.WriteLine($"array[2] = {array[2]}");
            Console.WriteLine($"array[100] = {array[100]}");

            array[100] = 999;
            Console.WriteLine($"array[100] after out-of-bounds set = {array[100]}");
        }
    }
}Code language: C# (cs)

Explanation:

  • if (index < 0 || index >= items.Length) return 0;: Guards the get accessor, sidestepping the underlying array entirely whenever the requested index would normally throw an IndexOutOfRangeException.
  • value keyword in set: Refers to whatever value is being assigned, for example 999 in array[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’s set accessor already uses value as its implicit parameter name.
  • To read a bit, use (rawBits & (1 << bitIndex)) != 0. To set a bit on, use rawBits |= (1 << bitIndex), and to set it off, use rawBits &= ~(1 << bitIndex).
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    struct BitCompact
    {
        private int rawBits;

        public BitCompact(int rawBits)
        {
            this.rawBits = rawBits;
        }

        public bool this[int bitIndex]
        {
            get
            {
                return (rawBits & (1 << bitIndex)) != 0;
            }
            set
            {
                if (value)
                {
                    rawBits |= (1 << bitIndex);
                }
                else
                {
                    rawBits &= ~(1 << bitIndex);
                }
            }
        }

        public int RawBits => rawBits;
    }

    class Program
    {
        static void Main(string[] args)
        {
            BitCompact bits = new BitCompact(0);

            bits[0] = true;
            bits[3] = true;

            Console.WriteLine($"bits[0] = {bits[0]}");
            Console.WriteLine($"bits[1] = {bits[1]}");
            Console.WriteLine($"bits[3] = {bits[3]}");
            Console.WriteLine($"Underlying Value = {bits.RawBits}");
        }
    }
}Code language: C# (cs)

Explanation:

  • (rawBits & (1 << bitIndex)) != 0: Shifts a single 1 bit into the target position, then uses & to isolate that one bit from rawBits, 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
using System;

namespace IndexerOperatorExercises
{
    class Grid
    {
        private readonly string[,] cells;

        public Grid(int rows, int columns)
        {
            cells = new string[rows, columns];
        }

        public string this[int row, int col]
        {
            get { return cells[row, col] ?? ""; }
            set { cells[row, col] = value; }
        }

        public string this[string columnLetter, int row]
        {
            get
            {
                int columnIndex = columnLetter[0] - 'A';
                return this[row, columnIndex];
            }
            set
            {
                int columnIndex = columnLetter[0] - 'A';
                this[row, columnIndex] = value;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Grid grid = new Grid(10, 10);

            grid[0, 0] = "Name";
            grid["A", 1] = "Alice";

            Console.WriteLine($"grid[0, 0] = {grid[0, 0]}");
            Console.WriteLine($"grid[\"A\", 1] = {grid["A", 1]}");
            Console.WriteLine($"grid[1, 0] via numeric indexer = {grid[1, 0]}");
        }
    }
}Code language: C# (cs)

Explanation:

  • Two indexer overloads: this[int row, int col] and this[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 on Id, and a separate this[string nameOrEmail] indexer that checks both Name and Email in a single condition.
  • Use LINQ’s FirstOrDefault() inside each indexer to return the first matching employee, or null if none is found.
▼ Solution & Explanation
using System;
using System.Collections.Generic;
using System.Linq;

namespace IndexerOperatorExercises
{
    class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }

    class EmployeeRegistry
    {
        private readonly List<Employee> employees;

        public EmployeeRegistry(List<Employee> employees)
        {
            this.employees = employees;
        }

        public Employee this[int id]
        {
            get { return employees.FirstOrDefault(e => e.Id == id); }
        }

        public Employee this[string nameOrEmail]
        {
            get { return employees.FirstOrDefault(e => e.Name == nameOrEmail || e.Email == nameOrEmail); }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Employee> employees = new List<Employee>
            {
                new Employee { Id = 1, Name = "Alice", Email = "alice@example.com" },
                new Employee { Id = 2, Name = "Bob", Email = "bob@example.com" }
            };

            EmployeeRegistry registry = new EmployeeRegistry(employees);

            Console.WriteLine($"registry[1].Name = {registry[1].Name}");
            Console.WriteLine($"registry[\"Bob\"].Email = {registry["Bob"].Email}");
            Console.WriteLine($"registry[\"alice@example.com\"].Name = {registry["alice@example.com"].Name}");
        }
    }
}Code language: C# (cs)

Explanation:

  • this[int id] vs this[string nameOrEmail]: The compiler chooses which indexer to use purely based on whether the caller passes an int or a string argument.
  • 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(): Returns null gracefully 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 as static methods.
  • For the scalar multiplication overload, the second parameter should be typed double instead of Vector2D, since it multiplies a vector by a plain number rather than another vector.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    struct Vector2D
    {
        public double X { get; }
        public double Y { get; }

        public Vector2D(double x, double y)
        {
            X = x;
            Y = y;
        }

        public static Vector2D operator +(Vector2D a, Vector2D b)
        {
            return new Vector2D(a.X + b.X, a.Y + b.Y);
        }

        public static Vector2D operator -(Vector2D a, Vector2D b)
        {
            return new Vector2D(a.X - b.X, a.Y - b.Y);
        }

        public static Vector2D operator *(Vector2D vector, double scalar)
        {
            return new Vector2D(vector.X * scalar, vector.Y * scalar);
        }

        public override string ToString()
        {
            return $"({X}, {Y})";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Vector2D a = new Vector2D(2, 3);
            Vector2D b = new Vector2D(4, 1);

            Vector2D sum = a + b;
            Vector2D difference = a - b;
            Vector2D scaled = a * 2.5;

            Console.WriteLine($"a + b = {sum}");
            Console.WriteLine($"a - b = {difference}");
            Console.WriteLine($"a * 2.5 = {scaled}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public static Vector2D operator +(Vector2D a, Vector2D b): Defines what the + symbol means whenever it appears between two Vector2D values, here adding their X and Y components separately.
  • operator *(Vector2D vector, double scalar): The two operand types do not have to match, allowing a vector to be scaled by a plain double rather 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
using System;

namespace IndexerOperatorExercises
{
    class Fraction
    {
        public int Numerator { get; }
        public int Denominator { get; }

        public Fraction(int numerator, int denominator)
        {
            int gcd = GreatestCommonDivisor(Math.Abs(numerator), Math.Abs(denominator));
            gcd = gcd == 0 ? 1 : gcd;

            Numerator = numerator / gcd;
            Denominator = denominator / gcd;
        }

        private static int GreatestCommonDivisor(int a, int b)
        {
            while (b != 0)
            {
                int temp = b;
                b = a % b;
                a = temp;
            }

            return a;
        }

        public static Fraction operator +(Fraction a, Fraction b)
        {
            return new Fraction(a.Numerator * b.Denominator + b.Numerator * a.Denominator, a.Denominator * b.Denominator);
        }

        public static Fraction operator -(Fraction a, Fraction b)
        {
            return new Fraction(a.Numerator * b.Denominator - b.Numerator * a.Denominator, a.Denominator * b.Denominator);
        }

        public static Fraction operator *(Fraction a, Fraction b)
        {
            return new Fraction(a.Numerator * b.Numerator, a.Denominator * b.Denominator);
        }

        public static Fraction operator /(Fraction a, Fraction b)
        {
            return new Fraction(a.Numerator * b.Denominator, a.Denominator * b.Numerator);
        }

        public override string ToString()
        {
            return $"{Numerator}/{Denominator}";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Fraction half = new Fraction(1, 2);
            Fraction twoFourths = new Fraction(2, 4);
            Fraction third = new Fraction(1, 3);

            Console.WriteLine($"2/4 reduced automatically = {twoFourths}");
            Console.WriteLine($"1/2 + 1/3 = {half + third}");
            Console.WriteLine($"1/2 - 1/3 = {half - third}");
            Console.WriteLine($"1/2 * 1/3 = {half * third}");
            Console.WriteLine($"1/2 / 1/3 = {half / third}");
        }
    }
}Code language: C# (cs)

Explanation:

  • Reduction in the constructor: Every Fraction is divided by its greatest common divisor the moment it is created, so 2/4 is stored internally as 1/2 right 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 / b is implemented as a.Numerator * b.Denominator over a.Denominator * b.Numerator, which is mathematically the same as multiplying by b‘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 / 60 and Minutes = 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 0 hours so the normalization logic runs again automatically.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    class Duration
    {
        public int Hours { get; }
        public int Minutes { get; }

        public Duration(int hours, int minutes)
        {
            Hours = hours + minutes / 60;
            Minutes = minutes % 60;
        }

        public static Duration operator +(Duration a, Duration b)
        {
            int totalMinutes = (a.Hours * 60 + a.Minutes) + (b.Hours * 60 + b.Minutes);
            return new Duration(0, totalMinutes);
        }

        public static Duration operator -(Duration a, Duration b)
        {
            int totalMinutes = (a.Hours * 60 + a.Minutes) - (b.Hours * 60 + b.Minutes);
            return new Duration(0, totalMinutes);
        }

        public override string ToString()
        {
            return $"{Hours}h {Minutes}m";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Duration first = new Duration(1, 45);
            Duration second = new Duration(0, 30);

            Duration sum = first + second;

            Console.WriteLine($"{first} + {second} = {sum}");
        }
    }
}Code language: C# (cs)

Explanation:

  • Hours = hours + minutes / 60; Minutes = minutes % 60;: Normalizes any raw minute count into a proper hours-and-minutes pair the moment a Duration is 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 Currency values differ, and call it at the start of every comparison operator.
  • Whenever == and != are overloaded, also override Equals() and GetHashCode() on the struct, since the compiler expects the two to stay consistent with each other.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    struct Money
    {
        public decimal Amount { get; }
        public string Currency { get; }

        public Money(decimal amount, string currency)
        {
            Amount = amount;
            Currency = currency;
        }

        private static void EnsureSameCurrency(Money a, Money b)
        {
            if (a.Currency != b.Currency)
            {
                throw new InvalidOperationException($"Cannot compare {a.Currency} with {b.Currency}.");
            }
        }

        public static bool operator ==(Money a, Money b)
        {
            EnsureSameCurrency(a, b);
            return a.Amount == b.Amount;
        }

        public static bool operator !=(Money a, Money b)
        {
            EnsureSameCurrency(a, b);
            return a.Amount != b.Amount;
        }

        public static bool operator <(Money a, Money b)
        {
            EnsureSameCurrency(a, b);
            return a.Amount < b.Amount;
        }

        public static bool operator >(Money a, Money b)
        {
            EnsureSameCurrency(a, b);
            return a.Amount > b.Amount;
        }

        public override bool Equals(object obj)
        {
            return obj is Money other && Currency == other.Currency && Amount == other.Amount;
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(Amount, Currency);
        }

        public override string ToString()
        {
            return $"{Amount} {Currency}";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Money tenDollars = new Money(10, "USD");
            Money twentyDollars = new Money(20, "USD");
            Money tenEuros = new Money(10, "EUR");

            Console.WriteLine($"{tenDollars} < {twentyDollars} = {tenDollars < twentyDollars}");
            Console.WriteLine($"{tenDollars} == {tenDollars} = {tenDollars == tenDollars}");

            try
            {
                bool result = tenDollars < tenEuros;
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}Code language: C# (cs)

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) and GetHashCode(): 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 a SmartString as the left operand and an int as the right operand.
  • Use a StringBuilder inside a loop that runs count times, appending the original text on each pass, then wrap the final result in a new SmartString.
▼ Solution & Explanation
using System;
using System.Text;

namespace IndexerOperatorExercises
{
    class SmartString
    {
        public string Value { get; }

        public SmartString(string value)
        {
            Value = value;
        }

        public static SmartString operator *(SmartString text, int count)
        {
            StringBuilder builder = new StringBuilder();

            for (int i = 0; i < count; i++)
            {
                builder.Append(text.Value);
            }

            return new SmartString(builder.ToString());
        }

        public override string ToString()
        {
            return Value;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SmartString greeting = new SmartString("Hi");
            SmartString repeated = greeting * 3;

            Console.WriteLine($"Repeated = {repeated}");
        }
    }
}Code language: C# (cs)

Explanation:

  • operator *(SmartString text, int count): Since the operand types are SmartString and int rather than two of the same type, this overload only applies to multiplying a SmartString by a whole number, in that specific order.
  • StringBuilder inside a loop: Appends the original text count times, 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 through public 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
using System;

namespace IndexerOperatorExercises
{
    class Matrix
    {
        private readonly double[,] values;
        public int Rows { get; }
        public int Columns { get; }

        public Matrix(int rows, int columns)
        {
            Rows = rows;
            Columns = columns;
            values = new double[rows, columns];
        }

        public double this[int row, int col]
        {
            get { return values[row, col]; }
            set { values[row, col] = value; }
        }

        public static Matrix operator *(Matrix a, Matrix b)
        {
            if (a.Columns != b.Rows)
            {
                throw new InvalidOperationException("Matrix dimensions do not match for multiplication.");
            }

            Matrix result = new Matrix(a.Rows, b.Columns);

            for (int row = 0; row < a.Rows; row++)
            {
                for (int col = 0; col < b.Columns; col++)
                {
                    double sum = 0;

                    for (int k = 0; k < a.Columns; k++)
                    {
                        sum += a[row, k] * b[k, col];
                    }

                    result[row, col] = sum;
                }
            }

            return result;
        }

        public override string ToString()
        {
            string result = "";

            for (int row = 0; row < Rows; row++)
            {
                for (int col = 0; col < Columns; col++)
                {
                    result += this[row, col] + " ";
                }

                result = result.TrimEnd() + Environment.NewLine;
            }

            return result.TrimEnd();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Matrix a = new Matrix(2, 2);
            a[0, 0] = 1; a[0, 1] = 2;
            a[1, 0] = 3; a[1, 1] = 4;

            Matrix b = new Matrix(2, 2);
            b[0, 0] = 5; b[0, 1] = 6;
            b[1, 0] = 7; b[1, 1] = 8;

            Matrix product = a * b;

            Console.WriteLine("Result of a * b:");
            Console.WriteLine(product);
        }
    }
}Code language: C# (cs)

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 plain double to be assigned directly to a Distance variable 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
using System;

namespace IndexerOperatorExercises
{
    class Distance
    {
        public double Meters { get; }

        public Distance(double meters)
        {
            Meters = meters;
        }

        public static implicit operator Distance(double meters)
        {
            return new Distance(meters);
        }

        public static explicit operator string(Distance distance)
        {
            return $"{distance.Meters}m";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Distance distance = 1500.0;

            string formatted = (string)distance;

            Console.WriteLine($"Formatted = {formatted}");
        }
    }
}Code language: C# (cs)

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 raw double into a Distance automatically.
  • (string)distance: The conversion to string requires 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’s get return 0 for 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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IndexerOperatorExercises
{
    class Polynomial
    {
        private readonly Dictionary<int, double> coefficients = new Dictionary<int, double>();

        public double this[int power]
        {
            get { return coefficients.TryGetValue(power, out double value) ? value : 0; }
            set { coefficients[power] = value; }
        }

        public static Polynomial operator +(Polynomial a, Polynomial b)
        {
            Polynomial result = new Polynomial();
            IEnumerable<int> allPowers = a.coefficients.Keys.Union(b.coefficients.Keys);

            foreach (int power in allPowers)
            {
                result[power] = a[power] + b[power];
            }

            return result;
        }

        public override string ToString()
        {
            List<int> powers = coefficients.Keys.Where(p => coefficients[p] != 0).OrderByDescending(p => p).ToList();

            if (powers.Count == 0)
            {
                return "0";
            }

            StringBuilder builder = new StringBuilder();

            foreach (int power in powers)
            {
                if (builder.Length > 0)
                {
                    builder.Append(" + ");
                }

                if (power == 0)
                {
                    builder.Append(this[power]);
                }
                else if (power == 1)
                {
                    builder.Append($"{this[power]}x");
                }
                else
                {
                    builder.Append($"{this[power]}x^{power}");
                }
            }

            return builder.ToString();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Polynomial first = new Polynomial();
            first[2] = 3;
            first[1] = 2;
            first[0] = 5;

            Polynomial second = new Polynomial();
            second[2] = 1;
            second[0] = 4;

            Polynomial sum = first + second;

            Console.WriteLine($"First = {first}");
            Console.WriteLine($"Second = {second}");
            Console.WriteLine($"Sum = {sum}");
        }
    }
}Code language: C# (cs)

Explanation:

  • coefficients.TryGetValue(power, out double value) ? value : 0: Treats any power that was never assigned as having a coefficient of 0, 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’s get to 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 & and operator | as regular binary operators returning a new combined Criteria.
  • Overload operator true and operator false as the two special unary operators the compiler needs, both taking a single Criteria and returning a bool.
  • For x && y, the compiler effectively evaluates operator false(x) ? x : (x & y), meaning y and the & operator are skipped entirely whenever x is already false.
▼ Solution & Explanation
using System;

namespace IndexerOperatorExercises
{
    class Criteria
    {
        public string Name { get; }
        public bool IsSatisfied { get; }

        public Criteria(string name, bool isSatisfied)
        {
            Name = name;
            IsSatisfied = isSatisfied;
        }

        public static Criteria operator &(Criteria left, Criteria right)
        {
            return new Criteria($"({left.Name} AND {right.Name})", left.IsSatisfied && right.IsSatisfied);
        }

        public static Criteria operator |(Criteria left, Criteria right)
        {
            return new Criteria($"({left.Name} OR {right.Name})", left.IsSatisfied || right.IsSatisfied);
        }

        public static bool operator true(Criteria criteria)
        {
            return criteria.IsSatisfied;
        }

        public static bool operator false(Criteria criteria)
        {
            return !criteria.IsSatisfied;
        }

        public override string ToString()
        {
            return $"{Name} = {IsSatisfied}";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Criteria isAdult = new Criteria("IsAdult", true);
            Criteria hasLicense = new Criteria("HasLicense", false);

            Criteria combinedAnd = isAdult & hasLicense;
            Console.WriteLine($"isAdult & hasLicense = {combinedAnd}");

            Criteria combinedOr = isAdult | hasLicense;
            Console.WriteLine($"isAdult | hasLicense = {combinedOr}");

            Criteria isBanned = new Criteria("IsBanned", false);
            Criteria hasValidPayment = new Criteria("HasValidPayment", true);

            if (isBanned && hasValidPayment)
            {
                Console.WriteLine("Access granted.");
            }
            else
            {
                Console.WriteLine("Access denied (short-circuited: IsBanned was already false, so HasValidPayment's AND combination was never evaluated).");
            }
        }
    }
}Code language: C# (cs)

Explanation:

  • isAdult & hasLicense: The single-& operator always evaluates both operands and calls operator & directly, which is why this line reliably produces the combined “(IsAdult AND HasLicense)” result.
  • isBanned && hasValidPayment: Since isBanned.IsSatisfied is already false, operator false(isBanned) returns true, so the compiler short-circuits immediately, returning isBanned itself without ever calling operator & or even touching hasValidPayment.
  • Why true/false operators are required: The if statement needs to know whether the resulting Criteria object should be treated as true or false, and since Criteria is not a bool, the compiler relies on operator true to 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 .Date portion 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 into myLedger = myLedger + new Transaction(...) by the compiler.
▼ Solution & Explanation
using System;
using System.Collections.Generic;
using System.Linq;

namespace IndexerOperatorExercises
{
    class Transaction
    {
        public DateTime Date { get; }
        public decimal Amount { get; }

        public Transaction(decimal amount) : this(DateTime.Today, amount)
        {
        }

        public Transaction(DateTime date, decimal amount)
        {
            Date = date;
            Amount = amount;
        }
    }

    class Ledger
    {
        private readonly List<Transaction> transactions = new List<Transaction>();

        public Transaction this[DateTime date]
        {
            get { return transactions.FirstOrDefault(t => t.Date.Date == date.Date); }
        }

        public static Ledger operator +(Ledger ledger, Transaction transaction)
        {
            ledger.transactions.Add(transaction);
            return ledger;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Ledger myLedger = new Ledger();

            myLedger += new Transaction(50.00m);

            Transaction found = myLedger[DateTime.Today];

            Console.WriteLine($"Transaction on {DateTime.Today:yyyy-MM-dd} = {found.Amount}");
        }
    }
}Code language: C# (cs)

Explanation:

  • myLedger += new Transaction(50.00m): The compiler rewrites this as myLedger = myLedger + new Transaction(50.00m), so no separate += overload is ever needed once + itself is defined.
  • ledger.transactions.Add(transaction); return ledger;: Mutates the same Ledger instance and hands it straight back, which is why reassigning myLedger to 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, like DateTime.Today.

Filed Under: C# Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

C# Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises
C# Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: C# Exercises
TweetF  sharein  shareP  Pin

  C# Exercises

  • C# Exercise for Beginners
  • C# Loops Exercise
  • C# Array Exercise
  • C# String Exercise
  • C# OOP Exercise
  • C# Structs, Records and Enums Exercise
  • C# Collections Exercise
  • C# List Exercise
  • C# Dictionary Exercise
  • C# LINQ Exercise
  • C# Exception Handling Exercise
  • C# File Handling Exercise
  • C# Date and Time Exercise
  • C# Generics Exercise
  • C# Lambda Expressions Exercise
  • C# Delegates and Events Exercise
  • C# Extension Methods Exercise
  • C# Regex Exercise
  • C# Pattern Matching Exercise
  • C# Iterators and Yield Exercise
  • C# Indexers and Operator Overloading Exercise
  • C# Random Data Generation Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises C# Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises
  • C# Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com