【问题标题】:Why can't I give a default value as optional parameter except null?为什么我不能将默认值作为除 null 之外的可选参数?
【发布时间】:2014-05-27 00:57:06
【问题描述】:

我想要一个 可选参数 并将其设置为我确定的默认值,当我这样做时:

private void Process(Foo f = new Foo())
{

}

我收到以下错误(Foo 是一个类):

'f'为Foo类型,非string引用类型的默认参数只能用null初始化。

如果我将 Foo 更改为 struct 则它可以工作,但只有默认的 无参数 构造函数。

我阅读了文档,其中明确说明我不能这样做,但没有提及为什么?,为什么存在此限制以及为什么 string 被排除在此之外?为什么可选参数的值必须是 compile-time 常量?如果这不是一个常数,那么会有什么副作用?

【问题讨论】:

  • 这仅适用于引用类型。值类型可以有其他默认值。
  • @T.J.Crowder 是的,我知道。我在问这个参考类型

标签: c# optional-parameters


【解决方案1】:

首先,CLR 对此不提供支持。它必须由编译器实现。你可以从一个小测试程序中看到一些东西:

class Program {
    static void Main(string[] args) {
        Test();
        Test(42);
    }
    static void Test(int value = 42) {
    }
}

反编译成:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  8
  IL_0000:  ldc.i4.s   42
  IL_0002:  call       void Program::Test(int32)
  IL_0007:  ldc.i4.s   42
  IL_0009:  call       void Program::Test(int32)
  IL_000e:  ret
} // end of method Program::Main

.method private hidebysig static void  Test([opt] int32 'value') cil managed
{
  .param [1] = int32(0x0000002A)
  // Code size       1 (0x1)
  .maxstack  8
  IL_0000:  ret
} // end of method Program::Test

请注意编译器完成后两个调用语句之间没有任何区别。是编译器应用了默认值并在调用站点这样做。

另请注意,当 Test() 方法实际存在于另一个程序集中时,这仍然需要工作。这意味着需要在元数据中编码默认值。注意.param 指令是如何做到这一点的。 CLI 规范 (Ecma-335) 在第 II.15.4.1.4 节中记录了它

该指令在元数据中存储一个与方法参数编号 Int32 关联的常量值, 见§II.22.9。虽然 CLI 要求为参数提供一个值,但某些工具可以使用 此属性的存在表明该工具而不是用户旨在提供值 参数。与 CIL 指令不同,.param 使用索引 0 来指定方法的返回值, index 1 指定方法的第一个参数, index 2 指定方法的第二个参数 方法等等。

[注意:CLI 对这些值不附加任何语义——这完全取决于编译器 实现他们希望的任何语义(例如,所谓的默认参数值)。尾注]

引用的第 II.22.9 节详细介绍了常量值的含义。最相关的部分:

类型应为以下之一:ELEMENT_TYPE_BOOLEAN、ELEMENT_TYPE_CHAR、 ELEMENT_TYPE_I1、ELEMENT_TYPE_U1、ELEMENT_TYPE_I2、ELEMENT_TYPE_U2、 ELEMENT_TYPE_I4、ELEMENT_TYPE_U4、ELEMENT_TYPE_I8、ELEMENT_TYPE_U8、 ELEMENT_TYPE_R4、ELEMENT_TYPE_R8 或 ELEMENT_TYPE_STRING; 或 ELEMENT_TYPE_CLASS 的值为 0

所以这就是责任停止的地方,甚至没有好方法来引用匿名帮助方法,因此某种代码提升技巧也无法工作。

值得注意的是,这不是问题,您始终可以为引用类型的参数实现任意默认值。例如:

private void Process(Foo f = null)
{
    if (f == null) f = new Foo();

}

这很合理。以及您在方法而不是调用站点中想要的那种代码。

【讨论】:

  • 感谢您的详细回答。我现在明白了。在您最后的报价中,zero 表示null 对吗?我不太清楚为什么使用零,我对标准了解不多:)
  • 当然,0 表示在元数据中编码时为 null。
  • 除了最后一个例子:如果fnull也有它自己的意义,你可以使用函数重载来提供两种不同风格的Process。事实上,无论如何你都可以这样做。
  • "没有好办法甚至引用辅助方法" 辅助方法可以被参数上的自定义属性引用,类似于 params 实际上是 ParamsArrayAttribute。
【解决方案2】:

因为除了 null 之外没有其他编译时常量。对于字符串,字符串字面量就是这样的编译时常量。

我认为它背后的一些设计决策可能是:

  • 实施简单
  • 消除隐藏/意外行为
  • 方法合同的明确性,特别是。在交叉组装场景中

让我们进一步详细说明这三个问题,以深入了解问题的本质:

1。实现简单

当仅限于常量值时,编译器和 CLR 的工作都非常简单。常量值可以很容易地存储在程序集元数据中,编译器可以很容易地 . Hans Passant's answer 中概述了这是如何完成的。

但是 CLR 和编译器可以做些什么来实现非常量默认值呢?有两种选择:

  1. 存储初始化表达式本身,并在那里编译它们:

    // seen by the developer in the source code
    Process();
    
    // actually done by the compiler
    Process(new Foo());  
    
  2. 生成 thunk:

    // seen by the developer in the source code
    Process();
    …
    void Process(Foo arg = new Foo())
    {
        … 
    }
    
    // actually done by the compiler
    Process_Thunk();
    …
    void Process_Thunk()
    {
        Process(new Foo());
    }
    void Process()
    {
        … 
    }
    

两种解决方案都在程序集中引入了更多新的元数据,并且需要编译器进行复杂的处理。此外,虽然解决方案(2)可以被视为隐藏的技术性(以及(1)),但它对感知行为有影响。开发人员希望在调用站点而不是其他地方评估参数。这可能会带来额外的问题需要解决(参见与方法契约相关的部分)。

2。消除隐藏/意外行为

初始化表达式可能非常复杂。因此像这样的简单调用:

    Process();

将展开到一个复杂的计算中在调用站点执行。例如:

    Process(new Foo(HorriblyComplexCalculation(SomeStaticVar) * Math.Power(GetCoefficient, 17)));

从不彻底检查“Process”声明的读者的角度来看,这可能是相当出乎意料的。它使代码混乱,使其可读性降低。

3。方法合同的明确性,特别是。在交叉组装场景中

方法的签名与默认值一起强加了一个契约。该合同存在于特定的上下文中。如果初始化表达式需要绑定到其他一些程序集,那么调用者需要什么?这个例子怎么样,“CalculateInput”方法来自“Other.Assembly”:

    void Process(Foo arg = new Foo(Other.Assembly.Namespace.CalculateInput()))

在这点上,实施方式在思考这是一个问题还是一个注意事项时起着至关重要的作用。在“简单”部分,我概述了实现方法 (1) 和 (2)。因此,如果选择了 (1),它将要求调用者绑定到“Other.Assembly”。另一方面,如果选择了 (2),则从实现的角度来看,对此类规则的需求要少得多,因为编译器生成的 Process_ThunkProcess 声明在同一位置,因此很自然引用了Other.Aseembly然而,一个理智的语言设计者会强加这样的规则,因为同一事物的多个实现是可能的,并且为了方法契约的稳定性和清晰性。

尽管如此,跨程序集场景会强加在调用站点的普通源代码中无法清楚看到的程序集引用。这又是一个可用性和可读性问题。

【讨论】:

  • 你有关于设计决策的陈述的来源吗?
  • @JohnSaunders 不,我不知道。然而,在编译器和语言设计花了几年时间之后,我有足够的勇气做出一个合格的猜测:-)(我过去也必须解决同样的问题。)
  • @JohnSaunders 是的,将您的编辑合并到我更长的解释中。
【解决方案3】:

这只是语言的工作方式,我不能说他们为什么这样做(还有这个网站is not a site for discussions like that,如果你想讨论它take it to chat)。

我可以向您展示如何解决它,只需创建两个方法并重载它(稍微修改您的示例以显示您将如何返回结果)。

private Bar Process()
{
    return Process(new Foo());
}

private Bar Process(Foo f)
{
    //Whatever.
}

【讨论】:

    【解决方案4】:

    默认参数以您提供默认参数的方式操纵调用者,它将在编译时更改您的方法签名。因此,您需要提供一个常量值,在您的情况下“new Foo()”不是。

    这就是为什么你需要一个常量。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-12
      • 1970-01-01
      • 2021-12-27
      • 2012-10-01
      相关资源
      最近更新 更多