Fun with Custom C# Collection Initializers
In object-oriented programming, classes can define instance properties to hold some data. Those properties can be populated with values once an object has been created:
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://mariusschulz.com");
httpClient.Timeout = TimeSpan.FromSeconds(10);
Alright, trivial so far.
#Object Initializers
To make property assignments after instantiation a little less redundant, C# 3 introduced object initializers which save you from having to repeatedly type the variable name:
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://mariusschulz.com"),
Timeout = TimeSpan.FromSeconds(10)
};
If you're using the object initializer syntax without providing any constructor arguments, you don't even have to type the pair of parentheses following the class name. All of this is syntactic sugar which helps improve the readability of your code.
#Collection Initializers
When you create a collection, you often want to seed it with some initial data, just like setting property values on a freshly created object:
var fibonacciNumbers = new List<long>();
fibonacciNumbers.Add(0);
fibonacciNumbers.Add(1);
These statements resemble property assignments on objects, for which there exist object initializers. Correspondingly, C# offers collection initializers:
var fibonacciNumbers = new List<long> { 0, 1 };
This list initialization looks a lot cleaner than its much more verbose counterpart. Other collection types can benefit from the collection initializer syntax as well. Consider the following code which creates a string lookup using a Dictionary<TKey, TValue>
:
var languageParadigms = new Dictionary<string, string>();
languageParadigms.Add("C#", "object-oriented");
languageParadigms.Add("F#", "functional");
By utilizing collection initializers, the snippet can be rewritten as follows:
var languageParadigms = new Dictionary<string, string>
{
{ "C#", "object-oriented" },
{ "F#", "functional" }
};
Hey, that already looks a lot nicer. Now let's see why and how this code compiles.
#Compiling Collection Initializers
When the C# compiler encounters a collection initializer, it'll replace the shorthand initializer syntax by appropriate method calls to the collection object. For that to succeed, the collection class needs to implement IEnumerable<T>
and provide an accessible method named Add
. This is a convention built into the compiler:
There are a few places in the C# language where we do this sort of "pattern matching"; we don’t care what the exact type is, just so long as the methods we need are available. Eric Lippert, Following the pattern
The Add
method also needs to have the correct number of parameters. In the first example, we initialize our list with numbers of type long
, which is a valid operation because List<long>
defines an Add(long item)
method. The second example used a Dictionary<string, string>
and provided a list of initializers with two values (e.g. "F#" and "functional"). Those two values map to the two parameters of the Add(string key, string value)
method.
#Custom Collection Initializers
The compiler has no special knowledge about the initialization of certain collection types. That is, there are no checks hardcoded for types like List<T>
or Dictionary<TKey, TValue>
. It instead relies on the convention of implementing IEnumerable<T>
and providing an Add
method with a type-compatible signature in the collection class. This is all about duck typing, if you will.
Now, consider this simple struct which represents a point in three-dimensional space:
public struct Point3D
{
public readonly double X;
public readonly double Y;
public readonly double Z;
public Point3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
}
Here's an implementation of a collection of points, which respects the compiler convention for collection initializers and therefore both implements IEnumerable<T>
and provides a public Add
method:
public class Points : IEnumerable<Point3D>
{
private readonly List<Point3D> _points;
public Points()
{
_points = new List<Point3D>();
}
public void Add(double x, double y, double z)
{
_points.Add(new Point3D(x, y, z));
}
public IEnumerator<Point3D> GetEnumerator()
{
return _points.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
We can now instantiate the Points
class and fill it with values like this:
var cube = new Points
{
{ -1, -1, -1 },
{ -1, -1, 1 },
{ -1, 1, -1 },
{ -1, 1, 1 },
{ 1, -1, -1 },
{ 1, -1, 1 },
{ 1, 1, -1 },
{ 1, 1, 1 }
};
The three elements of each item will be mapped to the x
, y
, and z
parameters, respectively, in order of their declaration. Note that the types have to be compatible in order for the code to compile.
Pretty cool, don't you think?