This project is read-only.

Collection initializers on non-IEnumerable types

Topics: C# Language Design
May 12, 2014 at 12:45 PM
C# specification, §7.6.10.3 Collection initializers:
The collection object to which a collection initializer is applied must be of a type that implements System.Collections.IEnumerable or a compile-time error occurs.
 

I suggest removing this restriction. There are quite a few cases where using the collection initializer syntax to initialize objects that aren't IEnumerables is useful.

An old blog post by Mads Torgersen explains why this restriction was added: to prevent the usage of collection initializers on objects whose Add method was not designed for this, e.g. immutable types.
However, (I think) it's extremely unlikely that a programmer would write a collection initializer for something that isn't thought of as a collection, and if they did, the bug would be obvious.



Here's an example of a type which can't implement IEnumerable in a sensible way, but has a collection initializer syntax that makes sense. It's a class that allows settings classes to declare their default values in a clean way (the actual code has lazy initialization and other stuff, but that's not important here) :
public class SettingsDefaultValues<TSettings> : IEnumerable
{
    public Dictionary<string, object> AsDictionary { get; private set; }
    
    public SettingsDefaultValues()
    {
        AsDictionary = new Dictionary<string, object>();
    }

    public void Add<TProp>( Expression<Func<TSettings, TProp>> expr, TProp value )
    {
        // GetPropertyName, whose code isn't defined here, does what it says
        AsDictionary.Add( GetPropertyName( expr ), value );
    }

    // Required for collection initializers
    public IEnumerator GetEnumerator()
    {
        throw new NotSupportedException();
    }
}
    
// Usage, in a "MySettings" class:
protected override SettingsDefaultValues<MySettings> GetDefaultValues()
{
    return new SettingsDefaultValues<MySettings>
    {
        { x => x.Name, "John Q. Public" },
        { x => x.Age, 20 }
    };
}
In this example, SettingsDefaultValues can't inherit from Dictionary<string, object> or from another collection without having some additional Add methods that do not make sense in this context. However, it is clearly a collection and it benefits from the collection initializer syntax.
May 12, 2014 at 2:45 PM
How about simply specifying an attribute which would indicate whether or not a collection should be constructable in that way? In the absence of an attribute, use the existing rules. Attributes strike me as an under-utilized feature, given that there are many places where no single rule can correctly identify all types where a certain rephrasing of code by the compiler would be legitimate.
May 12, 2014 at 2:55 PM
Edited May 12, 2014 at 2:55 PM
Attributes could be used in two different ways:
  • One per type, to let the compiler know that collection initializers should be allowed
  • One per add method, to tell the compiler which methods to consider. (Then they wouldn't even need to be called Add!)
If we're revisiting collection (and object?) initializers anyway, there are a couple of other options to consider:
  • Supporting immutable Add methods by using a return value, so that var x = new Foo { 1, 2 }; could be converted into var x = new Foo().Add(1).Add(2);
  • Supporting builder types (possibly configured via attributes) so that var x = new Foo { 1, 2 }; could be converted into var x = new Foo.Builder().Add(1).Add(2).Build();
But yes, finding some way of removing the IEnumerable restriction would be very welcome indeed.
May 12, 2014 at 5:30 PM
jonskeet wrote:
Attributes could be used in two different ways:
How about this approach: allow a class Foo to specify a construction approach. Possible approaches would include:
  1. Construct an array and pass it to a constructor or a designated static factory method; make a new array for each object, even if all of the items are constants.
  2. As with #1, but make the array static if all its contents are constants.
  3. Call a parameterless constructor or factory method, and then call some specified method upon the resulting object once per item.
  4. Call a constructor or factory method that takes an int parameter with the required size, and then calls an indexed property setter once per item.
It's not hard to imagine types where each of the above might be the "right" approach. Rather than having the compiler try to "guess" that it should call a parameterless cosntructor and then Add, it would be better to simply have a means of telling the compiler what it should do.