当存在无参数构造函数时,似乎 C# 3.0 对象初始值设定项语法允许在构造函数中排除开/关括号对。例子:
var x = new XTypeName { PropA = value, PropB = value };
相对于:
var x = new XTypeName() { PropA = value, PropB = value };
我很好奇为什么构造函数开/关括号对在这里是可选的XTypeName
?
当存在无参数构造函数时,似乎 C# 3.0 对象初始值设定项语法允许在构造函数中排除开/关括号对。例子:
var x = new XTypeName { PropA = value, PropB = value };
相对于:
var x = new XTypeName() { PropA = value, PropB = value };
我很好奇为什么构造函数开/关括号对在这里是可选的XTypeName
?
这个问题是我 2010 年 9 月 20 日博客的主题。Josh 和 Chad 的回答(“它们没有增加任何价值,所以为什么需要它们?”和“消除冗余”)基本上是正确的。再充实一点:
允许您将参数列表作为对象初始值设定项的“更大功能”的一部分省略的功能符合我们对“糖分”功能的要求。我们考虑的几点:
那么为什么在没有对象初始值设定项的对象创建表达式的默认构造函数调用中,空括号不是可选的呢?
再看看上面的标准列表。其中之一是该更改不会在程序的词汇、语法或语义分析中引入任何新的歧义。您提出的更改确实引入了语义分析歧义:
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;
则解析并且是合法的。)x
int*
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 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”?同样,我们有一个很好的猜测的启发式方法。
因为这就是指定语言的方式。它们没有增加任何价值,那么为什么要包括它们呢?
它也非常类似于隐式类型数组
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
这样做是为了简化对象的构造。语言设计者没有(据我所知)具体说明为什么他们认为这很有用,尽管在C# 版本 3.0 规范页面中明确提到了这一点:
对象创建表达式可以省略构造函数参数列表和括号,前提是它包含对象或集合初始值设定项。省略构造函数参数列表并用括号括起来等效于指定一个空的参数列表。
我想他们认为,在这种情况下,括号对于显示开发人员意图是不必要的,因为对象初始化程序显示了构造和设置对象属性的意图。
在您的第一个示例中,编译器推断您正在调用默认构造函数(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 = "!"};
我不是 Eric Lippert,所以我不能肯定地说,但我认为这是因为编译器不需要空括号来推断初始化构造。因此它成为冗余信息,并且不需要。