C# Dictionary Initialization: Syntax and Tradeoffs
Learn the main ways to initialize a Dictionary in C#: collection initializers, constructors, Add methods, and LINQ, with performance tradeoffs and common pitfalls.
The most common way to initialize a C# dictionary is with a brace initializer at construction time. The collection-initializer form calls Add:
var config = new Dictionary<string, string> { { "host", "localhost" }, { "port", "8080" }, { "timeout", "30" } };
The indexer-initializer form is also common:
var config2 = new Dictionary<string, string> { ["host"] = "localhost", ["port"] = "8080", ["timeout"] = "30" };
Dictionary<TKey, TValue> supports collection initializers because it implements IEnumerable and has a public Add(TKey key, TValue value) method. The explicit ICollection<KeyValuePair<TKey, TValue>>.Add implementation is not used by the collection-initializer element syntax; that is why the element must be written as { key, value } rather than as a bare KeyValuePair object. The { key, value } form is expanded into calls to the two-argument Add method. The ["key"] = value form is an indexer initializer; the compiler expands it into indexer assignments, not Add calls. That difference matters for duplicate keys: the indexer form overwrites an existing value silently, while the two-argument Add form throws an ArgumentException. Both forms are valid inside a dictionary initializer, but they are not behaviorally identical.
Initializing with the Constructor and Capacity
If you know how many entries the dictionary will hold, you can pass an initial capacity to the constructor:
var lookup = new Dictionary<string, int>(100);
This allocates enough internal storage to hold at least 100 entries before resizing. Without an explicit capacity, the dictionary starts with a small default size and grows as you add elements. Each resize allocates a new internal array and copies existing entries, which costs time and memory. Setting a reasonable initial capacity avoids those resizes when the final size is known or can be estimated. This is especially relevant when you are building a dictionary from a large collection and want to minimize reallocation overhead.
Using the Add Method for Incremental Initialization
When you need to build a dictionary conditionally or in a loop, the Add method is the standard approach:
var errors = new Dictionary<string, string>(); if (input.Name == null) { errors.Add("name", "Name is required."); } if (input.Age < 0) { errors.Add("age", "Age cannot be negative."); }
This pattern is clear because each Add call is a separate statement and can be guarded by a condition. It also allows you to start with an empty dictionary and populate it based on runtime logic. The main caveat is that Add throws an ArgumentException if the key already exists. If you want to overwrite an existing key, use the indexer assignment instead:
errors["name"] = "Updated message";
Initializing from Another Collection or LINQ
You can create a dictionary from an existing collection using LINQ's ToDictionary method. This is useful when you have a sequence of objects and want to project a key and value from each element:
var users = GetUsers(); var userById = users.ToDictionary(u => u.Id, u => u.Name);
ToDictionary returns a Dictionary<TKey, TValue> and throws an ArgumentException if duplicate keys appear. You can also pass a custom equality comparer as a third argument if you need case-insensitive keys or other custom comparison logic. This approach is concise but does not let you set an initial capacity directly; the dictionary grows as items are added, so for very large collections you may want to measure whether the overhead matters.
Performance Considerations for Initialization
Dictionary initialization affects runtime performance through memory allocation and hash computations. Every insertion path in dictionary initialization—collection initializer, indexer initializer, explicit Add, or LINQ's ToDictionary—inserts an entry and computes a hash for its key. When the dictionary needs to grow, it allocates a new internal array and re-hashes existing entries; that is an O(n) operation per resize. Repeated resizing while adding many entries can be costly for large dictionaries.
Setting an initial capacity in the constructor reduces the number of resizes when the final size is known or can be estimated. If you know the approximate number of entries, you can avoid most reallocation overhead. However, over-allocating capacity wastes memory because the internal storage is sized at least to the requested capacity, not the actual count. If you request a capacity much larger than needed, the extra memory is held until the dictionary is garbage collected.
Another performance detail is the equality comparer. By default, Dictionary<TKey, TValue> uses EqualityComparer<TKey>.Default, which for strings performs case-sensitive ordinal comparison. If you need case-insensitive keys, you can pass StringComparer.OrdinalIgnoreCase to the constructor. This affects how keys are hashed and compared, and it is set once at initialization. Choosing the right comparer can prevent subtle bugs and improve correctness; set it once at initialization so all later lookups and inserts behave consistently.
Common Pitfalls and Edge Cases
Duplicate keys are the most frequent mistake in dictionary initialization. The indexer-initializer syntax silently overwrites a previous entry, while the two-argument Add form throws. For example:
var dict = new Dictionary<string, int> { ["a"] = 1, ["a"] = 2 // overwrites, no exception }; var dict2 = new Dictionary<string, int> { { "a", 1 }, { "a", 2 } // throws ArgumentException };
Null keys are not allowed in a Dictionary<TKey, TValue>; every initialization approach throws an ArgumentNullException if the key is null. This is true for nullable reference types as well, because nullable annotations are compile-time-only. If you use a Nullable<T> value type as the key, a null value is also rejected by the dictionary. If a key might be missing, use a sentinel value or model the absence outside the dictionary rather than using a null key.
Case sensitivity is another common issue. Two keys that differ only by case are considered distinct by default. If your data is case-insensitive, you must pass a comparer at initialization. This is a decision you make once, and it affects all subsequent lookups and inserts.
Choosing the Right Initialization Approach
The choice of initialization syntax depends on the source of the data and the required behavior. Use a brace initializer when you have a fixed set of key-value pairs known at compile time; it is concise and self-documenting. Use the constructor with an initial capacity when you are building a dictionary from a large collection and want to minimize resizing overhead. Use the Add method when entries are added conditionally or in a loop, because it allows you to separate the logic into clear statements. Use ToDictionary when you are projecting from an existing sequence and want a one-line transformation. For all approaches, decide on the equality comparer early and pass it to the constructor if you need custom key comparison. This avoids subtle bugs and ensures consistent behavior throughout the dictionary's lifetime.