结构初始化和默认参数的非直观行为

public struct Test { public double Val; public Test(double val = double.NaN) { Val = val; } public bool IsValid { get { return !double.IsNaN(Val); } } } Test myTest = new Test(); bool valid = myTest.IsValid; 

上面给出了valid==true因为没有调用默认arg的构造函数,并且使用标准默认值val = 0.0创建对象。
如果struct是一个类,行为是valid==false ,这是我所期望的。

我发现行为的这种差异,特别是结构案例中的行为令人惊讶和不直观 – 发生了什么? stuct构造的默认arg服务是什么? 如果它无用为什么要让这个编译?

更新:澄清这里的重点不在于行为是什么 – 而是为什么在没有警告的情况下进行编译并且行为不直观。 即如果未应用默认arg,因为在新的Test()情况下没有调用构造函数,那么为什么要让它编译?

在C#中(至少在C#6之前 – 参见博客文章 ),调用new Test()等同于编写default(Test) – 实际上没有调用构造函数,提供了默认值。

默认arg没有任何意义,发生的事情是它可能是编译器实现中疏忽的结果,因为可选参数仅在C#4中添加:

  • 检查可选参数与现有重载不冲突的代码不知道在结构的情况下可能与初始化程序冲突;
  • 翻译new Test()意味着的代码可能不知道可选参数的存在;

    • 在深入评论之后,我注意到Mads Torgersen的以下gem:

      确实,当T是结构时,编译器实现到目前为止已经“优化”了“新T()”以表示基本上默认(T)。 这实际上是一个错误 – 它总是应该调用一个实际的无参数构造函数(如果有的话) – 它可能一直存在,因为它在IL中是允许的。

      对于您的示例,这意味着new Test()被编译器有效地替换为default(Test) – 因此这是一个错误,将在下一版本的Visual Studio中修复。

换句话说,你有一个角落案例。 这可能是观察下一版Visual Studio中的行为方式的好时机,因为这种行为正在发生变化。

对于所有值类型Tnew T()default(T)是等效的。 它们不调用任何构造函数,它们只是将所有字段设置为零。 这也是C#不允许你编写无参数构造函数的原因: public Test() { Val = double.NaN; } 不会编译,因为没有办法使用该构造函数。

你找到了一个角落案例。 你的构造函数看起来像是用于new T() 。 由于您的类型仍然是值类型,因此不使用它。 由于可以调用构造函数,因此不会发出错误。

我发现行为的这种差异,特别是结构案例中的行为令人惊讶和不直观 – 发生了什么? stuct构造的默认arg服务是什么? 如果它无用为什么要让这个编译?

它没什么用。 发出的IL代码不会使用默认参数生成对构造函数的调用,但会调用default(Test) 。 编译器发出警告说不会调用构造函数(尽管这是一个实现细节)似乎是完全合理的。 我在http://connect.microsoft.com上提交了一个问题

如果我们查看生成的IL代码:

 Test myTest = new Test(); bool valid = myTest.IsValid; 

走着瞧:

 IL_0000: ldloca.s 00 // myTest IL_0002: initobj UserQuery.Test // default(Test); IL_0008: ldloca.s 00 // myTest IL_000A: call UserQuery+Test.get_IsValid 

注意在IL中进行的调用不是对构造函数的方法调用(看起来像: call Test..ctor ),它生成了对initobj的调用:

将指定地址处的值类型的每个字段初始化为空引用或适当原始类型的0。 与Newobj不同, initobj不会调用构造函数方法。 Initobj用于初始化值类型,而newobj用于分配和初始化对象。

这意味着编译器只是忽略带有默认参数的构造函数,因为直到C#-6.0才禁止声明这样的构造函数。

@JonSkeet在他的回答中深入探讨了在结构上使用“new”在堆或堆栈上分配它吗?

编辑

我实际上问过Mads Torgerson关于C#-6.0中无参数构造函数的新用法的问题,我认为这是相关的,他说:

@Yuval和其他人,关于结构上的无参数构造函数:要意识到的事情是,在之前和现在, 构造函数不一定在结构上运行。 我们所做的只是添加了一个无参数构造函数的能力,也无法保证运行 。 没有合理的方法来保证已经初始化的结构,并且无参数构造函数对此没有帮助。

无参数构造函数帮助你的东西是允许你有一个无参数构造函数。

我认为混淆的一个主要原因是‘新S()’允许表示’默认(S)’。 这是语言中的历史错误,我非常希望能把它拿走。 我强烈反对任何人在没有无参数构造函数的结构上使用’new S()’ 。 据我所知,这是因为C#1.0中不存在默认(S)语法,所以这只是用于获取结构的默认值的语法。

确实,当T是结构时,编译器实现到目前为止已经“优化”了“新T()”以表示基本上默认(T)。 这实际上是一个错误 – 它总是应该调用一个实际的无参数构造函数(如果有的话) – 它可能一直存在,因为它在IL中是允许的。 我们正在解决这个问题,因此我们甚至会在通用情况下调用构造函数。

因此语义是干净的:新的S()是在结构上运行无参数构造函数的唯一方法,它总是运行该构造函数 – 即使通过generics。

我怀疑这是因为c#中不允许使用没有参数的默认构造函数,因此当您调用不带参数的构造函数Test时,它只是像往常一样初始化它们。 查看这篇文章了解更多细节(不太重复): 为什么我不能在.NET中为结构定义默认构造函数?

因为struct不能有用户定义的无参数构造函数。

Test(double val = double.NaN) 看起来像一个,但它实际上编译为Test(double val) ,其中包含一些关于默认值的元数据。