【问题标题】:Unintuitive behaviour with struct initialization and default arguments结构初始化和默认参数的不直观行为
【发布时间】:2015-01-24 13:45:36
【问题描述】:
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,这是我所期望的。

我发现这种行为差异,尤其是 struct 案例中的行为令人惊讶且不直观——这是怎么回事? stuct 构造上的默认 arg 有什么作用? 如果它没用,为什么要编译?

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

【问题讨论】:

  • 我想说的是,如果不传递参数,那么您就是在将所有成员设置为其默认值的结构上调用 implicit default constructor,而不是调用您的构造函数
  • @DanielJ.G.是的,情况就是这样——但我的意思是,这是不直观的。它不是关于发生了什么,而是关于为什么它会在没有警告的情况下编译。
  • 除了通过反射之外,您可以编写没有合理方式直接调用它们的方法/构造函数(不像这里您可以提供参数,并且默认值是“无意义的”)。 C# 编译器必须非常复杂(并且可能必须解决停止问题)才能防止您编译此类代码。你可能打算它只能通过反射使用,因为编译器都知道。
  • @Damien_The_Unbeliever 认为这种统一的行为可能是设计使然,这样您就可以通过反射使用默认参数……这在专业方面实际上是无穷大的,而且大象大小松散不利的一面。
  • 不,我的意思是编译器不应该有您要求的极其复杂的机器来防止此代码编译。跨度>

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


【解决方案1】:

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

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

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

    • 在深入研究 cmets 之后,我注意到Mads Torgersen 的以下宝石:

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

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

换句话说,你有一个极端情况。这可能是了解其在下一个版本的 Visual Studio 中的行为方式的好时机,因为这种行为正在发生变化。

【讨论】:

  • 顺便说一句,VB.NET 中的情况稍差一些,除了new T() 之外,它缺少与default(T) 对应的便捷类型等效项,如前所述,这并不是真正等效的。
  • 显然自 2014 年以来没有任何变化。我在 dotnet core 1.1 控制台应用程序中运行 @Ricibob 代码,结果仍然是 valid == true
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    我发现这种行为差异,尤其是 struct 案例令人惊讶且不直观 - 发生了什么?什么 stuct 构造上的默认参数是否有效?如果它没用为什么 让这个编译?

    它没有任何作用。发出的 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 不调用构造函数方法。初始化对象 用于初始化值类型,而 newobj 用于 分配和初始化对象。

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

    @JonSkeet 在他对Does using "new" on a struct allocate it on the heap or stack? 的回答中对此进行了深入探讨

    编辑

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

    @Yuval 和其他人,关于结构上的无参数构造函数: 要意识到的是,在过去和现在,构造函数不会 必须在结构上运行。我们所做的只是增加了拥有一个 也不能保证运行的无参数构造函数。那里 没有合理的方式来保证结构 已初始化,无参数构造函数对此无济于事。

    无参数构造函数的帮助是让你拥有 一个无参数的构造函数。

    我认为混淆的一个主要来源是允许 'new S()' 意思是“默认(S)”。这是语言中的一个历史错误,我 真希望我能把它拿走。我会强烈劝阻任何人 在没有无参数的结构上使用“new S()” 构造函数。据我所知,这是因为默认(S) C# 1.0 中不存在语法,所以这只是用于 获取结构的默认值。

    到目前为止,编译器的实现确实已经“优化”了 当 T 是一个结构时,'new T()' 本质上意味着 default(T)。那是 实际上是一个错误 - 它总是应该调用一个实际的 无参数构造函数(如果有) - 可能有 一直以来,因为它在 IL 中是允许的。我们正在解决这个问题,以便 即使在一般情况下,我们也会调用构造函数。

    因此语义是干净的:new S() 是运行 结构上的无参数构造函数,它总是运行 构造函数——甚至通过泛型。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

        • 但是为什么 C# 团队会让这个通过 - 行为是反直觉的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-25
        • 1970-01-01
        • 2020-02-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多