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


125

这两个都会产生错误,指出它们必须是编译时常量:

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

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


11
与您的要求无关,但是请注意,new TimeSpan(2000)这并不意味着2000毫秒,而是2000个“滴答声”,即0.2毫秒,即2秒的十分之一。
Jeppe Stig Nielsen

Answers:


172

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

void Foo(TimeSpan? span = null) {

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

   ...

}

我应该详细说明-您的示例中的这些表达式不是编译时常量的原因是因为在编译时,编译器无法简单地执行TimeSpan.FromSeconds(2.0)并将结果的字节粘贴到已编译的代码中。

例如,考虑是否尝试使用DateTime.Now。DateTime.Now的值在每次执行时都会更改。或假设TimeSpan.FromSeconds考虑了重力。这是一个荒谬的示例,但是编译时常量的规则并没有特殊情况,因为我们碰巧知道TimeSpan.FromSeconds是确定性的。


15
现在,在中记录默认值<param>,因为它在签名中不可见。
Panic Panic 2012年

3
我无法执行此操作,我将特殊值null用于其他用途。
Panic Panic 2012年

4
@MattHickford-然后,您必须提供一个重载的方法或以毫秒为参数。
2012年

19
也可以span = span ?? TimeSpan.FromSeconds(2.0);在方法主体中与可为null的类型一起使用。或var realSpan = span ?? TimeSpan.FromSeconds(2.0);获取不可为空的局部变量。
Jeppe Stig Nielsen

5
我对此不喜欢的是,它向该函数的用户暗示该函数以空范围“工作”。但这不是真的!就函数的实际逻辑而言,Null 不是有效的span值。我希望有一种更好的方式,看起来好像没有代码的味道……
JoeCool

31

我的VB6传统使我对考虑“零值”和“缺失值”相等的想法感到不安。在大多数情况下,这可能很好,但是您可能会有意想不到的副作用,或者可能吞下了特殊情况(例如,如果的来源span是不应该为null而是为null的属性或变量)。

因此,我将重载该方法:

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

1
这项很棒的技术+1。实际上,默认参数仅应与const类型一起使用。否则,这是不可靠的。
Lazlo

2
这是默认的值替换的“时间已久”方法,在这种情况下,我认为这是最丑陋的答案;)就其本身而言,它不一定适用于接口,因为您确实想要默认值一个地方。在这种情况下,我发现扩展方法是一个有用的工具:接口有一个包含所有参数的方法,然后在接口旁边的静态类中声明的一系列扩展方法在各种重载中实现了默认值。
OlduwanSteve 2014年

23

这工作正常:

void Foo(TimeSpan span = default(TimeSpan))


4
欢迎使用堆栈溢出。您的答案似乎是,您可以提供一个默认参数值,只要它是编译器允许的一个非常具体的值即可。我明白吗?(您可以编辑您的答案以澄清问题。)如果它显示了如何利用编译器允许的功能来获取最初查询的问题,该问题将具有任意其他 TimeSpan值,例如,new TimeSpan(2000)
罗伯·肯尼迪

2
使用某些特定默认值的替代方法是使用私有静态只读TimeSpan defaultTimespan = Timespan.FromSeconds(2)结合默认构造函数和采用时间跨度的构造函数。public Foo():this(defaultTimespan)和public Foo(Timespan ts)
johanmårtensson17年

15

可以用作默认值的值集与可以用于属性参数的值相同。原因是默认值被编码到内的元数据中DefaultParameterValueAttribute

至于为什么不能在编译时确定它。官方在C#语言规范中列出了在编译时允许的值和表达式集:

C#6.0-属性参数类型

属性类的位置和命名参数的类型仅限于属性参数类型,它们是:

  • 其中以下类型:boolbytechardoublefloatintlongsbyteshortstringuintulongushort
  • 类型object
  • 类型System.Type
  • 枚举类型。
    (只要它具有公共可访问性,并且嵌套的类型(如果有)也具有公共可访问性)
  • 上述类型的一维数组。

该类型TimeSpan不适合这些列表中的任何一个,因此不能用作常量。


2
轻微的挑剔:调用静态方法不适合任何列表。TimeSpan可以适合此列表中的最后一个default(TimeSpan)有效。
CodesInChaos 2011年

12
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值是函数有效值的机会很少,因此应该更好。


4

TimeSpan是的特殊情况,DefaultValueAttribute并且使用可以通过TimeSpan.Parse方法解析的任何字符串来指定。

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

3

我的建议:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0)不相等new TimeSpan(2000)-构造函数会产生刻度。


2

其他答案也对为什么可选参数不能为动态表达式给出了很好的解释。但是,要重新计数,默认参数的行为类似于编译时间常数。这意味着编译器必须能够对其进行评估并给出答案。有些人希望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#的内置默认方法参数被视为编译时常量,并且已烘焙到调用站点中,因此默认参数仅在代码重新编译后才能使用。通过实例化设置对象,调用方在调用您的方法时动态加载默认值。这意味着您可以通过更改设置类来更新默认值。因此,如果需要,此模式可让您更改默认值,而不必重新编译调用方以查看新值。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.