C# - Indexers
C# - Indexers
Section titled “C# - Indexers”An indexer is a special member that allows an object to be indexed like an array. When you define an indexer for a class, you can access instances of that class using the familiar array access operator ([]). This provides a more intuitive and expressive syntax for accessing elements within a custom collection or data structure.
Syntax
Section titled “Syntax”The syntax for a one-dimensional indexer is similar to a property, but it uses the this keyword as the name and includes an index parameter in square brackets.
public T this[int index]{ // The get accessor is used for reading a value. get { // Logic to return the value at the specified index. }
// The set accessor is used for assigning a value. set { // Logic to set the value at the specified index. }}Like properties, indexers can have get and/or set accessors. You can also use expression-bodied syntax for concise implementations, which is common in modern C#.
Core Concepts and Use Cases
Section titled “Core Concepts and Use Cases”While properties are used to access specific, named data members, indexers are used to access elements in a collection-like object by a key (which can be an integer, string, or another type). This effectively abstracts the internal storage mechanism of your class, allowing it to behave like a virtual array or dictionary.
Example: A Simple Sentence Class
Section titled “Example: A Simple Sentence Class”Let’s create a Sentence class that stores a collection of words. We’ll add an indexer to access individual words by their position. This example uses modern C# features like expression-bodied members and proper exception handling.
using System;
public class Sentence{ private readonly string[] _words;
public Sentence(string text) { _words = text.Split(' '); }
// Indexer for accessing words by their integer position public string this[int index] { get { if (index < 0 || index >= _words.Length) { throw new IndexOutOfRangeException($"Index {index} is out of range."); } return _words[index]; } set { if (index < 0 || index >= _words.Length) { throw new IndexOutOfRangeException($"Index {index} is out of range."); } _words[index] = value; } }
public int Length => _words.Length;}
public class Program{ public static void Main(string[] args) { var sentence = new Sentence("The quick brown fox jumps over the lazy dog");
// Using the 'get' accessor of the indexer Console.WriteLine($"The fourth word is: {sentence[3]}"); // Output: fox
// Using the 'set' accessor of the indexer sentence[1] = "slow";
Console.WriteLine("The modified sentence is:"); for (int i = 0; i < sentence.Length; i++) { Console.Write($"{sentence[i]} "); } // Output: The slow brown fox jumps over the lazy dog }}Best Practice: Exception Handling
Section titled “Best Practice: Exception Handling”In the previous example, we checked the index and threw an IndexOutOfRangeException if it was invalid. This is standard practice and aligns with how built-in .NET collections behave. Returning null or an empty string can hide bugs and lead to unexpected NullReferenceException later in the code.
Using Different Index Types
Section titled “Using Different Index Types”Indexers are not limited to integer keys. You can use any type, such as a string, to create dictionary-like behavior. This is often achieved by using a Dictionary<TKey, TValue> as the internal data store.
Example: A Dictionary-Style Configuration Manager
Section titled “Example: A Dictionary-Style Configuration Manager”Here’s an example of a simple configuration manager that uses a string indexer to get and set configuration values.
using System;using System.Collections.Generic;
public class AppSettings{ private readonly Dictionary<string, string> _settings = new();
// String indexer public string? this[string key] { get { // TryGetValue is more efficient than checking ContainsKey then accessing the key _settings.TryGetValue(key, out var value); return value; // Returns null if key is not found } set { if (value is null) { _settings.Remove(key); } else { _settings[key] = value; } } }}
public class Program{ public static void Main() { var settings = new AppSettings();
// Use the 'set' accessor settings["ApiUrl"] = "https://api.example.com"; settings["Timeout"] = "30000";
// Use the 'get' accessor Console.WriteLine($"API URL: {settings["ApiUrl"]}"); Console.WriteLine($"Non-existent Key: {settings["ApiKey"] ?? "Not Found"}"); }}Overloading Indexers
Section titled “Overloading Indexers”A class can have multiple indexers, a feature known as indexer overloading. Each overload must have a different number or type of parameters. This allows for flexible ways to access data.
Example: A 2D Grid
Section titled “Example: A 2D Grid”A class representing a 2D grid is a perfect candidate for an overloaded indexer. We can provide an indexer to access a specific cell (row, col) and another to access an entire row.
using System;
public class Grid<T>{ private readonly T[,] _matrix;
public Grid(int rows, int cols) { if (rows <= 0) throw new ArgumentOutOfRangeException(nameof(rows)); if (cols <= 0) throw new ArgumentOutOfRangeException(nameof(cols)); _matrix = new T[rows, cols]; }
public int Rows => _matrix.GetLength(0); public int Cols => _matrix.GetLength(1);
// Indexer with two parameters for cell access public T this[int row, int col] { get => _matrix[row, col]; set => _matrix[row, col] = value; }
// Overloaded indexer to get a whole row (read-only) public T[] this[int row] { get { var rowData = new T[Cols]; for (int i = 0; i < Cols; i++) { rowData[i] = _matrix[row, i]; } return rowData; } }}
public class Program{ public static void Main() { var grid = new Grid<int>(3, 4); grid[1, 2] = 99; // Set a specific cell
Console.WriteLine($"Value at (1, 2): {grid[1, 2]}");
var secondRow = grid[1]; // Get the entire second row Console.WriteLine($"Value from the extracted row at index 2: {secondRow[2]}"); }}Common Errors and Debugging
Section titled “Common Errors and Debugging”IndexOutOfRangeException: This is the most common error. It occurs when you try to access an index that is outside the valid bounds of the underlying data structure. Always add boundary checks in your indexer implementation.KeyNotFoundException: This occurs with dictionary-based indexers when you try to access a key that does not exist. UseTryGetValueto handle this gracefully.- Ambiguous Overloads: If you define multiple indexers, ensure their signatures are unique. The compiler will report an error if a call to an indexer is ambiguous.
Further Learning
Section titled “Further Learning”Indexers are a powerful feature for creating custom, intuitive APIs for your classes. To deepen your understanding, consider exploring:
- Implementing standard interfaces like
IEnumerable<T>alongside indexers to enable LINQ queries andforeachloops on your custom types. - Exploring indexers in the context of C# 8.0’s indices and ranges feature, which provides a more concise syntax for accessing elements from the end or slicing collections (
myCollection[^1]). - Reviewing the source code of built-in .NET collections like
List<T>andDictionary<TKey, TValue>to see how they are implemented.