C#4.0:我可以使用TimeSpan作为默认值的可选参数吗?

这两个都会产生一个错误,说他们必须是编译时常量:

void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0)) void Foo(TimeSpan span = new TimeSpan(2000)) 

首先,有人可以解释为什么在编译时无法确定这些值吗? 有没有一种方法来指定一个可选的TimeSpan对象的默认值?

您可以通过更改签名来轻松解决此问题。

 void Foo(TimeSpan? span = null) { if (span == null) { span = TimeSpan.FromSeconds(2); } ... } 

我应该详细说明 – 在你的例子中这些expression式不是编译时常量的原因是因为在编译时,编译器不能简单地执行TimeSpan.FromSeconds(2.0)并将结果的字节粘贴到编译的代码中。

作为一个例子,考虑如果你试图使用DateTime.Now来代替。 DateTime.Now的值每次执行都会更改。 或者,假设TimeSpan.FromSeconds考虑了重力。 这是一个荒谬的例子,但是编译时常量的规则并不是仅仅因为我们知道TimeSpan.FromSeconds是确定性的,

我的VB6的遗产让我感到不安,认为“空值”和“缺失值”是等价的。 在大多数情况下,这可能是好的,但是你可能会有一个意想不到的副作用,或者你可能会吞下特殊的情况(例如,如果span的源是一个属性或variables,不应该为null,但是)。

因此我会重载这个方法:

 void Foo() { Foo(TimeSpan.FromSeconds(2.0)); } void Foo(TimeSpan span) { //... } 

这工作正常:

void Foo(TimeSpan span = default(TimeSpan))

可以用作默认值的一组值可以用于属性参数。 原因是默认值被编码到DefaultParameterValueAttribute内部的元数据中。

至于为什么在编译时无法确定。 C#lang规范的第7.18节列出了在编译时允许的值和expression式集合。 就允许的数值而言,它仅限于

  • 文字包括null
  • 引用其他常量值
  • 枚举值
  • 默认值expression式

TimeSpantypes不适合任何这些列表,因此不能用作常量。

 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值是一个有效的function价值的机会是罕见的。

TimeSpanDefaultValueAttribute一个特例,并且使用可以通过TimeSpan.Parse方法parsing的任何string来指定。

 [DefaultValue("0:10:0")] public TimeSpan Duration { get; set; } 

其他答案已经给出了很好的解释,为什么一个可选参数不能是一个dynamicexpression式。 但是,要重新计算,默认参数的行为就像编译时间常量一样。 这意味着编译器必须能够评估它们并提出答案。 有一些人希望C#增加对编译器在遇到常量声明时评估dynamicexpression式的支持 – 这种特性与标记“纯”的方法有关,但这不是现在的现实,也许永远不会。

对这种方法使用C#默认参数的一种替代方法是使用XmlReaderSettings示例的模式。 在此模式中,使用无参数构造函数和公开可写的属性定义一个类。 然后用这种types的对象replace您的方法中的所有选项。 即使通过为其指定默认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); } } 

要调用,使用一个奇怪的语法来在一个expression式中实例化和分配属性:

 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#被视为编译时常量,并被放到调用站点中,所以一旦重新编译,默认参数将只被代码使用。 通过实例化设置对象,调用者在调用方法时dynamic加载默认值。 这意味着您可以通过更改设置类来更新默认值。 因此,这种模式可以让您更改默认值,而无需重新编译调用者来查看新值,如果需要的话。

我的build议:

  void A( long spanInMs = 2000 ) { var ts = TimeSpan.FromMilliseconds(spanInMs); //... } 

BTW TimeSpan.FromSeconds(2.0)不等于new TimeSpan(2000) – 构造函数需要滴答。