1. Indexers with Multiple Parameters
A simple indexer (like this[int index]) is handy when you need access to elements by an integer index—almost like an array. But C# lets you do way more: you can use multiple parameters, parameters of different types, set different access modifiers for get and set, and even implement several indexers in one class (as long as their signatures are different).
Also, in newer versions of C#, there are extra features that make working with indexers easier, including some syntactic sugar for cleaner code.
An indexer doesn't have to take just one index or only an int. You define its parameters—and you can have several parameters of different types if that's what your task needs.
public class ChessBoard
{
private string[,] board = new string[8, 8];
// Indexer with two parameters!
public string this[int row, int col]
{
get { return board[row, col]; }
set { board[row, col] = value; }
}
}
Now we can write code like this:
ChessBoard chess = new ChessBoard();
chess[0, 0] = "Rook";
chess[7, 7] = "King";
string piece = chess[0, 0]; // "Rook"
This approach is great for matrices, 2D maps, board games, and complex collections.
2. Indexers with Parameters of Different Types
Your indexer can take parameters not just of type int, but any other type that makes sense for your task.
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Position { get; set; }
}
public class EmployeeCollection
{
private List<Employee> employees = new List<Employee>();
// Indexer by employee name (string)
public Employee this[string name]
{
get
{
foreach (var employee in employees)
{
if (employee.Name == name)
return employee;
}
return null; // Or you could throw an exception
}
set
{
for (int i = 0; i < employees.Count; i++)
{
if (employees[i].Name == name)
{
employees[i] = value;
return;
}
}
// If there's no employee with that name—add a new one
employees.Add(value);
}
}
// For compatibility—indexer by numeric index
public Employee this[int index]
{
get { return employees[index]; }
set { employees[index] = value; }
}
}
var company = new EmployeeCollection();
company[0] = new Employee { Name = "Ivan", Age = 30, Position = "Programmer" };
company[1] = new Employee { Name = "Maria", Age = 25, Position = "Designer" };
Employee employee = company["Ivan"];
company["Peter"] = new Employee { Name = "Peter", Age = 28, Position = "Tester" };
Important: if you have several indexers—their signatures must differ in the set and types of parameters.
3. Different Access Modifiers for get and set
Sometimes you want to allow only reading by index, but not writing (or vice versa). In C# you can set different access modifiers for the get and set accessors of an indexer.
public class SecureEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
internal set { employees[index] = value; }
}
}
This is often used to protect collections from unauthorized changes, making the class more controlled.
4. Read-only and Write-only Indexers
Sometimes you want to allow only reading or only writing through an indexer.
public class ReadOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
// set is missing—you can't change!
}
}
public class WriteOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
set
{
employees.Insert(index, value);
}
// get is missing—you can't read!
}
}
In real projects, "write-only" is almost never used: usually you need "read-only", for example, when you want to allow looking at data from outside the class, but not changing it via the indexer.
5. Indexers with Bounds Checking and Logic
In quality examples, it's important not just to give access by index, but also to properly handle out-of-bounds, search errors, and other exceptional situations.
public class SafeEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Employee with this index does not exist!");
return employees[index];
}
set
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Can't replace a non-existent employee!");
employees[index] = value;
}
}
}
You can return null or use modern patterns for handling missing values—the choice depends on your app's logic.
6. Indexers with Unusual Parameters
You might run into collections where the indexer takes non-standard types: enums (enum), custom structs, even several parameters of different types.
public enum Department { IT, HR, Finance, Marketing }
public class DepartmentEmployeeCollection
{
private Dictionary<Department , Employee> departmentLeads = new Dictionary<Department , Employee>();
public Employee this[Department department]
{
get { return departmentLeads.TryGetValue(department, out var employee) ? employee : null; }
set { departmentLeads[department] = value; }
}
}
var company = new DepartmentEmployeeCollection();
company[Department.IT] = new Employee { Name = "Anna", Age = 35, Position = "IT Lead" };
Employee itLead = company[Department.IT];
This approach is used when you have unique identifiers or a clear mapping of type and value—it's convenient, clear, and type-safe.
7. Modern Features: Range and Index
With the appearance of Range and Index types in C# 8, indexers got new powers for working with ranges and indices from the end:
public class SmartArray
{
private int[] numbers = Enumerable.Range(0, 100).ToArray();
public int[] this[Range range] => numbers[range];
public int this[Index index] => numbers[index];
}
// Usage:
var smart = new SmartArray();
int[] middle = smart[20..30]; // from 20th to 29th elements
int last = smart[^1]; // last element
If you're building your own collection, supporting Range and Index makes it as "native" and convenient for developers as possible.
8. Properties vs Indexers
To better understand when to use properties and when to use indexers, it's helpful to compare their features.
- Properties are great for accessing individual characteristics of an object by name: person.Name, car.Speed.
- Indexers are for accessing elements of a collection or structure by key: employees[0], phoneBook["Ivan"].
- A property always has a specific name and there can only be one property with that name in a class.
- An indexer uses the this keyword and can have several variants if their signatures differ.
- Properties can be static, indexers can't, because this always refers to a specific object instance.
9. Common Mistakes When Working with Indexers
Mistake #1: No bounds checking.
If you don't check the index for going out of array bounds, you can get an IndexOutOfRangeException at a really bad time.
Mistake #2: Not handling possible null from the indexer.
If your string indexer returns null when there's no element, and the calling code blindly uses the result, you'll get a NullReferenceException.
Mistake #3: Duplicate indexer signatures.
C# doesn't let you create two indexers with the same set of parameters.
Mistake #4: Non-obvious logic in the set accessor.
If your set uses "add if missing" logic instead of replace, it can be confusing. Make these decisions explicit and well-documented.
10. Practical Use and Conclusion
In real projects, indexers are often used to create specialized collections, caches, dictionaries with extra logic, matrices, and multi-dimensional data structures. They make code more readable and intuitive—instead of calling methods like GetElementByIndex(5) you can just write collection[5].
Remember: indexers should logically fit the nature of your class. If your class isn't a collection or data structure, it probably doesn't need an indexer. But if your class stores and manages a set of elements, an indexer can make using it way more convenient and natural.
GO TO FULL VERSION