ZetCode

C# indexers

last modified October 16, 2026

In this article we show how to define and use indexers in C#.

An indexer allows objects of a class or struct to be accessed with the [] operator, in the same way as arrays. We use indexers every day: list[0], dict["key"], or text[3] all call indexers defined in the .NET types.

An indexer is declared like a property, but its name is the this keyword followed by parameters in square brackets.

public T this[int index]
{
    get { ... }
    set { ... }
}

Unlike array indexes, indexer parameters can be of any type and an indexer can have multiple parameters. A type can have several indexers with different parameter lists.

C# simple indexer

In the first example, we create a collection class with an integer indexer.

Program.cs
var week = new Week();

Console.WriteLine(week[0]);
Console.WriteLine(week[6]);

week[6] = "Sun";
Console.WriteLine(week[6]);

try
{
    Console.WriteLine(week[7]);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine(e.Message);
}

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

    public string this[int index]
    {
        get
        {
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, days.Length);

            return days[index];
        }

        set => days[index] = value;
    }
}

The Week class wraps an array of day names.

public string this[int index]

The indexer takes an integer and returns a string.

ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, days.Length);

In the getter, we validate the index and throw an exception with a meaningful message.

set => days[index] = value;

In the setter, the assigned value is available through the implicit value parameter.

$ dotnet run
Monday
Sunday
Sun
index ('7') must be less than '7'. (Parameter 'index')
Actual value was 7.

C# string indexer

The parameter of an indexer does not have to be an integer. In the next example, we access the values by name.

Program.cs
var settings = new Settings();

settings["theme"] = "dark";
settings["Language"] = "sk";

Console.WriteLine(settings["THEME"]);
Console.WriteLine(settings["language"]);
Console.WriteLine(settings["font"] ?? "not set");

class Settings
{
    private readonly Dictionary<string, string> data =
        new(StringComparer.OrdinalIgnoreCase);

    public string? this[string key]
    {
        get => data.TryGetValue(key, out var value) ? value : null;
        set
        {
            if (value is null)
            {
                data.Remove(key);
            }
            else
            {
                data[key] = value;
            }
        }
    }
}

The Settings class stores values in a case-insensitive dictionary. Unlike the dictionary indexer, our indexer returns null for missing keys instead of throwing an exception, and assigning null removes the key.

$ dotnet run
dark
sk
not set

C# multi-parameter indexer

An indexer can take multiple parameters. This is natural for two-dimensional data such as matrices or game boards.

Program.cs
var m = new Matrix(3, 3);

for (int i = 0; i < 3; i++)
{
    m[i, i] = 1;
}

m[0, 2] = 5;

Console.WriteLine(m);

class Matrix(int rows, int cols)
{
    private readonly double[] data = new double[rows * cols];

    public double this[int row, int col]
    {
        get => data[row * cols + col];
        set => data[row * cols + col] = value;
    }

    public override string ToString()
    {
        var lines = new List<string>();

        for (int r = 0; r < rows; r++)
        {
            var vals = Enumerable.Range(0, cols).Select(c => this[r, c]);
            lines.Add(string.Join(" ", vals));
        }

        return string.Join(Environment.NewLine, lines);
    }
}

The matrix stores its elements in a one-dimensional array; the indexer computes the position from the row and the column.

var vals = Enumerable.Range(0, cols).Select(c => this[r, c]);

Inside the class, the indexer is called with this[r, c].

$ dotnet run
1 0 5
0 1 0
0 0 1

C# overloaded and read-only indexers

A type can have several indexers with different parameter types. An indexer with only a getter is read-only; it can be written as an expression body.

Program.cs
var team = new Team(
[
    new Player("Rooney", 10),
    new Player("Giggs", 11),
    new Player("Scholes", 18)
]);

Console.WriteLine(team[1]);
Console.WriteLine(team["Scholes"]);
Console.WriteLine(team["Beckham"]?.ToString() ?? "not found");

record Player(string Name, int Number);

class Team(Player[] players)
{
    public Player this[int index] => players[index];

    public Player? this[string name] =>
        players.FirstOrDefault(p => p.Name == name);
}

The Team class can be indexed by a position or by a player name.

$ dotnet run
Player { Name = Giggs, Number = 11 }
Player { Name = Scholes, Number = 18 }
not found

No player called Beckham is in the team; the indexer returns null.

C# indices and ranges

If a type has an int indexer and a Count or Length property, the compiler supports the hat operator ^ automatically. To support ranges, the type must also have a Slice method.

Program.cs
var buf = new LogBook(["start", "load", "parse", "save", "exit"]);

Console.WriteLine(buf[0]);
Console.WriteLine(buf[^1]);
Console.WriteLine(buf[^2]);

var mid = buf[1..4];
Console.WriteLine(string.Join(", ", mid));

class LogBook(string[] entries)
{
    public int Count => entries.Length;

    public string this[int index] => entries[index];

    public string[] Slice(int start, int length) =>
        entries.AsSpan(start, length).ToArray();
}

We have not written any code for the ^ and .. operators.

Console.WriteLine(buf[^1]);

The compiler translates buf[^1] to buf[buf.Count - 1].

var mid = buf[1..4];

The compiler translates the range to the Slice(1, 3) call.

$ dotnet run
start
exit
save
load, parse, save

C# indexers in interfaces

Interfaces can declare indexers. Implementing classes must provide them.

Program.cs
IGrid grid = new Board(3);

grid[1, 1] = 'X';
grid[0, 2] = 'O';

for (int r = 0; r < grid.Size; r++)
{
    for (int c = 0; c < grid.Size; c++)
    {
        Console.Write(grid[r, c]);
    }

    Console.WriteLine();
}

interface IGrid
{
    int Size { get; }
    char this[int row, int col] { get; set; }
}

class Board : IGrid
{
    private readonly char[,] cells;

    public Board(int size)
    {
        cells = new char[size, size];

        for (int r = 0; r < size; r++)
            for (int c = 0; c < size; c++)
                cells[r, c] = '.';
    }

    public int Size => cells.GetLength(0);

    public char this[int row, int col]
    {
        get => cells[row, col];
        set => cells[row, col] = value;
    }
}

The IGrid interface declares a two-parameter indexer.

$ dotnet run
..O
.X.
...

Source

Indexers - C# programming guide

In this article we have worked with indexers in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.