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式
TimeSpan
types不适合任何这些列表,因此不能用作常量。
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价值的机会是罕见的。
TimeSpan
是DefaultValueAttribute
一个特例,并且使用可以通过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)
– 构造函数需要滴答。