Back to Blog
C#

C# Index Initializer: Syntax and Usage

c# index initializer: Learn how to use index initializers in C# to set indexer values during object creation, with syntax examples and practical usage patterns.

C#Index InitializerObject InitializerIndexer.NET
Diagram showing object initialization with indexer assignment in C#.

The C# index initializer is a concise way to set values on an indexer while creating an object. It is part of the object initializer syntax introduced in C# 6. Instead of assigning to an indexer after construction, you can embed those assignments directly in the initializer block. This article explains the syntax, how it works with built-in types like Dictionary<TKey, TValue>, and how to use it with custom indexers. It also covers common mistakes and the performance implications you should keep in mind.

What Is an Index Initializer in C#?

An index initializer is a syntactic feature that lets you assign to an indexer during object creation. Consider a typical object initializer that sets properties:

var person = new Person { Name = "Alice", Age = 30 };

Index initializers extend this pattern to indexers. For example, if you have a class with an indexer, you can write:

var matrix = new Matrix { [0, 0] = 1, [1, 1] = 2 };

This is equivalent to creating the object and then assigning each indexer element separately. The compiler expands the initializer into a sequence of assignments after the constructor runs. This feature is especially useful when you want to populate a dictionary or a custom collection in a single expression, reducing repetitive code.

Basic Syntax for Index Initializers

The syntax follows the same pattern as property initializers, but instead of a property name, you use an indexer argument list in square brackets. The general form is:

var obj = new Type { [index] = value, [index1, index2] = value2 };

The indexer arguments must match the signature of the indexer you are targeting. For a single-dimensional indexer, you provide one argument. For a multi-dimensional indexer, you provide multiple arguments separated by commas. The assignment value must be implicitly convertible to the indexer's return type.

Here is a minimal example with a custom class:

public class Bag { private readonly Dictionary<string, int> _items = new(); public int this[string key] { get => _items[key]; set => _items[key] = value; } } var bag = new Bag { ["apple"] = 3, ["banana"] = 5 };

This creates a Bag and immediately sets two entries. The code is more compact than the alternative:

var bag = new Bag(); bag["apple"] = 3; bag["banana"] = 5;

The initializer version is not only shorter but also keeps the initialization logic in one place, which can improve readability when the object is configured with several values.

Using Index Initializers with Dictionaries

The most common use of index initializers is with Dictionary<TKey, TValue>. Before C# 6, developers often used collection initializers to populate dictionaries, which required a specific Add method. Index initializers offer a more direct approach because they rely on the indexer rather than the Add method.

Consider the following dictionary creation:

var scores = new Dictionary<string, int> { ["Alice"] = 90, ["Bob"] = 85, ["Charlie"] = 92 };

This is equivalent to:

var scores = new Dictionary<string, int>(); scores["Alice"] = 90; scores["Bob"] = 85; scores["Charlie"] = 92;

The index initializer calls the indexer setter, which for Dictionary<TKey, TValue> adds a new key or overwrites an existing one. This behavior is identical to a direct assignment. If you want to ensure that keys are unique and throw on duplicates, you would use a collection initializer with Add, which throws when a key already exists. Index initializers do not enforce uniqueness; they simply assign, so duplicate keys in the initializer will silently overwrite earlier values.

This distinction matters when you are building a dictionary from a known set of values and want to catch accidental duplicates at compile time or runtime. The choice between index and collection initializers depends on whether you want to replace existing values or fail on duplicates.

Custom Indexers and Index Initializers

Index initializers work with any class or struct that exposes an indexer. The indexer must have a setter that is accessible from the calling context. If the indexer is read-only, the initializer will not compile. The same accessibility rules apply as for property initializers: the setter must be at least as accessible as the initializer context.

Here is an example with a custom indexer that validates input:

public class TemperatureTable { private readonly double[] _values = new double[10]; public double this[int index] { get => _values[index]; set { if (index < 0 || index >= _values.Length) throw new ArgumentOutOfRangeException(nameof(index)); _values[index] = value; } } } var table = new TemperatureTable { [0] = 21.5, [1] = 22.0, [2] = 20.8 };

The initializer calls the setter for each entry, so validation logic in the setter runs as expected. This is useful when you need to enforce invariants during initialization. You can also use index initializers with multi-dimensional indexers:

public class Grid { private readonly int[,] _cells = new int[3, 3]; public int this[int row, int col] { get => _cells[row, col]; set => _cells[row, col] = value; } } var grid = new Grid { [0, 0] = 1, [0, 1] = 2, [1, 0] = 3 };

In this case, the indexer takes two arguments, and the initializer supplies them as a comma-separated list inside the brackets. The compiler matches the arguments to the indexer signature, so the types must align.

Index Initializers vs. Collection Initializers

Collection initializers and index initializers are often confused because both can populate a collection during creation. The key difference is the underlying mechanism: collection initializers call an Add method, while index initializers call the indexer setter. This leads to different behaviors in terms of duplicate handling and the types that can be initialized.

Collection initializers require the type to implement IEnumerable and have an accessible Add method. Index initializers only require an accessible indexer setter. Many collection types implement both, but some only have one. For example, List<T> has an Add method but no indexer setter that can be used in an initializer because the indexer is read-only. Conversely, a custom type might expose a write-only indexer but no Add method.

The following table summarizes the main differences:

FeatureCollection InitializerIndex Initializer
MechanismCalls Add methodCalls indexer setter
Duplicate handlingThrows if Add rejects duplicatesOverwrites existing value
Required memberIEnumerable and AddIndexer with setter
Typical useLists, sets, and custom collectionsDictionaries and indexer-based types

In practice, you should choose based on the semantics you need. If you are building a dictionary and want to replace values, use an index initializer. If you want to guarantee that all keys are unique and fail on duplicates, use a collection initializer with Add.

Common Mistakes and Edge Cases

One common mistake is assuming that index initializers work with arrays. Arrays have indexers, but you cannot use an index initializer to create an array because the array type does not have a parameterless constructor that would be used in an object initializer. You would use an array initializer instead:

int[] numbers = { 1, 2, 3 }; // correct var arr = new int[] { [0] = 1 }; // compile error

Another edge case involves indexers that return by reference or have ref returns. Index initializers cannot be used with ref returning indexers because the setter is not a simple assignment; it would require a ref assignment, which is not supported in this context.

Also, be aware that index initializers are evaluated in the order they appear. If the indexer setter has side effects, the order matters. For example, if you are counting assignments, the count will reflect the order in the initializer. This is usually not an issue, but it can be surprising if you rely on the sequence of setter calls.

Finally, index initializers cannot be used with anonymous types because anonymous types do not have indexers. They are limited to named types that expose an indexer.

Performance and Maintainability Considerations

From a performance perspective, index initializers are not a special runtime feature. The compiler translates them into ordinary indexer assignments after the constructor call. There is no extra allocation or boxing overhead compared to writing the assignments manually. The only cost is the same as calling the indexer setter directly. For Dictionary<TKey, TValue>, each assignment performs a hash lookup and an insertion or update, which is the same as a direct assignment.

One subtle difference is that the object is fully constructed before the initializer runs, so the constructor has already executed. This means any side effects in the constructor happen before the indexer assignments. If the constructor initializes internal state that the indexer depends on, that state is ready when the initializer executes.

Maintainability is where index initializers shine. They group all indexer assignments into a single expression, making the code more declarative and easier to read. This is particularly valuable when you have a configuration object that is populated with many values. However, if the initializer becomes very long, it may hurt readability. In that case, consider breaking the initialization into separate methods or using a builder pattern.

Another maintainability benefit is that index initializers work well with object initializers for nested objects. You can combine property initializers and index initializers in the same block:

var config = new Configuration { Name = "Production", ["retryCount"] = 5, ["timeout"] = 30 };

This allows you to set both properties and indexer values in one place, which can reduce the number of separate statements and make the configuration intent clearer.

When using index initializers with custom types, ensure that the indexer setter is efficient because it will be called for each entry. If the setter performs expensive validation or I/O, the initialization cost will be multiplied. In such cases, you might prefer a dedicated Add method that can batch operations or defer work. But for typical in-memory collections, the overhead is negligible.

Overall, index initializers are a safe and readable feature. They do not introduce hidden costs, and they integrate cleanly with existing object initializer syntax. The main decision is whether you want overwrite semantics or duplicate detection, which depends on the type and the intent of the initialization.

c# index initializer: Practical Usage and Code Examples | RYUSLOG DEV