DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Exploring Indexers in C#: Array-Like Access for Custom Types

Indexers in C# enable you to treat custom objects like arrays, providing an intuitive way to access elements while keeping the internal structure private. In this article, we’ll explore indexers step by step, with clear examples to help you apply the concept in real-world scenarios.


What Are Indexers?

An indexer is a special kind of property that allows you to access elements of a class or struct using array-like syntax. You define it using the this keyword, along with an index type.

Why Use Indexers?

  • Encapsulation: Hide the internal collection structure.
  • Readability: Simplify access to elements without exposing unnecessary details.
  • Flexibility: Customize how elements are retrieved or set.

Step 1: A Simple Read-Only Indexer

Let’s create a class OrderList that holds an array of Order objects. We’ll define an indexer to retrieve orders by their position in the array.

Full Example

// Define an Order class
public class Order
{
    public int Id { get; set; }
    public string Description { get; set; }
}

// Define OrderList with an indexer
public class OrderList
{
    private Order[] _orders;

    // Constructor to initialize orders
    public OrderList(Order[] orders)
    {
        _orders = orders;
    }

    // Read-only indexer
    public Order this[int index]
    {
        get
        {
            if (index < 0 || index >= _orders.Length)
                throw new IndexOutOfRangeException("Invalid index.");
            return _orders[index];
        }
    }
}

// Program to demonstrate usage
public class Program
{
    public static void Main()
    {
        // Initialize an array of orders
        var orders = new Order[]
        {
            new Order { Id = 1, Description = "Laptop" },
            new Order { Id = 2, Description = "Smartphone" },
            new Order { Id = 3, Description = "Tablet" }
        };

        // Create an OrderList
        var orderList = new OrderList(orders);

        // Access orders using the indexer
        Console.WriteLine(orderList[0].Description); // Output: Laptop
        Console.WriteLine(orderList[1].Description); // Output: Smartphone
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. The this Keyword: Used to define an indexer.
  2. Type Safety: The int type ensures only valid indices are used.
  3. Encapsulation: The _orders array is private, so the internal data structure isn’t exposed.
  4. Validation: The indexer validates the index to prevent runtime errors.

Step 2: Adding a Custom Key to the Indexer

What if you want to retrieve an order by its Id instead of its position? We can extend the indexer to accept custom keys like int or GUID.

Full Example

public class OrderList
{
    private Order[] _orders;

    public OrderList(Order[] orders)
    {
        _orders = orders;
    }

    // Indexer with integer index
    public Order this[int index]
    {
        get
        {
            if (index < 0 || index >= _orders.Length)
                throw new IndexOutOfRangeException("Invalid index.");
            return _orders[index];
        }
    }

    // Indexer with GUID key
    public Order this[Guid orderId]
    {
        get
        {
            var order = _orders.FirstOrDefault(o => o.Id == orderId.GetHashCode());
            if (order == null)
                throw new KeyNotFoundException("Order not found.");
            return order;
        }
    }
}

// Program to demonstrate usage
public class Program
{
    public static void Main()
    {
        var orders = new Order[]
        {
            new Order { Id = 1, Description = "Laptop" },
            new Order { Id = 2, Description = "Smartphone" },
            new Order { Id = 3, Description = "Tablet" }
        };

        var orderList = new OrderList(orders);

        // Access orders using integer index
        Console.WriteLine(orderList[1].Description); // Output: Smartphone

        // Access orders using GUID
        var guid = Guid.NewGuid();
        var order = new Order { Id = guid.GetHashCode(), Description = "Monitor" };

        Console.WriteLine(orderList[guid].Description); // Throws KeyNotFoundException
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Use a Method Instead of an Indexer?

For custom keys, retrieving an element can be expensive, especially with large data sets. Using an indexer might create a false expectation of fast access. Instead, define a method for such lookups:

public Order FindById(Guid orderId) => _orders.FirstOrDefault(o => o.Id == orderId.GetHashCode());
Enter fullscreen mode Exit fullscreen mode

Step 3: Real-World Example

The Dictionary<TKey, TValue> class in .NET uses indexers to retrieve values by their keys:

var dictionary = new Dictionary<int, string>
{
    [1] = "Laptop",
    [2] = "Smartphone"
};

Console.WriteLine(dictionary[1]); // Output: Laptop
Enter fullscreen mode Exit fullscreen mode

Best Practices for Indexers

  1. Use Indexers for Fast Access: Ensure the underlying structure allows efficient retrieval.
  2. Avoid Complex Lookups: Use methods for computationally expensive operations.
  3. Provide Clear Error Messages: Validate indices and keys, throwing appropriate exceptions.

Top comments (0)