我想要一个可选参数并将其设置为我确定的默认值,当我这样做时:
private void Process(Foo f = new Foo())
{
}
我收到以下错误(Foo
是一个类):
' F'是Foo的类型,除string之外的引用类型的默认参数只能用null初始化。
如果我将Foo
更改为struct
,那么它可以正常运行,但只使用默认的无参数构造函数。
我阅读了文档,它明确指出我不能这样做,但它没有提到为什么?,为什么这个限制存在以及为什么string
被排除在外吗?为什么可选参数的值必须是编译时常量?如果这不是一个常数那么副作用会是什么?
答案 0 :(得分:17)
一个起点是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节详细介绍了常数值的含义。最相关的部分:
Type必须是以下之一: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,其值为零
这就是降压停止的地方,甚至没有好的方法来引用匿名帮助器方法,所以某种代码提升技巧也无法工作。
值得注意的是,它只是一个问题,您始终可以为引用类型的参数实现任意默认值。例如:
private void Process(Foo f = null)
{
if (f == null) f = new Foo();
}
这是非常合理的。以及您在方法中而不是呼叫站点想要的代码类型。
答案 1 :(得分:10)
因为没有其他编译时常量而不是null。对于字符串,字符串文字是这样的编译时常量。
我认为其背后的一些设计决策可能是:
让我们详细阐述这三个问题,以便在问题的引导下获得一些见解:
当限制为常量值时,编译器和CLR的作业都非常简单。常量值可以轻松存储在程序集元数据中,编译器可以轻松实现。如何做到这一点在Hans Passant's answer中列出。
但CLR和编译器可以做些什么来实现非常量默认值?有两种选择:
存储初始化表达式,并在那里编译它们:
// seen by the developer in the source code
Process();
// actually done by the compiler
Process(new Foo());
生成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)),但它对感知行为有影响。开发人员希望在调用站点而不是其他地方评估参数。这可能会带来额外的问题(参见与方法合同相关的部分)。
初始化表达式可能是任意复杂的。因此这是一个简单的调用:
Process();
将展开在呼叫站点执行的复杂计算。例如:
Process(new Foo(HorriblyComplexCalculation(SomeStaticVar) * Math.Power(GetCoefficient, 17)));
从读者的角度来看,这可能是相当出乎意料的,因为他没有彻底检查'过程'的声明。它使代码混乱,使其可读性降低。
方法的签名与默认值一起签订合同。该合同生活在特定的背景下。如果初始化表达式需要绑定到某些其他程序集,那么调用者需要什么?这个例子怎么样,'CalculateInput'方法来自'Other.Assembly':
void Process(Foo arg = new Foo(Other.Assembly.Namespace.CalculateInput()))
这就是实现这种方式的关键所在,在思考这是一个问题还是一个问题时起着至关重要的作用。在“简单”部分,我概述了实现方法(1)和(2)。因此,如果选择(1),它将要求调用者绑定到'Other.Assembly'。另一方面,如果选择(2),那么从实现的角度来看,这种规则的需求远远少于此类规则,因为编译器生成的Process_Thunk
与{{1}在同一个地方声明因此自然会引用Process
。 然而,一个理智的语言设计者甚至会施加这样的规则,因为同一事物的多个实现是可能的,并且为了方法契约的稳定性和清晰度。
尽管如此,交叉装配场景会强制执行从调用站点的普通源代码中无法清楚看到的程序集引用。这又是一个可用性和可读性问题。
答案 2 :(得分: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.
}
答案 3 :(得分:2)
默认参数以一种提供默认参数的方式操纵调用者,它将在编译时更改方法签名。因此你需要提供一个恒定值,在你的情况下"新的Foo()"不是。
这就是你需要一个常数的原因。