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


114

似乎C#3.0对象初始化程序语法允许在存在无参数构造函数的情况下在构造函数中排除括号的打开/关闭对。例:

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

相对于:

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

我很好奇为什么构造函数打开/关闭括号对在此之后是可选的XTypeName


9
顺便说一句,我们在上周的代码回顾中发现了这一点:var list = new List <Foo> {}; 如果某些东西可以被滥用...
蓝光

@blu这是我想问这个问题的原因之一。我注意到我们的代码不一致。一般而言,不一致会困扰我,所以我想我会看看语法的可选性背后是否有很好的理由。:)
詹姆斯·邓恩

Answers:


143

这个问题是我2010年9月20日发表的博客的主题。乔什和乍得的答案(“它们没有任何价值,为什么需要它们?”和“消除冗余”)基本上是正确的。要充实一点:

作为对象初始化程序的“较大功能”的一部分,允许您省去参数列表的功能符合我们的“必需”功能标准。我们考虑了以下几点:

  • 设计和规格成本低
  • 无论如何,我们将广泛地更改用于处理对象创建的解析器代码;与较大功能的成本相比,使参数列表为可选的额外开发成本并不大
  • 与较大功能的成本相比,测试负担相对较小
  • 与之相比,文档负担相对较小。
  • 预计维护负担很小;自发布以来,我不记得此功能多年来报告的任何错误。
  • 该功能不会对该地区的未来功能造成任何直接明显的风险。(我们要做的最后一件事是现在制作一个便宜,简单的功能,这使得将来实现更引人注目的功能变得更加困难。)
  • 该功能不会对该语言的词法,语法或语义分析增加任何新的歧义。在键入时,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
    }
}

第1行创建一个新的C,调用默认的构造函数,然后在新对象上调用实例方法M。第2行创建BM的新实例并调用其默认构造函数。如果第1行上的括号是可选的,则第2行将是不明确的。 然后,我们将不得不提出解决歧义的规则;我们无法将其设为错误,因为这将是一个重大更改,将现有的合法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。毕竟,*x = 10;如果xis则解析并合法)int*

G(frob + x)

这是否意味着“在x上添加一元加运算符的结果”或“在x上添加表达式frob”?

等等。为了解决这些歧义,我们可能会引入启发式方法。当您说“ 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进行高级类型推断时,我们写了我们的建议,然后将它们发送给了剑桥的Microsoft Research,那里的语言团队有足够的能力来正式证明类型推断建议是理论上合理。

今天的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
哦,对不起,我忘了...蝙蝠信号法似乎可行,但比(IMO)首选直接联系方式,因为这样一来,就不会以SO帖子的形式获得想要进行公共教育的公众曝光度是可索引的,可搜索的,并且易于引用。我们是否应该直接联系以便编排上演的SO发布/回答舞蹈?:)
詹姆斯·邓恩

5
我建议您避免上一篇文章。对于可能对该问题有其他见解的其他人来说,这将是不公平的。更好的方法是发布问题,然后通过电子邮件发送链接以要求参与。
chilltemp

1
@James:我已经更新了答案,以解决您的后续问题。
埃里克·利珀特

8
@Eric,您是否可以在博客中列出此“从不这样做”列表?我很好奇看到其他示例永远不会成为C#语言的一部分:)
Ilya Ryzhenkov 2010年

2
@Eric:我真的真的真的跟我感谢您的耐心:)谢谢!非常丰富。
詹姆斯·邓恩

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
它们没有任何意义,因为它的意图很明显,但是它一贯地中断了,因为现在我们有了两种不同的对象构造语法,一种带有括号(并带有逗号分隔的参数表达式),而另一种则没有括号。 。
詹姆斯·邓恩

1
@James Dunne,它实际上与隐式数组语法非常相似,请参见我的编辑。没有类型,没有构造函数,并且意图很明显,因此不需要声明它
CaffGeek

7

这样做是为了简化对象的构造。语言设计师没有(据我所知)没有特别说明为什么他们觉得这很有用,尽管在C#3.0版规范页面中明确提到了这一点:

如果对象创建表达式包含对象或集合初始化程序,则它可以省略构造函数参数列表和括号。省略构造函数参数列表并包含括号等效于指定一个空的参数列表。

我想在这种情况下,他们认为不需要括号来显示开发人员的意图,因为对象初始化程序显示了构造和设置对象属性的意图。


4

在第一个示例中,编译器推断您正在调用默认构造函数(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 = "!"};

对。在您的示例的第一种和第二种情况下,它们在功能上是相同的,对吗?
詹姆斯·邓恩

1
@詹姆斯·邓恩-正确。这是语言规范指定的部分。空括号是多余的,但是您仍然可以提供它们。
Justin Niessner'9

1

我不是Eric Lippert,所以不能肯定地说,但是我认为这是因为编译器不需要空括号即可推断出初始化构造。因此,它成为多余的信息,不需要。


是的,这是多余的,但是我很好奇为什么突然引入它们是可选的?它似乎破坏了语言语法的一致性。如果我没有大括号来表示初始化程序块,那么这应该是非法的语法。有趣的是您提到利珀特先生,我在公开场合钓鱼寻求他的回答,这样我自己和其他人就会从闲着的好奇心中受益。:)
詹姆斯·邓恩
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.