Indexers
Definition
An indexer is a special property-like member that allows objects to be indexed using square bracket syntax (obj[index]). Indexers enable a class or struct to behave like a virtual array or dictionary, providing intuitive element access without exposing the underlying data structure.
// Built-in indexer — every List<T> has one
var list = new List<string> { "a", "b", "c" };
string first = list[0]; // Uses List<T>'s indexer
// Custom indexer — your own types can do the same
var buffer = new TemperatureBuffer();
buffer[0] = 22.5;
double monday = buffer[0];
Core Concepts
Basic Indexer Syntax
Indexers are declared with the this keyword and square bracket parameters. They support get and set accessors just like properties.
public class TemperatureBuffer
{
private readonly double[] _temps = new double[7];
public double this[int day]
{
get
{
if (day < 0 || day >= 7)
throw new ArgumentOutOfRangeException(nameof(day));
return _temps[day];
}
set
{
if (day < 0 || day >= 7)
throw new ArgumentOutOfRangeException(nameof(day));
_temps[day] = value;
}
}
}
// Usage
var buffer = new TemperatureBuffer();
buffer[0] = 22.5; // set
buffer[6] = 18.0; // set
double monday = buffer[0]; // get — 22.5
Indexers can also be expression-bodied:
public class SimpleBuffer
{
private readonly int[] _data = new int[10];
public int this[int index]
{
get => _data[index];
set => _data[index] = value;
}
}
Multi-Dimensional Indexers
Indexers can accept multiple parameters, enabling matrix-like or table-like access patterns:
public class Matrix
{
private readonly int[,] _data = new int[3, 3];
public int this[int row, int col]
{
get => _data[row, col];
set => _data[row, col] = value;
}
}
var matrix = new Matrix();
matrix[0, 0] = 1;
matrix[1, 2] = 42;
int value = matrix[1, 2]; // 42
// String-keyed multi-parameter indexer
public class ConfigSection
{
private readonly Dictionary<(string Section, string Key), string> _values = new();
public string this[string section, string key]
{
get => _values.TryGetValue((section, key), out var v) ? v : "";
set => _values[(section, key)] = value;
}
}
var config = new ConfigSection();
config["database", "host"] = "localhost";
config["database", "port"] = "5432";
Overloading Indexers
A class can have multiple indexers with different parameter types, similar to method overloading:
public class LookupTable
{
private readonly Dictionary<string, int> _byName = new();
private readonly List<(string Name, int Value)> _items = new();
// Index by integer position
public (string Name, int Value) this[int index]
{
get => index >= 0 && index < _items.Count
? _items[index]
: throw new ArgumentOutOfRangeException(nameof(index));
}
// Index by string name
public int this[string name]
{
get => _byName.TryGetValue(name, out var value)
? value
: throw new KeyNotFoundException(name);
}
public void Add(string name, int value)
{
_byName[name] = value;
_items.Add((name, value));
}
}
var table = new LookupTable();
table.Add("width", 100);
table.Add("height", 200);
int w = table["width"]; // String indexer — 100
var first = table[0]; // Int indexer — ("width", 100)
Indexers in Interfaces
Indexers can be declared in interfaces and implemented by classes or structs:
public interface INameLookup
{
string this[int id] { get; }
bool Contains(int id);
}
public class EmployeeLookup : INameLookup
{
private readonly Dictionary<int, string> _employees = new();
public string this[int id] =>
_employees.TryGetValue(id, out var name) ? name : "Unknown";
public bool Contains(int id) => _employees.ContainsKey(id);
public void Add(int id, string name) => _employees[id] = name;
}
// Program against the interface
INameLookup lookup = new EmployeeLookup();
string name = lookup[42];
Indexers vs Properties
| Feature | Property | Indexer |
|---|---|---|
| Identified by | Name | this + parameters |
| Parameters | None | One or more |
| Static | Can be static | Cannot be static |
| Overloading | No (unique name) | Yes (different parameter types) |
| Purpose | Get/set a single value | Get/set indexed elements |
| Access syntax | obj.Name | obj[index] |
C# 8 Indices and Ranges
C# 8 introduced Index and Range structs that work with indexers:
int[] arr = { 0, 1, 2, 3, 4, 5 };
// Index from end
int last = arr[^1]; // 5
int second = arr[^2]; // 4
// Range
int[] slice = arr[1..4]; // [1, 2, 3]
int[] last3 = arr[^3..]; // [3, 4, 5]
Custom types can support these by defining indexers that accept Index and Range:
public class MyCollection
{
private readonly int[] _data = { 10, 20, 30, 40, 50 };
public int this[Index index] => _data[index.GetOffset(_data.Length)];
public int[] this[Range range] => _data[range];
}
See Modern C# for full details on indices and ranges.
When to Use
| Scenario | Use Indexers? |
|---|---|
| Custom collection wrapping an internal array or dictionary | Yes |
| Matrix or grid data structure | Yes (multi-dimensional) |
| Config/settings accessed by string key | Yes |
| Simple value get/set (no index semantics) | No — use a property |
| Type is not conceptually a container | No — use methods |
| Complex lookup logic with side effects | No — use named methods |
Common Pitfalls
-
Missing bounds checking — An indexer that silently accesses invalid indices causes
IndexOutOfRangeExceptionor corrupts state. Always validate and throwArgumentOutOfRangeExceptionwith a clear message. -
Exposing internal mutable collections — An indexer that returns a reference to an internal mutable object breaks encapsulation. Return copies or read-only views for reference types.
-
Overusing indexers — If the access pattern doesn't feel natural (like array/dictionary access), a named method is clearer.
config["timeout"]is intuitive;person["age"]is questionable. -
Read-only indexers with no documentation — Omitting the
setaccessor without documentation confuses consumers expecting write access. Make the intent clear.
Key Takeaways
- Indexers use
this[parameters]syntax and behave like parameterized properties. - They are ideal for types that logically represent collections, tables, or lookup structures.
- Indexers can be multi-dimensional, overloaded by parameter type, and declared in interfaces.
- Always validate index bounds in the
getandsetaccessors. - Prefer named methods when the "indexed access" metaphor doesn't fit the type's abstraction.
Interview Questions
Q: What is an indexer in C#?
An indexer is a special member that allows instances of a class or struct to be accessed like an array using square bracket syntax (
obj[index]). It is defined withthis[parameters]and has get/set accessors like a property.
Q: Can indexers be overloaded?
Yes. A class can have multiple indexers as long as they differ in the number or types of parameters, similar to method overloading.
Q: What is the difference between an indexer and a property?
A property is identified by a name and has no parameters. An indexer is identified by
thisand accepts one or more parameters (indices). Indexers cannot be static; properties can.
Q: Can you declare an indexer in an interface?
Yes. Interface indexers declare the signature with
thisand accessors without implementation. Implementing classes must provide the actual getter and setter logic.
Q: When should you use an indexer vs a method?
Use an indexer when the type logically represents a collection or lookup table and array-like access (
obj[key]) feels natural. Use a named method when the operation is more complex, has side effects, or the "indexed access" metaphor doesn't fit.