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

时间:2014-11-26 09:35:48

标签: c# .net struct default-constructor default-arguments

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创建对象。 如果结构是一个类,行为是valid==false,这是我所期望的。

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

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

5 个答案:

答案 0 :(得分:8)

在C#中(至少直到C#6 - see blog post),调用new Test()等同于编写default(Test) - 实际上没有调用构造函数,提供了默认值。

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

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

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

        

      到目前为止,编译器实现确实已经优化了#34; '新T()'当T是结构时,本质上意味着默认(T)。这实际上是一个错误 - 它总是应该调用一个实际的无参数构造函数(如果有的话) - 它可能一直存在,因为它在IL中是允许的。

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

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

答案 1 :(得分:2)

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

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

答案 2 :(得分:2)

  

我发现这种行为差异,尤其是行为   结构案例令人惊讶和不直观 - 发生了什么?什么   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的调用:

  

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

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

@JonSkeet在对Does using "new" on a struct allocate it on the heap or stack?

的回答中深入探讨了这一点

修改

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

  

@Yuval和其他人,关于结构上的无参数构造函数:   要意识到的是,在此之前和现在,构造函数都没有   必然在结构上运行。我们所做的只是增加了拥有的能力   无参数构造函数也无法保证运行。那里   没有合理的方法来保证已经存在的结构   初始化和无参数构造函数对此没有帮助。

     

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

     

我认为混淆的主要原因是允许'新S()'   意思是'默认(S)'。这是语言和我的历史错误   我非常希望能把它带走。我强烈劝阻任何人   在没有无参数的结构上使用'new S()'   构造即可。据我所知,这是因为默认(S)   C#1.0中不存在语法,所以这只是用于的语法   获取结构的默认值。

     

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

     

因此语义是干净的:新的S()是运行a的唯一方法   struct上的无参数构造函数,总是运行它   构造函数 - 甚至通过泛型。

答案 3 :(得分:1)

我怀疑这是因为c#中不允许使用没有参数的默认构造函数,因此当您调用不带参数的构造函数Test时,它只是像往常一样初始化它们。查看此帖子了解更多详情(不完全重复):Why can't I define a default constructor for a struct in .NET?

答案 4 :(得分:1)

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

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