Collection Initialization - Fast, Readable Ways to Fill Arrays and Collections

Vaibhav • September 10, 2025

When you start working with arrays and collections, you’ll very quickly run into a common pattern: create a container, then put stuff in it. Collection initialization syntax in C# helps you do both at once, making your code shorter, clearer, and less error-prone. In this article, you’ll learn the standard initialization forms for arrays, List<T>, Dictionary<TKey,TValue>, HashSet<T>, and a few practical patterns to make your everyday code simpler.

Why initialization syntax matters

You can always allocate a collection and then add items one by one. But that quickly turns into boilerplate:

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

Initialization syntax lets you declare the container and seed it with values in a single expression. This improves readability (the data is visible up front) and reduces the chance you’ll forget an Add call.

Array initialization

Arrays have two common initializer forms: implicit length and explicit length.

Implicit length

int[] scores = { 10, 20, 30, 40 };
  • int[] declares an array of integers.
  • The braces { ... } list the elements in order.
  • The compiler infers the length (here, 4) based on how many values you provided.

Explicit length

string[] names = new string[3] { "Ada", "Linus", "Grace" };
  • new string[3] allocates an array that can hold exactly three strings.
  • The initializer must provide exactly three items; otherwise, it won’t compile.

Initializing List<T>

List<T> is the go-to resizable sequence. A collection initializer can seed it with items immediately:

var primes = new List<int> { 2, 3, 5, 7, 11 };
  • new List<int> creates an empty list.
  • The braces after it call Add for each element internally (as if you wrote multiple Add lines).
  • Reading the right-hand side tells you both the type and the data at a glance.

Multi-line for readability

var todo = new List<string>
{
    "Write unit tests",
    "Refactor parser",
    "Review PR #42"
};

Spreading initializers over multiple lines is common for clarity, especially when items are longer strings or small objects.

Capacity hint (optional)

var values = new List<int>(capacity: 5) { 1, 2, 3, 4, 5 };

Supplying an initial capacity can reduce internal resizing if you roughly know how many items you’ll add. This is a micro-optimization; use it when performance measurements suggest it helps.

Initializing Dictionary<TKey,TValue>

Dictionaries map keys to values. Their initializer uses nested braces where each entry is a pair:

var countryCodes = new Dictionary<string, string>
{
    { "IN", "India" },
    { "US", "United States" },
    { "DE", "Germany" }
};
  • Each inner pair { key, value } behaves like a call to Add(key, value).
  • All keys must be unique; duplicating a key throws an exception during initialization.

Index-initializer syntax

var httpStatus = new Dictionary<int, string>
{
    [200] = "OK",
    [404] = "Not Found",
    [500] = "Server Error"
};

This reads naturally when keys are literals or simple expressions, and it avoids the extra braces and commas inside each pair.

Initializing HashSet<T> for unique items

A HashSet<T> keeps only unique items. Initializing it looks just like a list, but duplicates are ignored (or rather, not added):

var features = new HashSet<string>
{
    "DarkMode",
    "Offline",
    "Export",
    "Export" // duplicate, set will contain it only once
};
  • Each string is attempted to be added.
  • The set’s uniqueness rule ensures only distinct values remain.
  • Order is not guaranteed; if you need ordering, stick with List<T> or sort later.

Object initializers inside collection initializers

Sometimes you need a list of small objects with a few public fields or properties. You can combine an object initializer with a collection initializer to keep everything compact and readable:

public class TaskItem
{
    public string Title { get; set; }
    public int Priority { get; set; }
}

var backlog = new List<TaskItem>
{
    new TaskItem { Title = "Auth flow", Priority = 1 },
    new TaskItem { Title = "Profile page", Priority = 2 },
    new TaskItem { Title = "Export CSV", Priority = 3 }
};
  • TaskItem is a simple class with two auto-implemented properties.
  • Each new TaskItem { ... } uses an object initializer to set properties at creation time.
  • The outer list initializer collects those objects into a single list expression.

Nesting: collections of collections

Initializers can nest to express more complex shapes, like a list of arrays or a list of lists:

// A list of int arrays
var matrix = new List<int[]>
{
    new int[] { 1, 2, 3 },
    new int[] { 4, 5, 6 },
    new int[] { 7, 8, 9 }
};

// A list of lists (jagged)
var groups = new List<List<string>>
{
    new List<string> { "Alice", "Bob" },
    new List<string> { "Charlie" },
    new List<string> { "Dana", "Eve", "Frank" }
};

Keep nesting shallow in real code. If the structure grows complex, extract small helper methods to construct each piece so your top-level code stays readable.

When to choose an initializer vs. adding later

Use initializers when the data is clear and fixed. If the elements depend on computations, loops, or conditionals, initialize the empty collection and add items in code so each step is explicit.

// Computed data: clearer to build in a loop
var squares = new List<int>();
for (int i = 1; i <= 5; i++)
{
    squares.Add(i * i);
}

This form makes the calculation obvious and easier to modify (e.g., changing the range or formula).

Common mistakes to avoid

  • Trailing commas in dictionaries: Remember to separate entries with commas. Missing a comma between dictionary pairs is a frequent source of compile errors.
  • Wrong key types: Ensure your key literal matches TKey (e.g., [200] for int, not "200").
  • Assuming ordering: Dictionary and HashSet do not guarantee ordering. If you display items, consider ordering them after initialization.
  • Giant initializers: Long lists in code can hide meaning. If the initializer grows beyond a screen, consider loading data from a file or constructing it with small helpers.

Practical mini-project: seeding reference data

A classic use for collection initialization is “reference data” - small, fixed sets of values your program needs at startup (think country codes, roles, or menu options). Here’s a tiny example that seeds lookup data and uses it:

// Reference data seeded with a dictionary initializer
var months = new Dictionary<int, string>
{
    [1] = "January",
    [2] = "February",
    [3] = "March",
    [4] = "April",
    [5] = "May",
    [6] = "June",
    [7] = "July",
    [8] = "August",
    [9] = "September",
    [10] = "October",
    [11] = "November",
    [12] = "December"
};

// Using the seeded data
int userMonth = 9;
if (months.TryGetValue(userMonth, out string name))
{
    Console.WriteLine($"Month {userMonth} is {name}");
}
else
{
    Console.WriteLine("Unknown month");
}
  1. The dictionary initializer seeds all 12 months in one place, making the mapping easy to scan and verify.
  2. TryGetValue safely looks up a key and gives you the corresponding value if it exists.
  3. All of this stays within simple collections and control flow - perfect for beginners and small utilities.

Performance notes (brief)

Initialization syntax itself doesn’t make collections faster; it just makes your code clearer. For List<T>, supplying a capacity can avoid a few resizes. For dictionaries with many entries, you can pass an initial capacity as well:

var map = new Dictionary<int, string>(capacity: 100)
{
    [1] = "One",
    [2] = "Two"
    // ... more entries later
};

Don’t overthink it: start simple, measure when needed, and then tweak capacity for hot paths.

Checklist: picking the right initializer

  • Array: Fixed size, fast indexing, values known at compile time.
  • List<T>: Ordered, resizable sequence; use for most dynamic lists.
  • Dictionary<K,V>: Fast key lookup; seed with pairs or index-initializers.
  • HashSet<T>: Uniqueness with fast membership checks.

Summary

Collection initialization condenses “create + populate” into a single, readable expression. Arrays can be filled with brace initializers; List<T> accepts a simple comma-separated list; Dictionary<K,V> supports both pair and index styles; and HashSet<T> makes uniqueness effortless. Use initializers for small, known datasets, tests, and reference data. When your data is computed or complex, initialize an empty collection and build it step by step in code. Start with clarity; measure performance only when necessary. With these patterns, your programs become easier to read, audit, and maintain.