为什么C# 3.0的对象初始化器构造函数括号是可选的?

129

看起来C# 3.0的对象初始化语法允许当存在一个无参构造函数时,在构造函数中排除开放/关闭括号。例如:

var x = new XTypeName { PropA = value, PropB = value };

相对于:

var x = new XTypeName() { PropA = value, PropB = value };

我很好奇为什么在 XTypeName 后面构造函数的开闭圆括号是可选的?


12
作为一个旁白,上周我们在代码审查中发现了这个:var list = new List<Foo> { }; 如果可以被滥用... - blu
@blu 这就是我想问这个问题的原因之一。我注意到我们代码中的不一致性。总的来说,不一致性让我感到困扰,所以我想看看在语法的可选性背后是否有一个好的理由。 :) - James Dunne
可能是对象初始化器和构造函数之间的区别是什么?的重复问题。 - Owen Pauling
5个回答

155
这个问题是我在2010年9月20日博客的主题。Josh和Chad的答案("它们没有增加价值,为什么要求它们?"和"消除冗余")基本上是正确的。更具体地说:
允许您省略参数列表作为对象初始化器的一部分的功能符合我们对“糖果”功能的标准。我们考虑了以下几点:
  • 设计和规范成本较低
  • 我们将广泛更改处理对象创建的解析器代码;相对于更大的特性的成本,使参数列表变得可选的额外开发成本不大
  • 测试负担相对较小,与更大的特性的成本相比
  • 文档负担相对较小,与...
  • 维护负担预计很小;自发布以来,我不记得有任何关于此功能的错误报告。
  • 该功能对于未来此领域的其他功能没有任何明显的风险。(我们最不想做的就是现在推出一个便宜、易用的功能,使将来实现更有吸引力的功能变得更加困难。)
  • 该功能不会对语言的词法、语法或语义分析引入任何新的歧义。它对于IDE的“IntelliSense”引擎在您输入时执行的“部分程序”分析没有问题。等等。
  • 该功能命中了更大的对象初始化特性的常见“甜点”;通常,如果您正在使用对象初始化器,则恰好是因为对象的构造函数不允许您设置所需的属性。这样的对象通常只是"属性包",首先在ctor中没有参数。

那么,为什么您没有在没有对象初始化器的对象创建表达式的默认构造函数调用中也使空括号可选呢?

再看一下上面的标准列表。其中一个是更改不会在程序的词法、语法或语义分析中引入任何新的歧义。您提出的更改确实引入了语义分析歧义:

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

第一行创建一个新的C,调用默认构造函数,然后在新对象上调用实例方法M。第二行创建B.M的新实例并调用其默认构造函数。如果第一行的括号是可选的,那么第二行将会有歧义。我们就必须想出一个解决歧义的规则;我们不能把它变成一个错误,因为那样会把一个现有的合法C#程序变成一个错误的程序。
因此,规则必须非常复杂:基本上,只有在不引入歧义的情况下,才可以省略括号。我们必须分析所有可能导致歧义的情况,然后在编译器中编写代码来检测它们。
在这种情况下,回过头来看我提到的所有成本。其中有多少现在变得很大?复杂的规则具有设计、规范、开发、测试和文档成本。复杂的规则更容易导致未来功能的意外交互问题。
为了什么?一个微小的客户利益,没有为语言增加新的表现力,但确实添加了疯狂的角落案例,等待着向某个不知情的人喊出“抓住”!像这样的特性会被立即删除,并被列入“永远不要这样做”的列表。
“你是如何确定特定的歧义的?”
那个很明显,我对于在C#中确定何时期望一个点分名称的规则非常熟悉。
“在考虑新功能时,您如何确定它是否会导致任何歧义?手动,正式证明,机器分析,还是什么?”
三者都有。大多数情况下,我们只看规范并思考它,就像我上面所做的那样。例如,假设我们想向C#添加一个名为“frob”的新前缀运算符:
x = frob 123 + 456;

(更新:当然,“frob”是“await”的意思;这里的分析基本上是设计团队在添加“await”时进行的分析。)
这里的“frob”类似于“new”或“++”,它出现在某种表达式之前。我们会确定所需的优先级和结合性等,然后开始问诸如“如果程序已经有一个名为frob的类型、字段、属性、事件、方法、常量或局部变量会怎样?”这将立即导致以下情况:
frob x = 10;

这是否意味着“在 x = 10 的结果上执行 frob 操作,或者创建一个名为 x 的 frob 类型变量并将其赋值为 10?”(或者,如果 frobbing 生成一个变量,它可以是将 10 赋值给 frob x。毕竟,如果 xint**x = 10; 可以解析并且是合法的。)
G(frob + x)

这是指“对x的一元加操作符的结果进行frob处理”还是“将表达式frob添加到x”?

为了解决这些歧义,我们可以引入启发式方法。当您说“var x = 10;”时,这是有歧义的;它可能意味着“推断x的类型”,也可能意味着“x的类型是var”。因此,我们有一个启发式方法:我们首先尝试查找名为var的类型,只有在不存在该类型时才推断x的类型。

或者,我们可以更改语法以使其不含歧义。当设计C# 2.0时,他们就遇到了这个问题:

yield(x);

这是指“在迭代器中yield x”还是“调用带有参数x的yield方法”?通过修改它为

yield return(x);

现在已经没有歧义了。

对于对象初始化程序中可选括号的情况,很容易判断是否引入了歧义,因为可以引入以 { 开头的内容的情况非常少。基本上只有各种语句上下文、语句 lambda、数组初始化程序等。很容易推理出所有情况并证明没有歧义。确保 IDE 保持高效有点困难,但也不会太麻烦。

这种规范的调整通常就足够了。如果是特别棘手的功能,那么我们会使用更重的工具。例如,在设计 LINQ 时,编译器团队和 IDE 团队中都有背景知识的解析器理论人员自行构建了一个解析器生成器,可以分析语法中的歧义,并将查询理解的 C# 语法输入其中;通过这样做,发现了许多查询存在歧义的情况。

或者,在 C# 3.0 中对 lambda 进行高级类型推断时,我们撰写了提案,然后将其发送到剑桥的微软研究院,那里的语言团队足够优秀,能够制定出一份形式化的证明,证明类型推断方案在理论上是正确的。

C# 中是否存在歧义?

当然有。

G(F<A, B>(0))

在C# 1中,这个意思很清楚。它和以下代码相同:
G( (F<A), (B>0) )

也就是说,它使用两个布尔参数调用了G。在C# 2中,这可能意味着与C# 1中相同的含义,但也可能意味着“将0传递给带有类型参数A和B的通用方法F,然后将F的结果传递给G”。我们向解析器添加了一个复杂的启发式算法,以确定您可能想要的两种情况中的哪一种。

类似地,即使在C# 1.0中,强制转换也是不明确的:

G((T)-x)

这是“将x转换为T”还是“从T中减去x”?我们再次采用一种启发式方法进行猜测。

3
抱歉,我忘了……虽然蝙蝠信号的方法似乎行得通,但我认为直接联系的方式更好。这样一来就可以在SO帖子中得到所需的公众曝光,以达到公共教育的目的。该帖子可被索引、搜索和轻松地查阅。我们要直接联系,以便协调一个精心策划的SO帖子/回答活动吗? :) - James Dunne
5
建议您避免发布分阶段的帖子,这可能对那些可能对问题有额外见解的人不公平。更好的方法是发布问题,然后通过电子邮件发送链接以请求参与。 - chilltemp
1
@James:我已经更新了我的答案以回答你的后续问题。 - Eric Lippert
8
@Eric,你能写一篇关于“永远不要这样做”的列表的博客吗?我很好奇还有哪些例子永远不会成为 C# 语言的一部分 :) - Ilya Ryzhenkov
2
@Eric:非常感谢您对我包容和耐心的付出 :) 谢谢!很有启发。 - James Dunne
显示剩余6条评论

12

那是因为这就是该语言的规定。它们没有任何价值,所以为什么要包含它们呢?

它也非常类似于隐式类型数组。

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

参考资料:http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx


1
它们没有增加任何价值,因为显然应该知道意图是什么,但它会破坏一致性,因为现在我们有两个不同的对象构造语法,一个需要括号(其中包含逗号分隔的参数表达式),另一个则不需要。 - James Dunne
1
@James Dunne,实际上它的语法非常类似于隐式类型数组语法,请参见我的编辑。没有类型、构造函数,意图明显,因此无需声明它。 - CaffGeek

7
这样做是为了简化对象的构造。语言设计者并没有(据我所知)明确说明他们为什么认为这很有用,尽管在C# Version 3.0 Specification page中明确提到:

一个对象创建表达式可以省略构造函数参数列表和括号,只要它包含一个对象或集合初始化器。省略构造函数参数列表和括号相当于指定一个空的参数列表。

我想他们认为,在这种情况下,括号并不是必需的,因为对象初始化器显示了构造和设置对象属性的意图。

5
在第一个示例中,编译器推断您正在调用默认构造函数(C#3.0语言规范指出,如果未提供括号,则调用默认构造函数)。
在第二个示例中,您显式调用默认构造函数。
您还可以使用该语法在明确传递值给构造函数的同时设置属性。 如果您有以下类定义:
public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

以下三个语句都是有效的:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};

对的。在你的例子中,第一种和第二种情况在功能上是相同的,对吗? - James Dunne
1
@James Dunne - 正确。那是语言规范指定的部分。空括号是多余的,但你仍然可以提供它们。 - Justin Niessner

1

我不是Eric Lippert,所以不能确定,但我认为这是因为编译器不需要空括号来推断初始化结构。因此它变成了冗余信息,不再需要。


没错,这是多余的,但我只是好奇为什么它们突然变成可选项了?这似乎违反了语言语法的一致性。如果我没有打开花括号来指示初始化块,那么这应该是非法语法。有趣的是你提到了Lippert先生,我也在公开地寻找他的答案,这样我和其他人就可以从我的好奇心中受益。 :) - James Dunne

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