为啥 C# 3.0 对象初始值设定项构造函数括号是可选的?
Posted
技术标签:
【中文标题】为啥 C# 3.0 对象初始值设定项构造函数括号是可选的?【英文标题】:Why are C# 3.0 object initializer constructor parentheses optional?为什么 C# 3.0 对象初始值设定项构造函数括号是可选的? 【发布时间】:2011-04-09 07:53:55 【问题描述】:似乎 C# 3.0 对象初始值设定项语法允许在存在无参数构造函数时排除构造函数中的开/关括号对。示例:
var x = new XTypeName PropA = value, PropB = value ;
相对于:
var x = new XTypeName() PropA = value, PropB = value ;
我很好奇为什么 XTypeName
之后的构造函数开/关括号对在这里是可选的?
【问题讨论】:
顺便说一句,我们在上周的代码审查中发现了这一点: var list = new ListThis question was the subject of my blog on September 20th 2010。 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
第 1 行创建一个新的 C,调用默认构造函数,然后在新对象上调用实例方法 M。第 2 行创建 B.M 的新实例并调用其默认构造函数。 如果第 1 行的括号是可选的,那么第 2 行就会有歧义。 然后我们必须想出一个规则来解决歧义;我们无法将其设为错误,因为这将是一个破坏性更改,将现有的合法 C# 程序更改为损坏的程序。
因此,规则必须非常复杂:基本上,括号仅在不引入歧义的情况下是可选的。我们必须分析所有可能引入歧义的情况,然后在编译器中编写代码来检测它们。
鉴于此,回头看看我提到的所有费用。现在有多少变大了?复杂的规则具有巨大的设计、规范、开发、测试和文档成本。复杂的规则更有可能在未来与功能进行意外交互时出现问题。
为了什么?一个微小的客户利益,它没有为语言增加新的表现力,但确实增加了疯狂的角落案例,只是等待对遇到它的某个可怜的毫无戒心的灵魂大喊“抓住了”。像这样的功能会立即被删掉,并放在“从不这样做”的名单上。
您是如何确定这种特定的歧义的?
这一点立刻就清楚了;我非常熟悉 C# 中用于确定何时应使用带点名称的规则。
在考虑一项新功能时,您如何确定它是否会引起歧义?手工、形式证明、机器分析,什么?
这三个。大多数情况下,我们只看上面的规格和面条,就像我在上面所做的那样。例如,假设我们想在 C# 中添加一个名为“frob”的新前缀运算符:
x = frob 123 + 456;
(UPDATE:frob
当然是await
;这里的分析本质上是设计团队在添加await
时经过的分析。)
“frob”在这里就像“new”或“++”——它出现在某种表达式之前。我们会计算出所需的优先级和关联性等等,然后开始问诸如“如果程序已经有一个类型、字段、属性、事件、方法、常量或本地称为 frob 怎么办?”这将立即导致以下情况:
frob x = 10;
这是否意味着“对 x = 10 的结果进行 frob 运算,或者创建一个名为 x 的 frob 类型的变量并将 10 分配给它?” (或者,如果 frobbing 产生一个变量,它可能是 10 到 frob x
的赋值。毕竟,如果 x
是 int*
,*x = 10;
解析并且是合法的。)
G(frob + x)
这意味着“frob x 上一元加号运算符的结果”还是“将表达式 frob 添加到 x”?
等等。为了解决这些歧义,我们可能会引入启发式算法。当你说“var x = 10;”时这是模棱两可的;它可能意味着“推断 x 的类型”,也可能意味着“x 是 var 类型”。所以我们有一个启发式方法:我们首先尝试查找一个名为 var 的类型,只有当一个类型不存在时才推断 x 的类型。
或者,我们可能会更改语法以使其不模棱两可。他们在设计 C# 2.0 时遇到了这个问题:
yield(x);
这是否意味着“在迭代器中生成 x”或“使用参数 x 调用 yield 方法?”通过将其更改为
yield return(x);
现在是明确的。
在对象初始化器中的可选括号的情况下,很容易推断是否引入了歧义,因为允许引入以 开头的东西的情况非常少。基本上只是各种语句上下文、语句 lambdas、数组初始值设定项,仅此而已。很容易推理所有案例并表明没有歧义。确保 IDE 保持高效有些困难,但可以轻松完成。
这种对规范的摆弄通常就足够了。如果这是一个特别棘手的功能,那么我们会使用更重的工具。例如,在设计 LINQ 时,其中一位编译器人员和一位 IDE 人员都具有解析器理论背景,他们自己构建了一个解析器生成器,可以分析语法以查找歧义,然后将用于查询理解的建议 C# 语法输入其中;这样做会发现许多查询不明确的情况。
或者,当我们在 C# 3.0 中对 lambdas 进行高级类型推断时,我们编写了我们的建议,然后将它们发送到剑桥的 Microsoft 研究中心,那里的语言团队已经足够好,可以对类型进行正式证明推理提议在理论上是合理的。
今天的 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”?同样,我们有一个很好的猜测。
【讨论】:
哦,抱歉,我忘记了……蝙蝠信号方法虽然看起来有效,但比 (IMO) 一种直接接触方式更可取,这样人们就不会获得公众教育所需的公众曝光率以可索引、可搜索且易于参考的 SO 帖子的形式。我们是否应该直接联系以编排上演的 SO 帖子/回答舞蹈? :) 我建议您避免发布帖子。这对于可能对这个问题有更多见解的其他人来说是不公平的。更好的方法是发布问题,然后通过电子邮件发送一个链接请求参与。 @James:我已更新我的答案以解决您的后续问题。 @Eric,您可以在博客上写下这个“从不这样做”的列表吗?我很想看到其他永远不会成为 C# 语言一部分的示例 :) @Eric:我真的真的真的感谢您对我的耐心:) 谢谢!信息量很大。【参考方案2】:因为这是指定语言的方式。它们没有任何价值,为什么要包括它们?
它也非常类似于隐式类型数组
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
【讨论】:
它们没有增加任何价值,因为它的意图应该很明显,但它打破了一致性,因为现在我们有两种不同的对象构造语法,一种需要括号(和逗号-其中分隔的参数表达式)和一个没有。 @James Dunne,它实际上与隐式类型数组语法非常相似,请参阅我的编辑。没有类型,没有构造函数,意图很明显,所以不需要声明【参考方案3】:这样做是为了简化对象的构造。语言设计者没有(据我所知)具体说明为什么他们认为这很有用,尽管在 C# Version 3.0 Specification page 中明确提到了这一点:
对象创建表达式可以省略构造函数参数列表和括号,前提是它包含对象或集合初始值设定项。省略构造函数参数列表并用括号括起来等效于指定一个空参数列表。
我想他们认为,在这种情况下,括号对于显示开发人员意图不是必需的,因为对象初始化程序显示了构造和设置对象属性的意图。
【讨论】:
【参考方案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 = "!";
【讨论】:
对。在您的示例中的第一种和第二种情况下,它们在功能上是相同的,对吗? @James Dunne - 正确。那是语言规范指定的部分。空括号是多余的,但您仍然可以提供它们。【参考方案5】:我不是 Eric Lippert,所以我不能肯定地说,但我认为这是因为编译器不需要空括号来推断初始化构造。因此它成为冗余信息,不需要。
【讨论】:
对,这是多余的,但我只是好奇为什么突然引入它们是可选的?它似乎打破了语言语法的一致性。如果我没有打开大括号来指示初始化程序块,那么这应该是非法语法。有趣的是,你提到了 Lippert 先生,我有点公开地寻找他的答案,这样我自己和其他人就可以从无所事事的好奇心中受益。 :)以上是关于为啥 C# 3.0 对象初始值设定项构造函数括号是可选的?的主要内容,如果未能解决你的问题,请参考以下文章
TypeScript是否支持带有对象初始值设定项的构造函数?