Back to Blog
C#

C# Collection Initializer Syntax and Behavior

c# collection initializer: Learn how C# collection initializers work, how the compiler translates them, where they perform best, and where they create hidden costs.

C# collectionsobject initializersList<T> initializerdictionary initializerC# syntax
A diagram showing C# collection initializer syntax translating into individual Add method calls on a collection

The C# collection initializer lets you populate a collection at construction time without writing multiple Add calls or a separate loop. For example, new List<int> { 1, 2, 3 } reads more cleanly than the equivalent sequence of three Add invocations. The syntax is straightforward, but what happens behind the code is more subtle. Understanding that behavior matters when you move beyond simple lists and start initializing dictionaries, custom collections, or collections inside object initializers.

How the Compiler Translates Collection Initializers

A collection initializer is not a special constructor. The C# compiler requires the target type to implement IEnumerable and to have an accessible Add method. When you write:

var numbers = new List<int> { 1, 2, 3 };

the compiler expands it to:

var numbers = new List<int>(); numbers.Add(1); numbers.Add(2); numbers.Add(3);

There is no hidden bulk-insert optimization. Each element is added sequentially through the normal Add method. For most collection types this is fine, but it is worth knowing when you are working with large collections or types where Add has overhead beyond a simple append.

Because the expansion is based on Add, you can use a collection initializer on any type that exposes an appropriate Add method, not just BCL collections. That type must implement IEnumerable, but it does not need to be a standard List or Dictionary. The same rule applies to indexers: C# 6 and later allow collection initializers to target indexers as well as Add methods, which is how dictionary initializers work.

Dictionary Initializers and Indexer Semantics

When you write a dictionary initializer like this:

var lookup = new Dictionary<string, int> { ["one"] = 1, ["two"] = 2 };

the compiler generates calls to the indexer setter rather than to Add:

var lookup = new Dictionary<string, int>(); lookup["one"] = 1; lookup["two"] = 2;

This distinction matters because the two approaches fail differently. Dictionary<TKey, TValue>.Add throws on a duplicate key, while assigning to the indexer silently overwrites an existing key. So the following code throws an exception at runtime:

var duplicateKeys = new Dictionary<string, int> { ["key"] = 1, ["key"] = 2 };

But this code does not, because the second assignment replaces the first value:

var duplicateKeys = new Dictionary<string, int>(); duplicateKeys["key"] = 1; duplicateKeys["key"] = 2;

If you prefer explicit failure on duplicates, use the Add method or a collection initializer that uses Add (for example, new SortedList<,> uses Add). If you want last-write-wins behavior, the indexer is the right choice. The syntax itself does not tell you which one is happening unless you know the target type.

Using Collection Initializers Inside Object Initializers

A common pattern is initializing a collection property while constructing an object. For example:

public class Order { public int Id { get; set; } public List<OrderLine> Lines { get; set; } = new List<OrderLine>(); }

Then you create an order like this:

var order = new Order { Id = 42, Lines = { new OrderLine { Product = "Laptop", Quantity = 1 }, new OrderLine { Product = "Mouse", Quantity = 2 } } };

The compiler does not create the Lines list; it assumes the property already points to a list. If Lines is null, this code throws a NullReferenceException. The initializer syntax works because the compiler treats Lines = { ... } as a call to order.Lines.Add(...) for each element. That means the property must be initialized before the object initializer runs, either through a field initializer, a constructor, or a getter that returns a non-null instance.

If you change the property definition to an auto-property without initialization:

public List<OrderLine> Lines { get; set; }

the same object initializer fails at runtime. The fix is to initialize the property in the constructor or to use the alternative object initializer syntax that creates a new list first:

var order = new Order { Id = 42, Lines = new List<OrderLine> { new OrderLine { Product = "Laptop", Quantity = 1 } } };

This second form assigns a brand new list to the property, so it works regardless of whether the property had a default value. The distinction is subtle and a frequent source of bugs when refactoring a class that used to initialize the property but later removed that initialization.

Performance and Allocation Characteristics

Collection initializers do not add meaningful overhead compared to manual Add calls because the compiler inlines the calls in release builds. The main performance consideration is the behavior of the underlying collection type. For List<T>, each Add may cause an internal array resize. If you already know the capacity, you can pass it to the constructor:

var list = new List<int>(1000) { 1, 2, 3 };

This reserves capacity up front and avoids repeated resizing during the initial population. For very large collections, that can reduce memory churn and time. But the collection initializer syntax itself does not let you supply capacity; you must combine the constructor argument with the initializer. The same pattern applies to HashSet<T> and Dictionary<TKey, TValue>, where you can provide an initial capacity or a comparer in the constructor while still using the initializer for elements.

Another allocation point appears when you use a collection initializer in a hot path, such as inside a loop that creates many small collections. Each initializer creates a new collection instance and adds elements one by one. If the workload is allocation-sensitive, consider whether a reusable collection or a different construction strategy reduces pressure. In most application code this is not a bottleneck, but it becomes relevant in tight loops that process thousands of requests per second.

Custom Collections and Add Method Overloads

Because a collection initializer relies on an Add method, you can use it with custom collection types. For example:

public class EventList : IEnumerable<Event> { private readonly List<Event> _events = new List<Event>(); public void Add(Event item) => _events.Add(item); public IEnumerator<Event> GetEnumerator() => _events.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }

You can now write:

var events = new EventList { new Event("start"), new Event("stop") };

The compiler sees the Add(Event) method and generates calls to it. This is convenient, but it also means that adding a second Add overload can change which method the compiler picks. If you have both Add(Event) and Add(string), the initializer with a string argument will call the string overload. That can lead to surprising behavior if the two methods differ in side effects or validation. Keep the Add methods clear and avoid ambiguous overloads when the initializer is part of your public API.

Also note that the IEnumerable requirement applies to the collection type itself, not to the element type. A type that implements IEnumerable but has no usable Add method will produce a compile error if you try to use a collection initializer. The error message varies by compiler version, but the root cause is the missing Add.

Collection Initializers with Indexers

C# 6 introduced support for indexers in collection initializers. This allows you to write:

var matrix = new Dictionary<(int row, int col), double> { [(1, 1)] = 0.5, [(1, 2)] = 0.75 };

The compiler translates this to indexer assignments, not to Add. This behavior is useful when you want to overwrite existing entries or when the collection type only exposes an indexer. But it also means that the same code can behave differently depending on whether the target type uses an indexer or an Add method. For instance, a List<T> does not have a settable indexer that accepts an index greater than the current count, so you cannot use an indexer initializer to append to a list. The list would throw an ArgumentOutOfRangeException. Stick to Add semantics for list-like collections.

Where Collection Initializers Break: Null and Read-Only Properties

A collection initializer inside an object initializer fails when the property is null or when the property is read-only and not pre-populated. The most common case is null. The compiler does not generate code to check for null or to create the collection; it simply calls Add on the property reference. If the property is null, you get a NullReferenceException at runtime.

Read-only properties behave similarly. Consider:

public class Team { public List<string> Members { get; } = new List<string>(); }

You can initialize Members because it has a getter and it is already set to a non-null list. If you removed the initializer from the property, the same pattern would throw. This behavior is consistent with the general rule: the initializer is just sugar for a sequence of Add calls on an existing instance. If that instance does not exist, the sugar does not create it.

When you design a class for object initializers, decide whether the property should always exist (initialize it in the constructor or field) or whether you want callers to have the flexibility to assign a new collection. If you choose the latter, document that the property must be assigned before elements can be added. The object initializer syntax cannot distinguish between "add to existing" and "create new" unless you explicitly write Property = new Collection { ... }.

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