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.