Answers:
您可以通过更改签名来轻松解决此问题。
void Foo(TimeSpan? span = null) {
if (span == null) { span = TimeSpan.FromSeconds(2); }
...
}
我应该详细说明-您的示例中的这些表达式不是编译时常量的原因是因为在编译时,编译器无法简单地执行TimeSpan.FromSeconds(2.0)并将结果的字节粘贴到已编译的代码中。
例如,考虑是否尝试使用DateTime.Now。DateTime.Now的值在每次执行时都会更改。或假设TimeSpan.FromSeconds考虑了重力。这是一个荒谬的示例,但是编译时常量的规则并没有特殊情况,因为我们碰巧知道TimeSpan.FromSeconds是确定性的。
<param>
,因为它在签名中不可见。
span = span ?? TimeSpan.FromSeconds(2.0);
在方法主体中与可为null的类型一起使用。或var realSpan = span ?? TimeSpan.FromSeconds(2.0);
获取不可为空的局部变量。
我的VB6传统使我对考虑“零值”和“缺失值”相等的想法感到不安。在大多数情况下,这可能很好,但是您可能会有意想不到的副作用,或者可能吞下了特殊情况(例如,如果的来源span
是不应该为null而是为null的属性或变量)。
因此,我将重载该方法:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
//...
}
这工作正常:
void Foo(TimeSpan span = default(TimeSpan))
可以用作默认值的值集与可以用于属性参数的值相同。原因是默认值被编码到内的元数据中DefaultParameterValueAttribute
。
至于为什么不能在编译时确定它。官方在C#语言规范中列出了在编译时允许的值和表达式集:
C#6.0-属性参数类型:
属性类的位置和命名参数的类型仅限于属性参数类型,它们是:
- 其中以下类型:
bool
,byte
,char
,double
,float
,int
,long
,sbyte
,short
,string
,uint
,ulong
,ushort
。- 类型
object
。- 类型
System.Type
。- 枚举类型。
(只要它具有公共可访问性,并且嵌套的类型(如果有)也具有公共可访问性)- 上述类型的一维数组。
该类型TimeSpan
不适合这些列表中的任何一个,因此不能用作常量。
TimeSpan
可以适合此列表中的最后一个default(TimeSpan)
有效。
void Foo(TimeSpan span = default(TimeSpan))
{
if (span == default(TimeSpan))
span = TimeSpan.FromSeconds(2);
}
提供default(TimeSpan)
的功能无效。
要么
//this works only for value types which TimeSpan is
void Foo(TimeSpan span = new TimeSpan())
{
if (span == new TimeSpan())
span = TimeSpan.FromSeconds(2);
}
提供的new TimeSpan()
值无效。
要么
void Foo(TimeSpan? span = null)
{
if (span == null)
span = TimeSpan.FromSeconds(2);
}
考虑到null
值是函数有效值的机会很少,因此应该更好。
其他答案也对为什么可选参数不能为动态表达式给出了很好的解释。但是,要重新计数,默认参数的行为类似于编译时间常数。这意味着编译器必须能够对其进行评估并给出答案。有些人希望C#在遇到常量声明时增加对编译器评估动态表达式的支持,这种功能将与将方法标记为“纯”有关,但现在不是现实,而且可能永远不会。
对于这种方法,使用C#默认参数的一种替代方法是使用以表示的模式XmlReaderSettings
。在此模式中,定义一个具有无参数构造函数和可公开写入属性的类。然后,使用此类型的对象将方法中的所有选项替换为默认值。甚至通过null
为其指定默认值来使该对象成为可选对象。例如:
public class FooSettings
{
public TimeSpan Span { get; set; } = TimeSpan.FromSeconds(2);
// I imagine that if you had a heavyweight default
// thing you’d want to avoid instantiating it right away
// because the caller might override that parameter. So, be
// lazy! (Or just directly store a factory lambda with Func<IThing>).
Lazy<IThing> thing = new Lazy<IThing>(() => new FatThing());
public IThing Thing
{
get { return thing.Value; }
set { thing = new Lazy<IThing>(() => value); }
}
// Another cool thing about this pattern is that you can
// add additional optional parameters in the future without
// even breaking ABI.
//bool FutureThing { get; set; } = true;
// You can even run very complicated code to populate properties
// if you cannot use a property initialization expression.
//public FooSettings() { }
}
public class Bar
{
public void Foo(FooSettings settings = null)
{
// Allow the caller to use *all* the defaults easily.
settings = settings ?? new FooSettings();
Console.WriteLine(settings.Span);
}
}
要调用,请使用一种奇怪的语法在一个表达式中实例化和分配属性:
bar.Foo(); // 00:00:02
bar.Foo(new FooSettings { Span = TimeSpan.FromDays(1), }); // 1.00:00:00
bar.Foo(new FooSettings { Thing = new MyCustomThing(), }); // 00:00:02
这是解决这个问题的真正重量级方法。如果您正在编写一个快速且肮脏的内部接口,并使其可以为TimeSpan
空并且将null当作所需的默认值可以正常工作,请改用该方法。
另外,如果您有大量参数或在紧密循环中调用该方法,则将产生类实例化的开销。当然,如果在紧密循环中调用这种方法,则很自然,甚至很容易重用该FooSettings
对象的实例。
正如我在示例中的注释中提到的那样,我认为这种模式对于公共API来说非常有用。向类中添加新属性是一项不间断的ABI更改,因此您可以使用此模式添加新的可选参数,而无需更改方法的签名-为更多的最近编译的代码提供更多选项,同时继续支持旧的已编译代码,而无需进行其他工作。
另外,由于C#的内置默认方法参数被视为编译时常量,并且已烘焙到调用站点中,因此默认参数仅在代码重新编译后才能使用。通过实例化设置对象,调用方在调用您的方法时动态加载默认值。这意味着您可以通过更改设置类来更新默认值。因此,如果需要,此模式可让您更改默认值,而不必重新编译调用方以查看新值。
new TimeSpan(2000)
这并不意味着2000毫秒,而是2000个“滴答声”,即0.2毫秒,即2秒的十分之一。