Implicit Indexer Access in Object Initialisers
C# 13 fills a small but noticeable gap: you can now use the ^ (index from end) operator inside object initialisers. Previously, while [0] and [1] worked fine in initialisers, [^1] and [^2] did not. This inconsistency has been resolved.
The Gap That Existed
C# 8 introduced index and range operators. The ^ operator creates an Index that counts from the end of a collection:
int[] numbers = [1, 2, 3, 4, 5];
var last = numbers[^1]; // 5
var penultimate = numbers[^2]; // 4
Object initialisers have supported indexer assignment since C# 3:
var buffer = new Buffer(size: 10)
{
[0] = "first",
[1] = "second"
};
But combining the two did not compile before C# 13:
// Before C# 13: Error
var buffer = new Buffer(size: 10)
{
[^1] = "last" // CS0131: The left-hand side of an assignment must be a variable
};
What Changed
In C# 13, this works as expected:
var buffer = new Buffer(size: 10)
{
[0] = "first",
[1] = "second",
[^2] = "penultimate",
[^1] = "last"
};
The compiler translates [^1] into the appropriate indexer call, just as it does outside of initialisers.
Practical Example: Initialising Grids and Buffers
This is useful when setting up data structures where you know the size and want to populate specific positions from both ends:
public class Grid
{
private readonly string[,] _cells;
public Grid(int rows, int cols) => _cells = new string[rows, cols];
public string[] this[int row]
{
// Simplified for illustration
get => Enumerable.Range(0, _cells.GetLength(1))
.Select(c => _cells[row, c]).ToArray();
set
{
for (int c = 0; c < value.Length; c++)
_cells[row, c] = value[c];
}
}
}
More commonly, this applies to types with Index-based indexers:
public class RingBuffer<T>
{
private readonly T[] _items;
public RingBuffer(int capacity) => _items = new T[capacity];
public T this[Index index]
{
get => _items[index];
set => _items[index] = value;
}
public int Length => _items.Length;
}
var buffer = new RingBuffer<string>(5)
{
[0] = "start",
[^1] = "end"
};
Timer and Countdown Patterns
Consider initialising a countdown display:
public class CountdownSlots
{
private readonly string[] _slots;
public CountdownSlots(int size)
{
_slots = new string[size];
Array.Fill(_slots, "---");
}
public string this[Index i]
{
get => _slots[i];
set => _slots[i] = value;
}
public int Count => _slots.Length;
}
var countdown = new CountdownSlots(10)
{
[0] = "GO!",
[^1] = "Ready",
[^2] = "Set"
};
Without ^ support in initialisers, you would need separate assignment statements after construction, breaking the declarative style.
How It Is Lowered
The compiler translates the ^ operator in initialisers the same way it does elsewhere. Given a type with a Length or Count property and an int-based indexer:
// What you write
var buf = new RingBuffer<string>(5)
{
[^1] = "end"
};
// What the compiler generates (approximately)
var buf = new RingBuffer<string>(5);
buf[buf.Length - 1] = "end";
For types that accept Index directly in their indexer, the compiler passes the Index value as-is.
When This Matters
This feature matters most for types that use Index-based indexers — custom collections, buffers, wrappers around arrays. If you are designing collection types and want them to feel natural in object initialisers, ensure your indexer accepts System.Index and that your type has a Count or Length property.
It is a consistency fix more than a capability expansion. Code that had to work around the limitation — using separate assignment statements or helper methods — can now be written in the declarative style that object initialisers are designed for. Small wins like this reduce cognitive overhead and keep the language feeling coherent.