C# 为什么可以';除了null之外,我是否给出一个默认值作为可选参数?

C# 为什么可以';除了null之外,我是否给出一个默认值作为可选参数?,c#,optional-parameters,C#,Optional Parameters,我希望有一个可选参数,并将其设置为我确定的默认值,当我执行此操作时: private void Process(Foo f = new Foo()) { } 我得到以下错误(Foo是一个类): “f”是Foo的类型,字符串以外的引用类型的默认参数只能用null初始化 如果我将Foo更改为struct,则它可以工作,但只使用默认的无参数构造函数 我阅读了文档,文档清楚地说明我不能这样做,但没有提到为什么?,为什么会存在此限制,以及为什么字符串被排除在外?为什么可选参数的值必须是编译时常量?如果

我希望有一个可选参数,并将其设置为我确定的默认值,当我执行此操作时:

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

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

“f”是Foo的类型,字符串以外的引用类型的默认参数只能用null初始化

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


我阅读了文档,文档清楚地说明我不能这样做,但没有提到为什么?,为什么会存在此限制,以及为什么
字符串
被排除在外?为什么可选参数的值必须是编译时常量?如果这不是一个常数,那么会有什么副作用呢?

因为除了null之外,没有其他编译时常数了。对于字符串,字符串文本是编译时常量

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

  • 实现的简单性
  • 消除隐藏/意外行为
  • 方法契约的清晰性,特别是在交叉装配场景中
让我们进一步阐述这三个方面,以了解问题的根源:

1.实现的简单性 当限制为常量值时,编译器和CLR的工作都非常简单。常量值可以很容易地存储在程序集元数据中,编译器也可以很容易地保存。如何做到这一点已在中概述

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

  • 存储初始化表达式本身,并在其中编译它们:

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

    // 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)));
    
    private void Process(Foo f = null)
    {
        if (f == null) f = new Foo();
    
    }
    
    从未彻底检查“流程”声明的读者的角度来看,这可能是相当意外的。它会使代码混乱,使其可读性降低

    3.方法契约的清晰性,特别是在交叉装配场景中 方法的签名与默认值一起构成合同。本合同适用于特定的环境。如果初始化表达式需要绑定到其他一些程序集,那么调用者需要什么?这个例子怎么样,其中“CalculateInput”方法来自“Other.Assembly”:

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


    尽管如此,跨程序集场景将强制使用在调用站点的普通源代码中看不清楚的程序集引用。这也是一个可用性和可读性问题。

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


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

    这就是语言的工作方式,我说不出他们为什么这么做(如果你想讨论的话,这个网站也是如此)

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


    一个起点是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
    
    请注意,在编译器使用这两个call语句之后,它们之间是如何没有任何区别的。是编译器应用了默认值,并在调用站点应用了默认值

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

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

    [注意:CLI对这些值不附加任何语义,这完全取决于编译器 实现他们希望的任何语义(例如,所谓的
    private void Process(Foo f = null)
    {
        if (f == null) f = new Foo();
    
    }