Gosh it feels like a long time since I’ve blogged – particularly since I’ve blogged anything really C#-language-related.
At some point I want to blog about my two CodeMash 2013 sessions (making the C# compiler/team cry, and learning lessons about API design from the Spice Girls) but those will take significant time – so here’s a quick post about object and collection initializers instead. Two interesting little oddities…
Is it an object initializer? Is it a collection initializer? No, it’s a syntax error!
The first part came out of a real life situation – FakeDateTimeZoneSource, if you want to look at the complete context.
Basically, I have a class designed to help test time zone-sensitive code. As ever, I like to create immutable objects, so I have a builder class. That builder class has various properties which we’d like to be able to set, and we’d also like to be able to provide it with the time zones it supports, as simply as possible. For the zones-only use case (where the other properties can just be defaulted) I want to support code like this:
var source = new FakeDateTimeZoneSource.Builder
{
CreateZone("x"),
CreateZone("y"),
CreateZone("a"),
CreateZone("b")
}.Build();
(CreateZone
is just a method to create an arbitrary time zone with the given name.)
To achieve this, I made the Builder
implement IEnumerable<DateTimeZone>
, and created an Add
method. (In this case the IEnumerable<>
implementation actually works; in another case I’ve used explicit interface implementation and made the GetEnumerator()
method throw NotSupportedException
, as it’s really not meant to be called in either case.)
So far, so good. The collection initializer worked perfectly as normal. But what about when we want to set some other properties? Without any time zones, that’s fine:
var source = new FakeDateTimeZoneSource.Builder
{
VersionId = "foo"
}.Build();
But how could we set VersionId
and add some zones? This doesn’t work:
var invalid = new FakeDateTimeZoneSource.Builder
{
VersionId = "foo",
CreateZone("x"),
CreateZone("y")
}.Build();
That’s neither a valid object initializer (the second part doesn’t specify a field or property) nor a valid collection initializer (the first part does set a property).
In the end, I had to expose an IList<DateTimeZone>
property:
var valid = new FakeDateTimeZoneSource.Builder
{
VersionId = "foo",
Zones = { CreateZone("x"), CreateZone("y") }
}.Build();
An alternative would have been to expose a propert of type Builder
which just returned itself – the same code would have been valid, but it would have been distinctly odd, and allowed some really spurious code.
I’m happy with the result in terms of the flexibility for clients – but the class design feels a bit messy, and I wouldn’t have wanted to expose this for the "production" assembly of Noda Time.
Describing all of this to a colleague gave rise to the following rather sillier observation…
Is it an object initializer? Is it a collection initializer? (Parenthetically speaking…)
In a lot of C# code, an assignment expression is just a normal expression. That means there’s potentially room for ambiguity, in exactly the same kind of situation as above – when sometimes we want a collection initializer, and sometimes we want an object initializer. Consider this sample class:
using System;
using System.Collections;
class Weird : IEnumerable
{
public string Foo { get; set; }
private int count;
public int Count { get { return count; } }
public void Add(string x)
{
count++;
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotSupportedException();
}
}
As you can see, it doesn’t actually remember anything passed to the Add
method, but it does remember how many times we’ve called it.
Now let’s try using Weird
in two ways which only differ in terms of parentheses. First up, no parentheses:
string Foo = "x";
Weird weird = new Weird { Foo = "y" };
Console.WriteLine(Foo);
Console.WriteLine(weird.Foo);
Console.WriteLine(weird.Count);
Okay, so it’s odd having a local variable called Foo
, but we’re basically fine. This is an object initializer, and it’s setting the Foo
property within the new Weird
instance. Now let’s add a pair of parentheses:
string Foo = "x";
Weird weird = new Weird { (Foo = "y") };
Console.WriteLine(Foo);
Console.WriteLine(weird.Foo);
Console.WriteLine(weird.Count);
Just adding those parenthese turn the object initializer into a collection initializer, whose sole item is the result of the assignment operator – which is the value which has now been assigned to Foo
.
Needless to say, I don’t recommend using this approach in real code…