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:
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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
-
Use indexers when array-like access makes sense: Indexers should provide a natural, intuitive way to access elements of your class.
-
Consider performance: Indexer operations should be efficient, especially if they will be called frequently.
-
Validate indexes: Always validate input parameters to avoid runtime exceptions.
-
Follow naming conventions: The parameter names should clearly indicate what they represent.
-
Document behavior: Clearly document the behavior of your indexers, especially if they have multiple parameters or non-standard access patterns.
-
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
- Create a simple StringList class that stores strings and provides an indexer to access them.
- Implement a WeekDays class with an indexer that accepts both integers (0-6) and day names ("Monday", etc.) to get the day.
- Create a Matrix class that provides access to elements via row and column indices.
- Implement a custom Dictionary class with an indexer that's case-insensitive for string keys.
- 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! :)