为什么 C# 3.0 对象初始值设定项构造函数括号是可选的?
似乎 C# 3.0 对象初始值设定项语法允许在存在无参数构造函数时排除构造函数中的开/闭括号对。示例:
var x = new XTypeName { PropA = value, PropB = value };
相反:
var x = new XTypeName() { PropA = value, PropB = value };
我很好奇为什么构造函数左/右括号对在 XTypeName
之后是可选的?
It seems that the C# 3.0 object initializer syntax allows one to exclude the open/close pair of parentheses in the constructor when there is a parameterless constructor existing. Example:
var x = new XTypeName { PropA = value, PropB = value };
As opposed to:
var x = new XTypeName() { PropA = value, PropB = value };
I'm curious why the constructor open/close parentheses pair is optional here after XTypeName
?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
这个问题是我博客的主题2010 年 9 月 20 日。乔什和查德的回答(“它们没有增加任何价值,所以为什么需要它们?”和“消除冗余”)基本上是正确的。更具体地说:
允许您删除参数列表作为对象初始值设定项的“更大功能”的一部分的功能满足了我们对“糖”功能的标准。我们考虑的一些要点:
再看看上面的标准列表。其中之一是,这一变化不会在程序的词汇、语法或语义分析中引入任何新的歧义。您提议的更改确实引入了语义分析歧义:
第 1 行创建一个新的 C,调用默认构造函数,然后在新对象上调用实例方法 M。第 2 行创建 BM 的新实例并调用其默认构造函数。 如果第 1 行的括号是可选的,那么第 2 行就会不明确。然后我们就必须想出一个规则来解决这个不明确的问题;我们不能将其设置为错误,因为这将是一个重大更改,将现有的合法 C# 程序更改为损坏的程序。
因此,规则必须非常复杂:本质上,括号只有在不引起歧义的情况下才是可选的。我们必须分析所有可能引入歧义的情况,然后在编译器中编写代码来检测它们。
鉴于此,请回过头来看看我提到的所有成本。现在有多少已经变大了?复杂的规则会产生大量的设计、规范、开发、测试和文档成本。复杂的规则更有可能导致未来与功能发生意外交互的问题。
一切都是为了什么?一个微小的客户利益,并没有为该语言增加新的代表性能力,但确实增加了疯狂的极端情况,只是等待对一些遇到它的可怜的毫无戒心的灵魂大喊“抓住了”。类似的功能会被立即删除并放入“永远不要这样做”列表中。
这个问题立刻就清楚了;我非常熟悉 C# 中用于确定何时需要点分名称的规则。
全部三个。大多数情况下,我们只是查看规格和内容,就像我上面所做的那样。例如,假设我们想在 C# 中添加一个新的前缀运算符,名为“frob”:(
更新:
frob
当然是await
;这里的分析本质上是分析设计团队在添加await
时经历了这一过程。)这里的“frob”就像“new”或“++”——它出现在某种表达式之前。我们会计算出所需的优先级和关联性等,然后开始提出诸如“如果程序已经有类型、字段、属性、事件、方法、常量或称为 frob 的本地变量怎么办?”之类的问题。这会立即导致这样的情况:
这是否意味着“对 x = 10 的结果进行 frob 操作,或者创建一个名为 x 的 frob 类型变量并将 10 赋给它?” (或者,如果 frobbing 生成一个变量,它可能是将 10 赋值给 frob x。毕竟,如果
,
是*x = 10;
解析并且是合法的xint*
。)这是否意味着“frob x 上一元加运算符的结果”或“将表达式 frob 添加到 x”?
等等。为了解决这些歧义,我们可以引入启发式方法。当你说“var x = 10;”时这是模棱两可的;它可能意味着“推断 x 的类型”,也可能意味着“x 的类型为 var”。所以我们有一个启发式:我们首先尝试查找名为 var 的类型,只有当该类型不存在时,我们才推断 x 的类型。
或者,我们可能会更改语法,使其不会产生歧义。当他们设计 C# 2.0 时,他们遇到了这个问题:
这是否意味着“在迭代器中生成 x”或“使用参数 x 调用yield 方法?”通过将其更改为
现在是明确的。
对于对象初始值设定项中的可选括号,可以直接推断是否引入了歧义,因为允许引入以 { 开头的内容的情况数量非常少 。基本上就是各种语句上下文、语句 lambda、数组初始值设定项,仅此而已。通过所有案例很容易推理并表明没有任何歧义。确保 IDE 保持高效有点困难,但可以轻松完成。
这种摆弄规范通常就足够了。如果这是一个特别棘手的功能,那么我们就会拿出更重的工具。例如,在设计 LINQ 时,一位具有解析器理论背景的编译器人员和一位 IDE 人员为自己构建了一个解析器生成器,可以分析语法以查找歧义,然后将建议的用于查询理解的 C# 语法输入其中;这样做发现了许多查询不明确的情况。
或者,当我们在 C# 3.0 中对 lambda 进行高级类型推断时,我们写下了我们的提案,然后将它们发送到剑桥的微软研究院,那里的语言团队足以提供正式的证明,证明类型推断提案是理论上是合理的。
当然。
在 C# 1 中,这意味着什么是很清楚的。它等同于:
也就是说,它用两个布尔值参数调用 G。在 C# 2 中,这可能意味着 C# 1 中的含义,但也可能意味着“将 0 传递给采用类型参数 A 和 B 的泛型方法 F,然后将 F 的结果传递给 G”。我们向解析器添加了一个复杂的启发式方法,它确定您可能指的是两种情况中的哪一种。
同样,即使在 C# 1.0 中,强制转换也是不明确的:
是“将 -x 强制转换为 T”还是“从 T 中减去 x”?同样,我们有一个启发式的方法可以做出很好的猜测。
This question was the subject of my blog on September 20th 2010. Josh and Chad's answers ("they add no value so why require them?" and "to eliminate redundancy") are basically correct. To flesh that out a bit more:
The feature of allowing you to elide the argument list as part of the "larger feature" of object initializers met our bar for "sugary" features. Some points we considered:
Take another look at that list of criteria above. One of them is that the change does not introduce any new ambiguity in the lexical, grammatical or semantic analysis of a program. Your proposed change does introduce a semantic analysis ambiguity:
Line 1 creates a new C, calls the default constructor, and then calls the instance method M on the new object. Line 2 creates a new instance of B.M and calls its default constructor. If the parentheses on line 1 were optional then line 2 would be ambiguous. We would then have to come up with a rule resolving the ambiguity; we could not make it an error because that would then be a breaking change that changes an existing legal C# program into a broken program.
Therefore the rule would have to be very complicated: essentially that the parentheses are only optional in cases where they don't introduce ambiguities. We'd have to analyze all the possible cases that introduce ambiguities and then write code in the compiler to detect them.
In that light, go back and look at all the costs I mention. How many of them now become large? Complicated rules have large design, spec, development, testing and documentation costs. Complicated rules are much more likely to cause problems with unexpected interactions with features in the future.
All for what? A tiny customer benefit that adds no new representational power to the language, but does add crazy corner cases just waiting to yell "gotcha" at some poor unsuspecting soul who runs into it. Features like that get cut immediately and put on the "never do this" list.
That one was immediately clear; I am pretty familiar with the rules in C# for determining when a dotted name is expected.
All three. Mostly we just look at the spec and noodle on it, as I did above. For example, suppose we wanted to add a new prefix operator to C# called "frob":
(UPDATE:
frob
is of courseawait
; the analysis here is essentially the analysis that the design team went through when addingawait
.)"frob" here is like "new" or "++" - it comes before an expression of some sort. We'd work out the desired precedence and associativity and so on, and then start asking questions like "what if the program already has a type, field, property, event, method, constant, or local called frob?" That would immediately lead to cases like:
does that mean "do the frob operation on the result of x = 10, or create a variable of type frob called x and assign 10 to it?" (Or, if frobbing produces a variable, it could be an assignment of 10 to
frob x
. After all,*x = 10;
parses and is legal ifx
isint*
.)Does that mean "frob the result of the unary plus operator on x" or "add expression frob to x"?
And so on. To resolve these ambiguities we might introduce heuristics. When you say "var x = 10;" that's ambiguous; it could mean "infer the type of x" or it could mean "x is of type var". So we have a heuristic: we first attempt to look up a type named var, and only if one does not exist do we infer the type of x.
Or, we might change the syntax so that it is not ambiguous. When they designed C# 2.0 they had this problem:
Does that mean "yield x in an iterator" or "call the yield method with argument x?" By changing it to
it is now unambiguous.
In the case of optional parens in an object initializer it is straightforward to reason about whether there are ambiguities introduced or not because the number of situations in which it is permissible to introduce something that starts with { is very small. Basically just various statement contexts, statement lambdas, array initializers and that's about it. It's easy to reason through all the cases and show that there's no ambiguity. Making sure the IDE stays efficient is somewhat harder but can be done without too much trouble.
This sort of fiddling around with the spec usually is sufficient. If it is a particularly tricky feature then we pull out heavier tools. For example, when designing LINQ, one of the compiler guys and one of the IDE guys who both have a background in parser theory built themselves a parser generator that could analyze grammars looking for ambiguities, and then fed proposed C# grammars for query comprehensions into it; doing so found many cases where queries were ambiguous.
Or, when we did advanced type inference on lambdas in C# 3.0 we wrote up our proposals and then sent them over the pond to Microsoft Research in Cambridge where the languages team there was good enough to work up a formal proof that the type inference proposal was theoretically sound.
Sure.
In C# 1 it is clear what that means. It's the same as:
That is, it calls G with two arguments that are bools. In C# 2, that could mean what it meant in C# 1, but it could also mean "pass 0 to the generic method F that takes type parameters A and B, and then pass the result of F to G". We added a complicated heuristic to the parser which determines which of the two cases you probably meant.
Similarly, casts are ambiguous even in C# 1.0:
Is that "cast -x to T" or "subtract x from T"? Again, we have a heuristic that makes a good guess.
因为这就是语言的指定方式。它们没有增加任何价值,那么为什么要包含它们呢?
它也与隐式类型数组非常相似
参考:http:// /msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx
Because that's how the language was specified. They add no value, so why include them?
It's also very similar to implicity typed arrays
Reference: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx
这样做是为了简化对象的构造。语言设计者(据我所知)没有具体说明为什么他们认为这很有用,尽管在 C# 版本 3.0 规范页面:
我想他们认为在这种情况下,括号对于显示开发人员的意图来说是不必要的,因为对象初始值设定项显示了构造和设置对象属性的意图。
This was done to simplify the construction of objects. The language designers have not (to my knowledge) specifically said why they felt that this was useful, though it is explicitly mentioned in the C# Version 3.0 Specification page:
I suppose that they felt the parenthesis, in this instance, were not necessary in order to show developer intent, since the object initializer shows the intent to construct and set the properties of the object instead.
在第一个示例中,编译器推断您正在调用默认构造函数(C# 3.0 语言规范规定,如果未提供括号,则调用默认构造函数)。
在第二个中,您显式调用默认构造函数。
您还可以使用该语法来设置属性,同时将值显式传递给构造函数。如果您有以下类定义:
所有三个语句均有效:
In your first example, the compiler infers that you're calling the default constructor (the C# 3.0 Language Specification states that if no parenthesis are provided, the default constructor is called).
In the second, you explicitly call the default constructor.
You can also use that syntax to set properties while explicitly passing values to the constructor. If you had the following class definition:
All three statements are valid:
我不是 Eric Lippert,所以我不能肯定地说,但我认为这是因为编译器不需要空括号来推断初始化构造。因此,它成为冗余信息,并且不需要。
I am no Eric Lippert, so I can't say for sure, but I would assume it is because the empty parenthesis is not needed by the compiler in order to infer the initialization construct. Therefore it becomes redundant information, and not needed.