Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

The [] (element access) operator provides indexed access to elements within arrays, spans, pointers, and custom types that implement indexers. It evaluates a base expression and one or more index expressions to return a reference or value associated with a specific logical position, physical memory offset, or mapped key.
using System.Collections.Generic;

public class CustomCollection
{
    private readonly int[] _data = new int[10];

    // Indexer definition (Custom Types)
    public int this[int index]
    {
        get { return _data[index]; }
        set { _data[index] = value; }
    }
}

public class OperatorExamples
{
    public void DemonstrateAccess()
    {
        // Single-dimensional access
        int[] array = new int[5];
        int element = array[0];

        // Multi-dimensional access
        int[,] matrix = new int[2, 2];
        int matrixElement = matrix[0, 1];

        // Null-conditional element access
        int[]? nullableArray = null;
        int? safeElement = nullableArray?[0];

        // Index initializer
        var dictionary = new Dictionary<string, int> 
        { 
            ["key"] = 42 
        };
    }
}

Underlying Mechanics

The behavior and compilation of the [] operator depend entirely on the type of the base expression it is applied to.

1. Arrays (T[], T[,])

For built-in arrays, the CLR handles the [] operator intrinsically, but the emitted Intermediate Language (IL) differs based on the array’s rank:
  • Single-dimensional arrays (T[]): The compiler generates ldelem and stelem IL instructions that calculate the direct memory offset from the array’s base address.
  • Multi-dimensional arrays (T[,]): The compiler emits call instructions to the multi-dimensional array’s intrinsic Get and Set methods rather than using direct element-loading instructions.
  • Bounds Checking: The CLR automatically injects bounds-checking instructions before memory access. If the index falls outside the allocated memory range, the runtime throws an IndexOutOfRangeException.

2. Custom Types (Indexers)

When applied to a class, struct, or interface, the [] operator is syntactic sugar for invoking an indexer. Indexers are not restricted to zero-based integers; they can accept any type as an argument (e.g., string in a dictionary).
  • MSIL Translation: The compiler translates the [] syntax into calls to underlying methods, typically named get_Item and set_Item.
  • Overloading: While the [] operator cannot be overloaded directly via the operator keyword, types can define multiple indexers with different signatures (e.g., this[int i], this[string key]).
  • Attribute Override: The default Item name in the emitted IL can be modified using the [IndexerName("NewName")] attribute.

3. Null-Conditional Element Access (?[])

The ?[] operator provides null-safe element access. If the base expression evaluates to null, the operator short-circuits and avoids invoking the indexer or array access, which prevents a NullReferenceException. As a consequence of this short-circuiting, the index expressions inside the brackets are not evaluated, preventing any unintended side-effects from those expressions. Crucially, the ?[] operator performs type lifting. If the underlying array or indexer returns a non-nullable value type (e.g., int), using the ?[] operator changes the expression’s compile-time return type to Nullable<T> (e.g., int?). Because of this type lifting, attempting to assign the result directly to a non-nullable value type (e.g., int item = collection?[0];) will result in a compiler error.

4. Index Initializers

During object initialization, the [] operator functions as an index initializer. Expressions utilizing this syntax (e.g., new Dictionary<string, int> { ["key"] = 1 }) are translated by the compiler into an object instantiation followed by direct calls to the type’s set_Item indexer method for each specified key.

5. Pointers (Unsafe Context)

In an unsafe context, applying the [] operator to a pointer (T*) performs raw pointer arithmetic.
  • Equivalence: The expression p[i] is strictly evaluated as *(p + i).
  • Memory Safety: Unlike arrays, pointer element access bypasses all CLR bounds checking. The developer is responsible for ensuring the calculated memory address is valid.

Modern C# Integration (System.Index and System.Range)

The [] operator natively supports System.Index and System.Range types, altering its mechanical evaluation:
  • Index (^ operator): When passed an Index (e.g., collection[^1]), the compiler translates this into an offset from the end of the collection. For arrays, collection[^i] is compiled as collection[collection.Length - i].
  • Range (.. operator): When passed a Range (e.g., collection[1..4]), the [] operator does not return a single element. Instead, it returns a slice.
    • For arrays, this allocates and returns a new array.
    • For Span<T> or types implementing a Slice(int start, int length) method, it returns a non-allocating view of the memory.

Evaluation Order

When the [] operator is invoked, the expressions are evaluated strictly from left to right. The base collection expression is evaluated first, followed by the index expressions in the exact order they appear within the brackets.
Master C# with Deep Grasping Methodology!Learn More