Why do C# collection initializers work this way?
Asked Answered
V

3

58

I was looking at C# collection initializers and found the implementation to be very pragmatic but also very unlike anything else in C#

I am able to create code like this:

using System;
using System.Collections;

class Program
{
    static void Main()
    {
        Test test = new Test { 1, 2, 3 };
    }
}

class Test : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }

    public void Add(int i) { }
}

Since I have satisfied the minimum requirements for the compiler (implemented IEnumerable and a public void Add) this works but obviously has no value.

I was wondering what prevented the C# team from creating a more strict set of requirements? In other words why, in order for this syntax to compile, does the compiler not require that the type implement ICollection? That seems more in the spirit of other C# features.

Valle answered 19/1, 2009 at 23:47 Comment(1)
Just a note to web developers: that NotImplementedException will be thrown if you attempted to serialize an instance of that class to JSON (implementing IEnumerable causes the serializer to iterate through the object)Kellsie
F
91

Your observation is spot on - in fact, it mirrors one made by Mads Torgersen, a Microsoft C# Language PM.

Mads made a post in October 2006 on this subject titled What Is a Collection? in which he wrote:

Admitted, we blew it in the first version of the framework with System.Collections.ICollection, which is next to useless. But we fixed it up pretty well when generics came along in .NET framework 2.0: System.Collections.Generic.ICollection<T> lets you Add and Remove elements, enumerate them, Count them and check for membership.

Obviously from then on, everyone would implement ICollection<T> every time they make a collection, right? Not so. Here is how we used LINQ to learn about what collections really are, and how that made us change our language design in C# 3.0.

It turns out that there are only 14 implementations of ICollection<T> in the framework, but 189 classes that implement IEnumerable and have a public Add() method.

There's a hidden benefit to this approach - if they had based it on the ICollection<T> interface, there would have been exactly one supported Add() method.

In contrast, the approach they did take means that the initializers for the collection just form sets of arguments for the Add() methods.

To illustrate, let's extend your code slightly:

class Test : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }

    public void Add(int i) { }

    public void Add(int i, string s) { }
}

You can now write this:

class Program
{
    static void Main()
    {
        Test test 
            = new Test 
            {
                1, 
                { 2, "two" },
                3 
            };
    }
}
Fiorenza answered 20/1, 2009 at 0:11 Comment(7)
In the quote, 2nd paragraph 1st line, it's "ICollection<T>" not "ICollection".Chemosh
You're right - I neglected to encode the less-than and greater-than characters. Fixed.Fiorenza
You're answer exemplifies the benefits/flexibility this provides, but I have to wonder if this means there isn't an effective way to provide such flexibility in similar scenarios. They effectively wrote into the spec "if you happen to have a method named Add somewhere in a hierarchy implementing IEnumerable, then we'll do some hokus-pokus in the compiler and have initializers call it, even though the method is not an overload, interface member, or in any way designated for such a purpose via syntax, attributes, or other means."Brew
I.e. I think the OP's question "what prevented the C# team from creating a more strict set of requirements?" is left a little open. You make a good case for why as-is it provides alot of flexibility. To answer that question would basically involve either 1) an exhaustive list of possible alternatives and why they wouldn't quite work, which you touched on some, or 2) an example that accomplishes the same benefits without compiler hokus-pokus. I'm curious from an API/plugin design standpoint, if #2 exists. I.e. you implement several delegates of parameters and my framework will choose the best.Brew
Not saying I'd expect an answer to actually accomplish #1 or #2, as you'd never know if you had a complete list for #1 or whether a #2 existed. Nor is it a cristicism of your great answer. It just seems surprising that they did it this way. I only just today read the specification on this feature, and it seems really out of character for C#Brew
It's actually not as far out of character as you might think - for example, dating back to the original C# implementation, the foreach keyword doesn't require an IEnumerable implementation, an implementation of GetEnumerator() is sufficient. They've continued this flexibility in a couple of other areas, including the implementation of async and await in C# 5.0.Fiorenza
I think the answer to the OP's question "what prevented the C# team from creating a more strict set of requirements?" is implied by Mads' post - they wanted to create a feature that was genuinely useful, not just a syntax curiosity that most found worthless.Fiorenza
R
9

I thought about this too, and the answer which satisfies me the most is that ICollection has many methods other than Add, such as: Clear, Contains, CopyTo, and Remove. Removing elements or clearing has nothing to do with being able to support the object initializer syntax, all you need is an Add().

If the framework was designed granularly enough, and there was an ICollectionAdd interface, then it would've had a "perfect" design. But I honestly don't think that would have added much value, having one method per interface. IEnumerable + Add seems like a hackish approach, but when you think about it, it's a better alternative.

EDIT: This is not the only time C# has approached a problem with this type of solution. Since .NET 1.1, foreach uses duck typing to enumerate a collection, all your class needs to implement is GetEnumerator, MoveNext and Current. Kirill Osenkov has a post which asks your question as well.

Roundel answered 20/1, 2009 at 0:26 Comment(1)
That's "interface segregation principle".Migratory
D
3

(I know I am 3 years late on this, but I was not satisfied with the existing answers.)

why, in order for this syntax to compile, does the compiler not require that the type implement ICollection?

I'll reverse your question: What use would it be if the compiler had requirements that are not really needed?

Non-ICollection classes too can benefit from the collection initializer syntax. Consider classes that allow adding data into them, without allowing access to previously added data.

Personally, I like to use the new Data { { ..., ... }, ... } syntax to add a light, DSL-like look to the code of my unit tests.

Actually, I'd rather weaken the requirement, so that I can use the nice-looking syntax without even having to bother implementing IEnumerable. Collection initializers are pure syntactic sugar to Add(), they shouldn't require anything else.

Dambrosio answered 28/9, 2012 at 12:2 Comment(2)
I see your point, but if syntactic sugar requires a method, I'd rather learn about that through an interface.Relationship
@Relationship Something like ICollectionInitializerSyntax<T> (and all the <T1, T2, ...> variants)? I agree, this would make the intent of the code more clear. And it would go nicely with my wish of weakening the current IEnumerable requirement.Dambrosio

© 2022 - 2024 — McMap. All rights reserved.