Back to Blog
C#

C# Indexer get set: Syntax and Usage

c# indexer get set: Learn how to implement C# indexers with get and set accessors, including syntax, validation logic, overloading, and performance considerations.

C# indexersget set accessorsindexer syntaxcustom collectionsC# properties
Diagram showing a C# indexer with get and set accessors accessing an array element

In C#, an indexer allows an object to be indexed like an array. The c# indexer get set pattern defines how elements are retrieved and assigned using square brackets. This is essential when building custom collection types or wrapper classes that need array-like access while keeping internal storage encapsulated.

Indexer Syntax and Accessors

An indexer is declared using the this keyword, followed by an index parameter list in square brackets. The accessors get and set behave similarly to property accessors but operate on the index value. The basic syntax is:

public T this[int index] { get { return _items[index]; } set { _items[index] = value; } }

The get accessor returns the element at the given index. The set accessor receives the assigned value through the implicit value keyword. The indexer can have any number of parameters, and the parameter types are not limited to integers.

Implementing a Read-Write Indexer

A common use case is a simple wrapper around an internal array or list. Here is a complete example that exposes a read-write indexer over a private array:

public class StringCollection { private string[] _items = new string[10]; public string this[int index] { get { return _items[index]; } set { _items[index] = value; } } }

Usage is straightforward:

var collection = new StringCollection(); collection[0] = "first"; string first = collection[0];

The set accessor assigns the incoming value to the array slot. The get accessor retrieves it. Without the set accessor, the indexer becomes read-only, and assignment produces a compile-time error.

Adding Logic in get and set

Indexers are not limited to direct storage access. You can validate inputs, transform values, or compute results. For example, a temperature converter might expose Celsius values while storing Fahrenheit internally:

public class TemperatureList { private double[] _fahrenheit = new double[10]; public double this[int index] { get { return (_fahrenheit[index] - 32) * 5 / 9; } set { _fahrenheit[index] = value * 9 / 5 + 32; } } }

The get accessor converts the stored value to Celsius, and the set accessor converts the incoming Celsius value to Fahrenheit before storing it. This keeps the conversion logic centralized and prevents it from leaking into the calling code.

Validation is another common pattern. The set accessor can reject invalid values or indices:

public class BoundedList { private int[] _items = new int[10]; public int this[int index] { get { if (index < 0 || index >= _items.Length) throw new ArgumentOutOfRangeException(nameof(index)); return _items[index]; } set { if (index < 0 || index >= _items.Length) throw new ArgumentOutOfRangeException(nameof(index)); if (value < 0) throw new ArgumentException("Value must be non-negative."); _items[index] = value; } } }

Here both accessors check the index range. The set accessor also validates the value. This keeps the collection safe and consistent.

Indexer Overloading and Multiple Parameters

Indexers can be overloaded, and they can accept multiple parameters. This is useful for multi-dimensional structures or key-value lookups. For example, a matrix class can expose a two-parameter indexer:

public class Matrix { private double[,] _data; public Matrix(int rows, int cols) { _data = new double[rows, cols]; } public double this[int row, int col] { get { return _data[row, col]; } set { _data[row, col] = value; } } }

Usage:

var matrix = new Matrix(3, 3); matrix[1, 2] = 42.5; double cell = matrix[1, 2];

Overloading allows different index types. A collection could have both an integer index and a string key index:

public class HybridCollection { private string[] _items = new string[10]; private Dictionary<string, int> _map = new Dictionary<string, int>(); public string this[int index] { get { return _items[index]; } set { _items[index] = value; } } public string this[string key] { get { return _items[_map[key]]; } set { _map[key] = Array.IndexOf(_items, value); } } }

This example is simplified, but it shows how different parameter types can be used. Overloading must have distinct parameter lists, just like regular methods.

Indexer vs Property: When to Use Each

Properties and indexers both use get and set accessors, but they serve different purposes. A property represents a single named value on an object. An indexer represents a collection of values accessed by an index or key. The choice depends on the semantics of the type.

CriterionPropertyIndexer
AccessBy nameBy index or key
ParametersNoneOne or more
Syntaxobj.Propertyobj[index]
Typical useSingle attributeCollection or lookup

Use a property when the object has a well-known attribute, such as Name or Count. Use an indexer when the object is conceptually a container, such as a list, dictionary, or matrix. Indexers are not a replacement for properties; they complement them.

Performance and Runtime Considerations

Indexers are method calls under the hood. The get and set accessors compile to get_Item and set_Item methods. This means there is a small overhead compared to direct array access, but the JIT compiler often inlines simple accessors. For most applications, the cost is negligible.

However, if the accessor performs expensive work, such as parsing or conversion, that cost is paid on every access. Consider caching computed values when the indexer is called frequently with the same index. Also, be aware that bounds checks are performed by the runtime when accessing arrays, but custom indexers can add their own checks, increasing overhead.

When implementing a custom collection, avoid making the indexer allocate memory on each call unless necessary. For example, returning a new object from get can cause garbage collection pressure. Instead, store objects directly and return references.

Common Pitfalls with Indexer get and set

One common mistake is forgetting to handle out-of-range indices. Without explicit checks, the underlying array or list will throw its own exception, but the message may not be clear. Always validate indices in the accessor if the collection has fixed bounds.

Another pitfall is using a reference type in set without copying. If the caller modifies the object after assignment, the collection reflects those changes. This is expected behavior, but it can surprise developers who assume value semantics.

Null values are another concern. If the indexer allows null, the get accessor may return null, and the caller must check for it. Decide whether null is a valid stored value and document it.

Finally, be careful with indexers that have side effects. The get accessor should not modify state; it should only retrieve. The set accessor should validate and assign. Mixing side effects into get makes the code unpredictable and hard to debug.

By keeping the accessors focused and predictable, an indexer becomes a clean, maintainable part of your type's API.

c# indexer get set: Practical Usage and Code Examples | RYUSLOG DEV