In C#, you can provide functionality to access data within instances of classes or structs using brackets [], just like arrays. This feature is called an Indexer.
Using indexers improves the intuitive operability of classes that hold collections internally (such as wrapper classes). In this article, using a class that manages an employee directory as an example, I will explain how to overload indexers to allow access by both an integer index and an Employee ID (string).
Basic Syntax of Indexers
Indexers are similar to properties, but they differ in that they use the this keyword instead of a name and take arguments.
public ReturnType this[IndexType argumentName]
{
get { /* Processing for retrieval */ }
set { /* Processing for setting (receive value with 'value' keyword) */ }
}
Practical Code Example: Employee Directory Management
The following code is an example of implementing two indexers for an EmployeeRegistry class that holds a List<Employee> internally.
inttype indexer: Retrieves elements by list order (Read-only).stringtype indexer: Retrieves or sets elements using the Employee ID as a key (Upsert behavior).
using System;
using System.Collections.Generic;
using System.Linq;
namespace PersonnelSystem
{
// Employee data (Using C# 9.0+ record type)
public record Employee(string Id, string Name, string Department);
// Employee Registry Class
public class EmployeeRegistry
{
// Internal data store
private readonly List<Employee> _employees = new();
// 1. Access by index number (int)
// Using Expression-bodied members for brevity.
// Enables access like registry[0].
public Employee this[int index] => _employees[index];
// 2. Access by Employee ID (string)
// Enables access like registry["EMP001"], acting like a dictionary.
public Employee this[string employeeId]
{
get
{
// Return employee with specified ID (Throws exception if not found)
return _employees.First(e => e.Id == employeeId);
}
set
{
// Search if the same ID exists in the list
var index = _employees.FindIndex(e => e.Id == employeeId);
if (index < 0)
{
// Add new if not found
_employees.Add(value);
}
else
{
// Overwrite (update) info if found
_employees[index] = value;
}
}
}
// Property to get current count
public int Count => _employees.Count;
}
class Program
{
static void Main()
{
var registry = new EmployeeRegistry();
// Register data using string indexer (set)
// Since ID doesn't exist, it adds a new record.
registry["EMP-001"] = new Employee("EMP-001", "Taro Yamada", "Development");
registry["EMP-002"] = new Employee("EMP-002", "Hanako Suzuki", "Sales");
// Update data using string indexer (set)
// Since ID exists, it overwrites the existing record.
registry["EMP-001"] = new Employee("EMP-001", "Taro Yamada", "R&D"); // Department transfer
// Get first data using int indexer (get)
var firstEmployee = registry[0];
Console.WriteLine($"[Index 0] {firstEmployee.Name} ({firstEmployee.Department})");
// Get data by ID using string indexer (get)
var targetEmployee = registry["EMP-002"];
Console.WriteLine($"[ID search] {targetEmployee.Name} ({targetEmployee.Department})");
}
}
}
Execution Result
[Index 0] Taro Yamada (R&D)
[ID search] Hanako Suzuki (Sales)
Technical Points
1. Indexer Overloading
Just like methods, you can define multiple indexers as long as the argument types or the number of arguments differ. In the example above, we use int (ordered access) and string (key search) separately. This allows you to provide flexible access methods suited to the context.
2. get and set Accessors
- get: Called when retrieving a value.
- set: Called when assigning a value. The assigned value can be referenced using the
valuekeyword.
By implementing only one of them, you can create “Read-only” or “Write-only” indexers (the int indexer above is read-only because it only defines get).
3. Expression-bodied Member Definitions
Since C# 6.0, if the content of a property or indexer is a single expression, you can use the => (arrow operator) to write it concisely.
// Standard syntax
public Employee this[int index] { get { return _employees[index]; } }
// Expression-bodied syntax
public Employee this[int index] => _employees[index];
Summary
Implementing indexers properly increases the convenience of classes that encapsulate collections. Especially for classes that frequently require operations like “Search by ID” or “Get by Order,” indexers provide a powerful feature that allows for code that is more intuitive and readable than standard method calls (like GetById).
