为什么 C# 3.0 对象初始值设定项构造函数括号是可选的?

发布于 2024-09-18 14:28:09 字数 294 浏览 8 评论 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

如歌彻婉言 2024-09-25 14:28:09

这个问题是我博客的主题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; 解析并且是合法的xint*。)

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);

现在是明确的。

对于对象初始值设定项中的可选括号,可以直接推断是否引入了歧义,因为允许引入以 { 开头的内容的情况数量非常少 。基本上就是各种语句上下文、语句 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”?同样,我们有一个启发式的方法可以做出很好的猜测。

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:

  • the design and specification cost was low
  • we were going to be extensively changing the parser code that handles object creation anyway; the additional development cost of making the parameter list optional was not large compared to the cost of the larger feature
  • the testing burden was relatively small compared to the cost of the larger feature
  • the documentation burden was relatively small compared...
  • the maintenance burden was anticipated to be small; I don't recall any bugs reported in this feature in the years since it shipped.
  • the feature does not pose any immediately obvious risks to future features in this area. (The last thing we want to do is make a cheap, easy feature now that makes it much harder to implement a more compelling feature in the future.)
  • the feature adds no new ambiguities to the lexical, grammatical or semantic analysis of the language. It poses no problems for the sort of "partial program" analysis that is performed by the IDE's "IntelliSense" engine while you are typing. And so on.
  • the feature hits a common "sweet spot" for the larger object initialization feature; typically if you are using an object initializer it is precisely because the constructor of the object does not allow you to set the properties you want. It is very common for such objects to simply be "property bags" that have no parameters in the ctor in the first place.

Why then did you not also make empty parentheses optional in the default constructor call of an object creation expression that does not have an object initializer?

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:

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
    }
}

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.

How did you determine that particular ambiguity?

That one was immediately clear; I am pretty familiar with the rules in C# for determining when a dotted name is expected.

When considering a new feature how do you determine whether it causes any ambiguity? By hand, by formal proof, by machine analysis, what?

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":

x = frob 123 + 456;

(UPDATE: frob is of course await; the analysis here is essentially the analysis that the design team went through when adding await.)

"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:

frob x = 10;

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 if x is int*.)

G(frob + x)

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:

yield(x);

Does that mean "yield x in an iterator" or "call the yield method with argument x?" By changing it to

yield return(x);

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.

Are there ambiguities in C# today?

Sure.

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

In C# 1 it is clear what that means. It's the same as:

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

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:

G((T)-x)

Is that "cast -x to T" or "subtract x from T"? Again, we have a heuristic that makes a good guess.

陪你到最终 2024-09-25 14:28:09

因为这就是语言的指定方式。它们没有增加任何价值,那么为什么要包含它们呢?

它也与隐式类型数组非常相似

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

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

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

Reference: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

浸婚纱 2024-09-25 14:28:09

这样做是为了简化对象的构造。语言设计者(据我所知)没有具体说明为什么他们认为这很有用,尽管在 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:

An object creation expression can omit the constructor argument list and enclosing parentheses, provided it includes an object or collection initializer. Omitting the constructor argument list and enclosing parentheses is equivalent to specifying an empty argument list.

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.

流年里的时光 2024-09-25 14:28:09

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

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:

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;
    }
}

All three statements are valid:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};
蓝海 2024-09-25 14:28:09

我不是 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文