一尘不染

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

c#

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

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

相对于:

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

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


阅读 277

收藏
2020-05-19

共1个答案

一尘不染

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

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

  • 设计和规格成本低
  • 无论如何,我们将广泛地更改用于处理对象创建的解析器代码;与较大功能的成本相比,使参数列表为可选的额外开发成本并不大
  • 与较大功能的成本相比,测试负担相对较小
  • 与之相比,文档负担相对较小。
  • 预计维护负担很小;自发布以来,我不记得此功能多年来报告的任何错误。
  • 该功能不会对该地区的未来功能造成任何直接明显的风险。(我们要做的最后一件事是现在制作一个便宜,简单的功能,这使得将来更难以实现更引人注目的功能。)
  • 该功能不会对该语言的词法,语法或语义分析增加任何新的歧义。键入时,IDE的“ IntelliSense”引擎执行的“部分程序”分析不会引起任何问题。等等。
  • 对于较大的对象初始化功能,该功能达到了常见的“最佳点”;通常,如果你使用的是对象初始化正是因为对象的构造并 没有 允许你设置你想要的属性。对于这类对象,很常见的是,它们首先是简单的,没有参数的“属性袋”。

那么,为什么你是不是也让空括号选配中,它的对象创建表达式的默认构造函数调用 不是 有一个对象初始化?

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

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”?同样,我们有一个启发式方法,可以很好地进行猜测。

2020-05-19