Indexers in C#
Vaibhav • September 11, 2025
In earlier chapters, we explored arrays, lists, and dictionaries - all of which let you access elements using
square brackets like myList[0]
or myDictionary["key"]
. This syntax is intuitive and powerful, especially when
working with collections. But did you know you can use the same syntax in your own classes? That’s exactly what
indexers allow you to do.
Indexers let you define how your custom types respond to indexing - just like arrays and lists. This means you
can write myObject[5]
and have it do something meaningful, based on your own
logic. In this article, we’ll explore how indexers work, how to define them, how they relate to properties and
collections, and how to use them effectively in real-world scenarios.
What Is an Indexer?
An indexer is a special kind of property that allows you to access elements in an object using the square
bracket syntax. It looks like a method, but behaves like a property. You define it using the this
keyword followed by a parameter list inside square brackets.
public class MyCollection
{
private int[] data = new int[10];
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
This class wraps an array and exposes it through an indexer. Now you can write:
MyCollection c = new();
c[0] = 42;
Console.WriteLine(c[0]); // Output: 42
The syntax is clean and familiar - just like working with arrays or lists. But behind the scenes, you’re calling
the get
and set
accessors of the indexer.
Indexers are not methods - they’re properties with parameters. You can define multiple indexers with different parameter types, just like method overloading.
Why Use Indexers?
Indexers make your types feel like collections - even if they’re not. They’re useful when:
- You want to expose internal data using array-like syntax.
- You want to simplify access to elements in a custom structure.
- You want to support multiple ways of accessing data (e.g., by index or by key).
They’re especially helpful in wrapper classes, domain models, and custom data containers.
Defining an Indexer
To define an indexer, use the this
keyword followed by a parameter list. You
must provide both get
and set
accessors
(unless it’s read-only).
public class NameBook
{
private string[] names = new string[5];
public string this[int index]
{
get { return names[index]; }
set { names[index] = value; }
}
}
This class lets you store and retrieve names using an index:
NameBook book = new();
book[0] = "Alice";
book[1] = "Bob";
Console.WriteLine(book[1]); // Output: Bob
The indexer behaves like a property - but with parameters. You can define multiple indexers with different parameter types, just like method overloading.
Read-Only Indexers
If you want to expose data without allowing modification, you can define a read-only indexer:
public class ReadOnlyBook
{
private string[] titles = { "C# Basics", "Advanced C#", "LINQ" };
public string this[int index]
{
get { return titles[index]; }
}
}
This class lets you read titles, but not change them:
ReadOnlyBook book = new();
Console.WriteLine(book[2]); // Output: LINQ
// book[2] = "New Title"; // ❌ Error: no set accessor
This is useful when you want to protect internal data from modification.
Using Indexers with Strings or Keys
Indexers don’t have to use integers. You can use strings, enums, or any type that makes sense for your data. For example:
public class PhoneBook
{
private Dictionary<string, string> contacts = new();
public string this[string name]
{
get
{
if (contacts.ContainsKey(name))
return contacts[name];
return "Not found";
}
set
{
contacts[name] = value;
}
}
}
Now you can write:
PhoneBook pb = new();
pb["Vaibhav"] = "123-456";
Console.WriteLine(pb["Vaibhav"]); // Output: 123-456
Console.WriteLine(pb["Unknown"]); // Output: Not found
This pattern is common in dictionaries, caches, and lookup tables.
Note: Indexers can use any parameter type - not just integers. Choose the type that best matches your data access pattern.
Multiple Indexers
You can define multiple indexers in the same class - as long as they have different parameter types. For example:
public class MultiAccess
{
private string[] data = { "Zero", "One", "Two" };
public string this[int index] => data[index];
public int this[string value]
{
get
{
for (int i = 0; i < data.Length; i++)
{
if (data[i] == value)
return i;
}
return -1;
}
}
}
This class lets you access data by index or by value:
MultiAccess m = new();
Console.WriteLine(m[1]); // Output: One
Console.WriteLine(m["Two"]); // Output: 2
This flexibility makes your types more expressive and user-friendly.
Indexers vs Properties
Indexers are similar to properties - but with parameters. Use properties when you want to expose a single value
(like Name
or Age
), and use indexers when you
want to expose a collection-like interface.
For example:
public class Student
{
public string Name { get; set; } // Property
public int this[int index] { get; set; } // Indexer
}
Properties are accessed by name; indexers are accessed by index.
Indexers and Encapsulation
Indexers help you encapsulate internal data while exposing a clean interface. You can validate input, handle errors, or transform data - all behind the scenes.
public class SafeArray
{
private int[] values = new int[5];
public int this[int index]
{
get
{
if (index < 0 || index >= values.Length)
return -1;
return values[index];
}
set
{
if (index >= 0 && index < values.Length)
values[index] = value;
}
}
}
This class protects against out-of-bounds access:
SafeArray sa = new();
sa[2] = 99;
Console.WriteLine(sa[2]); // Output: 99
Console.WriteLine(sa[10]); // Output: -1
This kind of defensive programming improves reliability and safety.
Common Mistakes and How to Avoid Them
Indexers are powerful - but they can be misused. Here are some common mistakes:
- Using indexers when a method would be clearer - not all access patterns need square brackets.
- Allowing unchecked access - always validate input to avoid exceptions.
- Returning references to internal data - which breaks encapsulation.
- Overloading indexers with confusing parameter types - keep it intuitive.
Use indexers when they improve clarity - not just because they’re available.
Use indexers to expose collection-like behavior. Validate input, protect internal data, and keep the interface intuitive.
Design Tips for Indexers
Indexers are a design tool. Use them to make your types more expressive and user-friendly:
- Use them when your type represents a collection or lookup.
- Keep the parameter type simple and predictable.
- Document the behavior clearly - especially for edge cases.
- Avoid side effects - indexers should be safe and fast.
These tips help you write clean, maintainable code - especially in libraries and shared APIs.
The List<T>
and Dictionary<K,V>
types use indexers to provide fast, intuitive access to
elements - that’s why you can write myList[0]
or myDict["key"]
.
Summary
Indexers let you define how your custom types respond to square bracket syntax - just like arrays and lists.
You’ve learned how to define indexers using the this
keyword, how to use them
with integers, strings, and other types, how to combine them with encapsulation and validation, and how to avoid
common mistakes.
By using indexers thoughtfully, you make your types more expressive, intuitive, and consistent with the rest of the language. In the next article, we’ll explore Attributes and Metadata - a feature that lets you annotate your code with extra information for tools, frameworks, and the compiler.