Skip to main content

C# Indexers

Introduction

Indexers in C# are special class members that allow objects of a class to be indexed like arrays. They provide a way to access elements of an object using the square bracket notation ([]), similar to how we access elements in an array or list. This feature makes your code more intuitive and readable when working with collection-like classes.

Indexers are particularly useful when you're creating custom collections or wrapper classes around existing collections, allowing users of your class to interact with your object in a familiar way.

Understanding Indexers

Basic Syntax of an Indexer

The syntax of an indexer is similar to a property, but with parameters enclosed in square brackets:

csharp
public return_type this[parameter_list]
{
get { /* return the value at the specified index */ }
set { /* set the value at the specified index */ }
}

The keyword this indicates that we're creating an indexer for the containing class. The parameter_list can include one or more parameters, and the return_type specifies the type of the elements accessed through the indexer.

Simple Indexer Example

Let's create a simple class that uses an indexer to provide access to an internal array:

csharp
public class SimpleArray
{
private int[] array = new int[10];

// Indexer declaration
public int this[int index]
{
get
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfRangeException("Index is out of range");
return array[index];
}
set
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfRangeException("Index is out of range");
array[index] = value;
}
}
}

Here's how you can use this class:

csharp
// Creating an instance of SimpleArray
SimpleArray myArray = new SimpleArray();

// Setting values using the indexer
myArray[0] = 10;
myArray[1] = 20;

// Getting values using the indexer
Console.WriteLine($"First element: {myArray[0]}");
Console.WriteLine($"Second element: {myArray[1]}");

// Output:
// First element: 10
// Second element: 20

Advanced Indexer Concepts

Multi-parameter Indexers

Indexers can have multiple parameters, allowing you to create multi-dimensional access patterns:

csharp
public class Matrix
{
private int[,] data = new int[10, 10];

public int this[int row, int col]
{
get { return data[row, col]; }
set { data[row, col] = value; }
}
}

Usage:

csharp
Matrix matrix = new Matrix();
matrix[0, 0] = 1;
matrix[1, 2] = 5;

Console.WriteLine($"Element at [0,0]: {matrix[0, 0]}");
Console.WriteLine($"Element at [1,2]: {matrix[1, 2]}");

// Output:
// Element at [0,0]: 1
// Element at [1,2]: 5

Indexers with Different Parameter Types

Indexers are not limited to integer parameters. You can use any type, such as strings, to create dictionary-like classes:

csharp
public class EmployeeCollection
{
private Dictionary<string, Employee> employees = new Dictionary<string, Employee>();

public Employee this[string name]
{
get
{
if (employees.ContainsKey(name))
return employees[name];
return null;
}
set
{
employees[name] = value;
}
}
}

Usage:

csharp
EmployeeCollection staff = new EmployeeCollection();
staff["John"] = new Employee { Name = "John Doe", Position = "Developer" };
staff["Jane"] = new Employee { Name = "Jane Smith", Position = "Manager" };

Console.WriteLine($"John's position: {staff["John"].Position}");
Console.WriteLine($"Jane's position: {staff["Jane"].Position}");

// Output:
// John's position: Developer
// Jane's position: Manager

Read-Only Indexers

You can create read-only indexers by omitting the set accessor:

csharp
public class ReadOnlyCollection
{
private int[] data = { 1, 2, 3, 4, 5 };

public int this[int index]
{
get { return data[index]; }
// No set accessor makes this indexer read-only
}
}

Practical Examples

Example 1: Custom String Collection

Let's create a custom case-insensitive string collection that allows access by index:

csharp
public class CaseInsensitiveStringCollection
{
private List<string> strings = new List<string>();

// Integer indexer - access by position
public string this[int index]
{
get { return strings[index]; }
set { strings[index] = value; }
}

// String indexer - find by content (case-insensitive)
public bool this[string value]
{
get { return strings.Any(s => s.Equals(value, StringComparison.OrdinalIgnoreCase)); }
}

public void Add(string value)
{
strings.Add(value);
}

public int Count => strings.Count;
}

Usage:

csharp
CaseInsensitiveStringCollection colors = new CaseInsensitiveStringCollection();
colors.Add("Red");
colors.Add("Green");
colors.Add("Blue");

// Access by index
Console.WriteLine($"First color: {colors[0]}");

// Check if a color exists (case-insensitive)
Console.WriteLine($"Contains 'red': {colors["red"]}");
Console.WriteLine($"Contains 'Yellow': {colors["Yellow"]}");

// Output:
// First color: Red
// Contains 'red': True
// Contains 'Yellow': False

Example 2: Temperature Converter

Here's a practical example of using an indexer in a temperature converter class:

csharp
public class TemperatureConverter
{
// Convert Celsius to other units
public double this[string unit, double celsius]
{
get
{
switch (unit.ToLower())
{
case "fahrenheit":
return (celsius * 9 / 5) + 32;
case "kelvin":
return celsius + 273.15;
case "celsius":
return celsius;
default:
throw new ArgumentException("Unknown temperature unit");
}
}
}
}

Usage:

csharp
TemperatureConverter converter = new TemperatureConverter();

double celsius = 25;
double fahrenheit = converter["fahrenheit", celsius];
double kelvin = converter["kelvin", celsius];

Console.WriteLine($"{celsius}°C = {fahrenheit}°F");
Console.WriteLine($"{celsius}°C = {kelvin}K");

// Output:
// 25°C = 77°F
// 25°C = 298.15K

Best Practices for Using Indexers

  1. Use indexers when array-like access makes sense: Indexers should provide a natural, intuitive way to access elements of your class.

  2. Consider performance: Indexer operations should be efficient, especially if they will be called frequently.

  3. Validate indexes: Always validate input parameters to avoid runtime exceptions.

  4. Follow naming conventions: The parameter names should clearly indicate what they represent.

  5. Document behavior: Clearly document the behavior of your indexers, especially if they have multiple parameters or non-standard access patterns.

  6. Consider implementing IEnumerable: Classes with indexers often benefit from implementing IEnumerable to support iteration.

Common Scenarios for Using Indexers

  • Custom collection classes
  • Wrapper classes around arrays, lists, or dictionaries
  • Data structures like matrices, grids, or graphs
  • Database result wrappers
  • Configuration and settings managers
  • Multi-dimensional data access

Summary

Indexers are a powerful feature in C# that allow you to create classes that can be accessed using array-like notation. They make your code more intuitive when working with collection-like objects and can significantly improve code readability when implemented appropriately.

Key points to remember:

  • Indexers use the this keyword with square brackets
  • They can have multiple parameters of any type
  • They can be read-only or read-write
  • They're useful for custom collections and data structures
  • When implemented well, they make your code more intuitive and readable

Exercises

  1. Create a simple StringList class that stores strings and provides an indexer to access them.
  2. Implement a WeekDays class with an indexer that accepts both integers (0-6) and day names ("Monday", etc.) to get the day.
  3. Create a Matrix class that provides access to elements via row and column indices.
  4. Implement a custom Dictionary class with an indexer that's case-insensitive for string keys.
  5. Create a FileLines class that lazily loads lines from a text file and provides indexer access to specific lines.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)