C# Indexers — NullReference Ambush in String-Indexed Stores
Unexpected NullReferenceException from a string-indexed store? Ensure your indexer throws KeyNotFoundException, not null.
- Indexers let custom C# types support bracket notation (obj[key]) for element access, just like arrays or dictionaries.
- Declared with 'this[parameterType param]' and get/set accessors — a property with a parameter, not a standalone feature.
- Index keys can be int, string, enum, or multiple parameters — overload by signature same as methods.
- Performance: indexers are JIT-inlineable, often zero overhead vs a manual method; validation logic adds runtime cost.
- Production insight: missing null checks or bounds validation in indexers cause confusing crashes deep in internal arrays, not at the call site.
Most C# developers discover indexers the moment they start digging into how List<T> or Dictionary<TKey, TValue> actually work under the hood. You use myList[0] every day without a second thought — but that bracket notation isn't some built-in language primitive reserved for arrays. It's a feature you can implement yourself on any class you write. That's what makes indexers genuinely powerful.
Before indexers existed, if you wanted your custom collection or data-wrapper class to support element access, you'd be stuck writing verbose GetItem(int index) and SetItem(int index, T value) methods. Users of your class would have to learn a bespoke API instead of reaching for the intuitive bracket syntax they already know. Indexers solve this by letting you define exactly what obj[key] means for your type — whether the key is an integer, a string, an enum, or anything else.
By the end of this article you'll understand not just how to declare an indexer, but when it's the right design choice, how to build multi-parameter and overloaded indexers, and the subtle bugs that catch even experienced developers off guard. You'll walk away ready to write cleaner APIs and answer indexer questions confidently in a technical interview.
What an Indexer Actually Is (and Why It Beats a Plain Method)
An indexer is a special kind of property that accepts one or more parameters — the index values inside the square brackets. Like a regular property it has a get accessor and an optional set accessor, but instead of using the property name as the access point, callers use bracket notation on the object itself.
The key design motivation is expressiveness. Compare these two lines:
string city = addressBook.GetEntry("Alice"); string city = addressBook["Alice"];
The second line is immediately obvious to anyone who has used a dictionary or array. The first forces the caller to learn your method name. When your class semantically represents a collection or a keyed store of data, an indexer makes your API feel native to the language.
Indexers are declared using the this keyword followed by the parameter list in square brackets — think of this as saying 'when someone indexes into me, here is what to do'. They live on the class just like properties, and they fully support access modifiers, readonly patterns (get-only), and even expression-bodied syntax for simple cases.
String-Keyed Indexers — Modeling a Real Configuration Store
Indexers don't have to use integers. Any type can be an index — and string-keyed indexers are especially common in the real world. ASP.NET's IConfiguration, HttpContext.Items, and ViewData all use string indexers. If you've ever written config["ConnectionStrings:Default"] you've used one.
A string indexer is ideal when your class wraps a keyed data store and you want callers to access values by name rather than position. It communicates intent clearly: this isn't a list, it's a lookup.
The example below models a lightweight application settings store. Notice how we return null for missing keys in the getter rather than throwing — this mirrors the design of Dictionary<TKey, TValue>.TryGetValue and prevents callers from wrapping every access in a try-catch. That's a deliberate API design choice, not an accident.
Overloaded and Multi-Parameter Indexers — When One Index Isn't Enough
C# lets you overload indexers just like methods — the same class can have multiple indexers as long as their parameter signatures differ. This is genuinely useful when your type can be meaningfully accessed by more than one kind of key.
You can also define indexers that take multiple parameters, separated by commas inside the brackets. This fits naturally for two-dimensional or grid-based data structures where obj[row, col] is far more readable than obj.GetCell(row, col).
The example below models a spreadsheet grid. It exposes both a two-integer indexer for positional access and a string indexer that parses Excel-style cell addresses like "B3". Both indexers share the same underlying storage. This gives callers the flexibility to use whichever style fits their context — and the class looks and feels like a first-class citizen of the language.
Read-Only Indexers and Interface Contracts — The Pattern You'll See in Real Codebases
Not every indexer should allow writes. A read-only indexer (get accessor only) is the right choice when your class exposes data that callers should query but never mutate directly — think of a compiled lookup table, a cached result set, or an immutable view over some underlying data.
Indexers can also be declared in interfaces, which is the key to writing testable, mockable code. When ASP.NET Core defines IHeaderDictionary with a string indexer, any class that implements that interface must provide that indexer. Your own interfaces can do exactly the same thing.
The example below shows a read-only country code lookup paired with an interface. Notice that the interface declares only a getter — implementing classes can add a setter internally if they need it, but callers holding a reference to the interface can't write through it. That's the encapsulation story indexers tell best.
Indexer Performance and Thread Safety — What Senior Engineers Watch For
Indexers are methods under the hood. The JIT can inline simple getters and setters, so for trivial cases they have zero overhead compared to a manually written method. But when you add validation, exception handling, or delegation to another method, the inlining budget shrinks.
More importantly, indexers are NOT inherently thread-safe. If two threads write and read through the same indexer concurrently, you get torn reads, corrupted state, or stale data. A common mistake: assuming that because you're using a collection as backing store, the indexer is safe. Dictionary<TKey,TValue> has internal locks only for some operations, but even then, concurrent indexer calls can cause exceptions or data loss.
This section shows a thread-safe indexer using a ReaderWriterLockSlim for a scenarios where reads are frequent and writes are rare. For high-throughput scenarios, consider using ConcurrentDictionary<TKey,TValue> and exposing its indexer directly.
| Feature / Aspect | Indexer | Regular Method (GetItem/SetItem) |
|---|---|---|
| Syntax at call site | obj[key] — familiar, concise | obj.GetItem(key) — verbose, custom name |
| Discoverability | Intuitive for any developer | Caller must learn your method name |
| Multiple overloads | Yes — by parameter type/count | Yes — but adds more method names |
| Usable in interfaces | Yes — get and/or set | Yes |
| Expression-bodied syntax | Yes — for simple getters | Yes |
| Supports multiple params | Yes — this[int r, int c] | Yes — but no syntactic benefit |
| Works with LINQ / foreach | No — indexer alone is not enough; IEnumerable needed separately | Same limitation |
| Best fit | Collection-like or keyed-access types | Types where access is a side-effectful operation |
Key Takeaways
- An indexer is declared with 'this[parameterType param]' and gives your class bracket-notation access — it's a property with a parameter, not a standalone language feature.
- Use string indexers when your type is semantically a keyed store; use int indexers when it's positional; use multi-parameter indexers for grid or matrix types where obj[row, col] reads naturally.
- Indexers can be declared in interfaces — declaring only a getter in the interface enforces a read-only contract for all consumers, even if the implementation supports writes internally.
- Overload indexers by parameter type or count, and delegate secondary overloads to the primary one to keep validation logic in a single place and avoid subtle inconsistency bugs.
Common Mistakes to Avoid
- Forgetting to validate the index in get and set
Symptom: An IndexOutOfRangeException from your private array, with a confusing stack trace that points inside your class rather than at the caller's bad input. Production logs show the error origin as your internal method, not the caller's line.
Fix: Add bounds or key validity checks at the top of both accessors. Throw ArgumentOutOfRangeException or ArgumentException with a clear message before touching the backing store. - Defining an indexer on a type that isn't conceptually a collection
Symptom: Other developers are confused about what bracket notation means on your class (e.g., order[3] on an Order class — does it mean the 4th item? the 4th property?). Code reviews become slow because meaning is ambiguous.
Fix: Only add an indexer when your type semantically represents a keyed store or collection. If the access is not naturally 'retrieve element by key', use a named method instead. - Returning a mutable reference type from an indexer without cloning it
Symptom: Callers can mutate the returned object and silently corrupt your class's internal state, because they received the actual reference not a copy. A bug manifests far from the actual indexer call, making it hard to trace.
Fix: Return a defensive copy for value-sensitive types, or document clearly that the returned object is live. For immutable types (string, structs, records) this is not an issue.
Interview Questions on This Topic
- QCan you define an indexer in a C# interface? If so, what's the difference between declaring only a getter versus both a getter and setter in the interface?SeniorReveal
- QHow does an indexer differ from a property in C#? When would you choose to add an indexer to a class instead of just adding a named method like
GetItem()?Mid-levelReveal - QIf a class has two indexers — one that takes an int and one that takes a string — how does the compiler decide which one to call? What happens if you try to define two indexers that both take a single int parameter?Mid-levelReveal
Frequently Asked Questions
Can a C# class have more than one indexer?
Yes. C# supports indexer overloading — you can define multiple indexers on the same class as long as their parameter signatures are different. For example, one indexer taking an int and another taking a string is perfectly valid. The compiler resolves which one to call based on the argument type you pass inside the brackets.
What is the difference between an indexer and a property in C#?
A property is accessed by name (person.Name) and takes no parameters. An indexer is accessed through bracket notation on the object itself (collection[key]) and requires at least one parameter. Both have get and set accessors and support access modifiers, but indexers are specifically designed to give your class array-like or dictionary-like access semantics.
Does adding an indexer to a class automatically make it iterable with foreach?
No — these are separate concerns. An indexer gives you bracket access (obj[0]) but foreach requires the class to implement IEnumerable or IEnumerable<T>. You can have an indexer without IEnumerable and vice versa. If you want both, implement IEnumerable<T> in addition to your indexer — List<T> does exactly this.
Can an indexer be declared as static?
No, indexers cannot be static. They always operate on an instance of the class. This makes sense because indexers provide access to instance data — a static indexer would have no instance context. If you need static-like access, create a singleton instance and expose an indexer on it.
Can an indexer be async?
No, indexers cannot be declared with the async keyword because they are properties under the hood, and properties cannot be async. If you need async behavior for index access, you must create a named method instead (e.g., GetValueAsync(int key)).
That's OOP in C#. Mark it forged?
4 min read · try the examples if you haven't