为什么C#集合初始化器是这样工作的?

58
我正在研究C#集合初始化器,发现其实现非常实用但也与C#中其他内容极为不同。
我可以创建如下代码:
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) { }
}

因为我已经满足了编译器的最低要求(实现了 IEnumerablepublic void Add),所以这个可以工作,但是显然没有价值。

我想知道C#团队为什么不创建一个更严格的要求集?换句话说,为什么为了使这个语法编译,编译器不要求类型实现ICollection?那似乎更符合其他C#功能的精神。


3
给网页开发人员的提示:如果你尝试将该类的实例序列化为 JSON(实现 IEnumerable 会导致序列化器迭代对象),将会抛出 NotImplementedException。请注意避免此情况。 - diachedelic
3个回答

91

你的观察很准确 - 实际上,这与微软C#语言PM Mads Torgersen的观察一致。

Mads在2006年10月发表了一篇关于此主题的文章,标题为《What Is a Collection?》,其中写道:

诚然,在框架的第一个版本中,我们在System.Collections.ICollection上犯了错误,它几乎没有用处。但是在.NET Framework 2.0中引入泛型后,我们对其进行了很好的修复:System.Collections.Generic.ICollection<T>允许您添加和删除元素,枚举它们,统计元素数量并检查成员身份。

显然,从那时起,每次创建集合时,每个人都会实现ICollection<T>,对吗?事实并非如此。以下是我们使用LINQ学习集合的真正含义以及如何在C# 3.0中改变我们的语言设计的方法。

事实证明,在框架中只有14种实现了ICollection<T>接口的类,但有189个实现了IEnumerable接口并具有公共的Add()方法。

这种方法有一个隐藏的好处 - 如果他们以ICollection<T>接口为基础,那么就只会支持一个Add()方法。

相比之下,采用现在的方法意味着集合的初始化器只是Add()方法的参数集合。

为了说明问题,让我们稍微扩展一下你的代码:

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

    public void Add(int i) { }

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

你现在可以写成这样:

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

在引用中,第二段第一行应该是“ICollection<T>”,而不是“ICollection”。 - Justin R.
你的回答展示了这种方法提供的好处和灵活性,但我不禁想知道是否在类似情况下提供这种灵活性没有有效的方法。他们有效地写入规范中:“如果您恰好在实现IEnumerable的层次结构中的某个位置有一个名为Add的方法,那么我们将在编译器中进行一些魔法操作,并使初始化程序调用它,即使该方法不是重载、接口成员或通过语法、属性或其他方式指定为此目的。” - AaronLS
我认为OP的问题“C#团队为什么没有创建一个更严格的要求集?”留下了一些疑问。你提出了一个很好的理由,说明为什么现在这样提供了很多灵活性。回答这个问题基本上涉及到:1)可能替代方案的详尽列表以及它们为什么不太可行,你已经涉及到了一些,或者2)一个例子,可以实现相同的好处而不需要编译器的技巧。从API /插件设计的角度来看,我很好奇是否存在第二种情况。也就是说,你实现了几个参数的委托,我的框架将选择最佳的。 - AaronLS
实际上,这并不像你想象的那样违背常规——例如,回溯到原始的 C# 实现,foreach 关键字并不需要 IEnumerable 实现,GetEnumerator() 的实现就足够了。他们在其他几个领域也保持了这种灵活性,包括在 C# 5.0 中实现 async 和 await。 - Bevan
我认为对于 OP 的问题“C# 团队为什么没有创建更严格的要求?”的答案在 Mads 的帖子中可以看出 - 他们想要创建一种真正有用的功能,而不只是一个语法上的好奇心,大多数人都觉得毫无价值。 - Bevan
显示剩余2条评论

9
我也考虑过这个问题,最让我满意的答案是ICollection有许多方法,而不仅仅是Add,例如:Clear,Contains,CopyTo和Remove。删除元素或清除与能否支持对象初始化语法无关,只需要一个Add()。
如果框架足够细致,并且有一个ICollectionAdd接口,那么它将具有“完美”的设计。但是,我真的认为这不会增加太多价值,每个接口一个方法。IEnumerable + Add似乎是一种hackish方法,但是当您考虑它时,它是更好的选择。
编辑:这不是C#第一次使用这种类型的解决方案。自.NET 1.1以来,foreach使用鸭子类型枚举集合,您的类只需要实现GetEnumerator,MoveNext和Current。Kirill Osenkov也发布了一个post,其中提出了您的问题。

这是“接口隔离原则”。 - v.oddou

3

我知道我晚了3年,但我对现有答案不满意。

为什么编译器不要求该类型实现ICollection才能编译此语法呢?

我来反问你:如果编译器需要不必要的要求,那有何用处呢?

ICollection类也可以从集合初始化语法中受益。考虑那些允许添加数据,但不允许访问先前添加的数据的类。

个人而言,我喜欢在我的单元测试代码中使用new Data { { ..., ... }, ... }语法,使其看起来更像DSL。

实际上,我更希望减弱这个要求,这样我就可以使用这种漂亮的语法,而无需实现IEnumerable。集合初始化器只是Add()方法的纯语法糖,不应该需要任何其他东西。


4
我明白你的观点,但如果语法糖需要一个方法,我宁愿通过一个接口来学习。 - nik.shornikov
@nik.shornikov 有点像ICollectionInitializerSyntax<T>(以及所有的<T1, T2, ...>变体)吗?我同意,这将使代码的意图更加清晰。而且它会很好地配合我的希望,即减弱当前IEnumerable的要求。 - Eldritch Conundrum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接