C# Indexers
An indexer lets you access elements of a class using square bracket notation — just like accessing elements in an array. It makes your custom classes behave like built-in collections, improving readability and usability.
What Is an Indexer?
┌──────────────────────────────────────────────────────────────┐ │ Without indexer: │ │ Console.WriteLine(classroom.GetStudent(2)); │ │ │ │ With indexer: │ │ Console.WriteLine(classroom[2]); ← array-like syntax │ │ │ │ Built-in types that use indexers: │ │ string s = "hello"; char c = s[1]; → 'e' │ │ List<T> list; int x = list[0]; → first element │ │ Dictionary d; d["key"] = val; → key-value access │ └──────────────────────────────────────────────────────────────┘
Indexer Syntax
Use this[type index] to define an indexer. It uses get and set accessors just like a property.
class Classroom
{
private string[] students = new string[30];
// Indexer definition:
public string this[int index]
{
get { return students[index]; }
set { students[index] = value; }
}
}
class Program
{
static void Main()
{
Classroom room = new Classroom();
room[0] = "Alice"; // calls set accessor
room[1] = "Bob";
room[2] = "Carol";
Console.WriteLine(room[0]); // calls get accessor → Alice
Console.WriteLine(room[2]); // Carol
}
}
Indexer Anatomy Diagram
┌──────────────────────────────────────────────────────────────┐ │ │ │ public string this[int index] │ │ │ │ │ │ │ │ │ │ │ └── index parameter (can be any type) │ │ │ │ └── 'this' = indexer keyword │ │ │ └── return type │ │ └── access modifier │ │ │ └──────────────────────────────────────────────────────────────┘
Indexer with Bounds Checking
class SafeArray
{
private int[] data;
public SafeArray(int size)
{
data = new int[size];
}
public int this[int index]
{
get
{
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index " + index + " is out of range.");
return data[index];
}
set
{
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index " + index + " is out of range.");
data[index] = value;
}
}
public int Length => data.Length;
}
class Program
{
static void Main()
{
SafeArray arr = new SafeArray(5);
arr[0] = 10;
arr[2] = 30;
Console.WriteLine(arr[0]); // 10
Console.WriteLine(arr[2]); // 30
// arr[10] = 99; // throws IndexOutOfRangeException
}
}
String Indexer (Non-Integer Index)
Indexers are not limited to int. A string index works like a dictionary.
class PhoneBook
{
private Dictionary<string, string> contacts = new Dictionary<string, string>();
public string this[string name]
{
get
{
return contacts.ContainsKey(name) ? contacts[name] : "Not found";
}
set
{
contacts[name] = value;
}
}
}
class Program
{
static void Main()
{
PhoneBook book = new PhoneBook();
book["Alice"] = "555-1234";
book["Bob"] = "555-5678";
Console.WriteLine(book["Alice"]); // 555-1234
Console.WriteLine(book["Bob"]); // 555-5678
Console.WriteLine(book["Carol"]); // Not found
}
}
Multi-Dimensional Indexer
Indexers can take multiple parameters — perfect for grid-like structures such as matrices or game boards.
class Grid
{
private int[,] cells;
int rows, cols;
public Grid(int rows, int cols)
{
this.rows = rows;
this.cols = cols;
cells = new int[rows, cols];
}
public int this[int row, int col]
{
get { return cells[row, col]; }
set { cells[row, col] = value; }
}
}
class Program
{
static void Main()
{
Grid board = new Grid(3, 3);
board[0, 0] = 1;
board[1, 1] = 5;
board[2, 2] = 9;
Console.WriteLine(board[1, 1]); // 5
// Display grid:
for (int r = 0; r < 3; r++)
{
for (int c = 0; c < 3; c++)
Console.Write(board[r, c] + " ");
Console.WriteLine();
}
// 1 0 0
// 0 5 0
// 0 0 9
}
}
Multi-Index Diagram
┌──────────────────────────────────────────────────────────────┐ │ board[1, 2] = 7; │ │ │ │ │ │ │ └── col = 2 │ │ └── row = 1 │ │ │ │ Grid: │ │ Col0 Col1 Col2 │ │ Row0 [ 1] [ 0] [ 0] │ │ Row1 [ 0] [ 5] [ 7] ← board[1,2] = 7 │ │ Row2 [ 0] [ 0] [ 9] │ └──────────────────────────────────────────────────────────────┘
Overloading Indexers
A class can have multiple indexers with different parameter types — this is called indexer overloading.
class DataStore
{
private string[] names = { "Alice", "Bob", "Carol" };
private int[] scores = { 90, 85, 92 };
// Access by integer index:
public string this[int index]
{
get { return names[index]; }
}
// Access by string name — returns score:
public int this[string name]
{
get
{
for (int i = 0; i < names.Length; i++)
if (names[i] == name) return scores[i];
return -1;
}
}
}
class Program
{
static void Main()
{
DataStore store = new DataStore();
Console.WriteLine(store[0]); // Alice (int indexer)
Console.WriteLine(store["Bob"]); // 85 (string indexer)
Console.WriteLine(store["Carol"]); // 92
}
}
Read-Only Indexer
Omit the set accessor to create a read-only indexer.
class FibonacciSequence
{
public int this[int n]
{
get
{
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++)
{
int temp = a + b;
a = b;
b = temp;
}
return b;
}
}
// No 'set' → read-only indexer
}
class Program
{
static void Main()
{
FibonacciSequence fib = new FibonacciSequence();
for (int i = 0; i <= 7; i++)
Console.Write(fib[i] + " ");
// 0 1 1 2 3 5 8 13
}
}
Indexer vs Property vs Method
┌───────────────────────────────────────────────────────────────┐ │ Property: obj.Name → single named value │ │ Indexer: obj[key] → value from a collection │ │ Method: obj.Get(key) → same, but explicit call │ │ │ │ Use indexer when your class logically represents a │ │ collection and bracket access feels natural to the caller. │ └───────────────────────────────────────────────────────────────┘
Quick Summary
┌─────────────────────────────────────────────────────────────┐
│ public T this[int i] { get { } set { } } → basic indexer │
│ public T this[string k] { get { } set { }}→ string key │
│ public T this[int r, int c] { ... } → 2D indexer │
│ │
│ Rules: │
│ • Defined with 'this' keyword │
│ • Can have any number/type of parameters │
│ • Can be overloaded (multiple parameter types) │
│ • get and/or set — make read-only by omitting set │
└─────────────────────────────────────────────────────────────┘
Indexers give your custom classes the familiar, intuitive feel of built-in collections. When your class manages a group of items — a grid, a cache, a book of contacts — an indexer makes accessing those items feel completely natural to anyone using your code.
