The Hidden C# Feature: Collection Initializers

In C# there is a handy feature which is often used, but whose definition is rather invisible: Collection Initializers. With Collection Initializers, collections can be filled with content directly during initialization. This looks like this:

string[] array1 = { "x", "y", "z" };
string[] array2 = new[] { "x", "y", "z" };
string[] array3 = new string[] { "x", "y", "z" };

List<string> list1 = new List<string> { "x", "y", "z" };
List<string> list2 = new List<string>() { "x", "y", "z" };
List<string> list3 = new List<string>(3) { "x", "y", "z" };
List<string> list4 = new() { "x", "y", "z" };

Dictionary<string, string> dictionary1 = new Dictionary<string, string>()
{
    { "x1", "x2" },
    { "y1", "y2" }
};

Dictionary<string, string> dictionary2 = new Dictionary<string, string>()
{
    ["x1"] = "x2",
    ["y1"] = "y2"
};

For arrays, you can simply specify the desired contents in curly braces at initialization. Alternatively, you can write new[] or, for example, new string[] before the curly braces. For other types that support this feature, new must always be specified. For example, in dictionaries where an item consists of key and value, the individual items can be combined with curly braces.

How to implement this behavior?

It looks like a certain interface or attribute must be implemented. But this is only partially the case. The secret is a method called Add. Additionally, the class must implement the IEnumerable or IEnumerable<T> interface, but this is common for collections anyway. If a class has at least one Add method and implements IEnumerable, Collection Initializers are possible. Variations of the name Add are not supported, neither add nor AddItem.

There can also be multiple Add methods with any number of parameters and also parameter arrays are supported. If the Add method is to be called with multiple parameters, the arguments must be grouped together in curly braces.

If the collection also has a settable Indexer, the key can be written in square brackets and the value can be specified with =.

Example

To demonstrate the feature, the class for a simple example:

namespace CollectionInitializers
{
    public class MyCollection : IEnumerable
    {
        private readonly List<string> _items = new List<string>();

        public void Add(string item)
        {
            _items.Add($"'{item}'");
        }

        public void Add(int item)
        {
            _items.Add(item.ToString());
        }

        public void Add(int number1, int number2)
        {
            _items.Add($"{number1} + {number2} = {number1 + number2}");
        }

        public void Add(params string[] items)
        {
            _items.Add("(" + string.Join(", ", items) + ")");
        }

        public string? this[int key]
        {
            get
            {
                return _items.FirstOrDefault(x => x.StartsWith($"{key} = "));
            }
            set
            {
                _items.Add($"{key} = {value}");
            }
        }

        public string? this[int key1,  int key2]
        {
            get
            {
                return _items.FirstOrDefault(x => x.StartsWith($"({key1}, {key2}) = "));
            }
            set
            {
                _items.Add($"({key1}, {key2}) = {value}");
            }
        }

        // IEnumerable
        // ...
    }
}

The class provides the various Add methods and Indexers, which can be called as in the following example:

MyCollection col1 = new MyCollection()
{
    99,
    "x",
    "y"
};

MyCollection col2 = new MyCollection()
{
    { 1, 2 },
    { "x", "y", "z" },
    "z"
};

MyCollection col3 = new MyCollection()
{
    [1] = "X",
    [2, 3] = "Y"
};

This is the equivalent for:

MyCollection col1 = new MyCollection();
col1.Add(99); // int
col1.Add("x"); // string
col1.Add("y"); // string

MyCollection col2 = new MyCollection();
col2.Add(1, 2); // int, int
col2.Add("x", "y", "z"); // params string[]
col2.Add("z"); // string

MyCollection col3 = new MyCollection();
col3[1] = "X"; // indexer[int] = string
col3[2, 3] = "Y"; // indexer[int, int] = string

A working project with the examples can be found in my GitHub repository.

Conclusion

The feature is just one of the few Duck Typing features in C#, which means, they work by names. To use Collection Initializers, the method to add should exactly be named Add for custom collections instead of AddItem or whatever.