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

时间:2010-01-30 17:50:32

标签: c# c#-4.0 default-value timespan optional-parameters

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

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

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

8 个答案:

答案 0 :(得分:155)

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

void Foo(TimeSpan? span = null) {

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

   ...

}

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

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

答案 1 :(得分:25)

我的VB6遗产让我感到不安,认为“空值”和“缺失值”是等价的。在大多数情况下,它可能没什么问题,但是你可能会产生意想不到的副作用,或者你可能会吞下一个异常条件(例如,如果span的来源是一个不应为null的属性或变量,那么)。

因此我会重载该方法:

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

答案 2 :(得分:18)

这很好用:

void Foo(TimeSpan span = default(TimeSpan))

答案 3 :(得分:13)

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

至于为何在编译时无法确定。在编译时允许的值和表达式的集合列在官方C# language spec

  

C# 6.0 - Attribute parameter types

     

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

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

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

答案 4 :(得分:11)

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值为函数的有效值的可能性很小,这应该会更好。

答案 5 :(得分:4)

TimeSpanDefaultValueAttribute的一个特例,使用任何可以通过TimeSpan.Parse方法解析的字符串来指定。

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

答案 6 :(得分:2)

Other answers已经给出了很好的解释,为什么可选参数不能是动态表达式。但是,要重新计算,默认参数的行为类似于编译时常量。这意味着编译器必须能够对它们进行评估并得出答案。有些人希望C#在遇到常量声明时添加对编译器评估动态表达式的支持 - 这种特性与标记方法“纯粹”有关,但这现在不是现实,可能永远不会成为现实。 / p>

对这种方法使用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

缺点

这是解决此问题的一种非常重要的方法。如果你正在编写一个快速而肮脏的内部界面,并且making the TimeSpan nullable and treating null like your desired default value可以正常工作,那就改为。

此外,如果您有大量参数或正在紧密循环中调用该方法,则会产生类实例化的开销。当然,如果在紧密循环中调用这样的方法,重用FooSettings对象的实例可能很自然,甚至非常容易。

优势

正如我在示例中的评论中提到的,我认为这种模式非常适合公共API。向类添加新属性是一个非破坏性的ABI更改,因此您可以使用此模式添加新的可选参数而无需更改方法的签名 - 为最近编译的代码提供更多选项,同时继续支持旧编译代码而无需额外工作

此外,由于C#内置的默认方法参数被视为编译时常量并被烘焙到调用点,因此默认参数只有在重新编译后才会被代码使用。通过实例化设置对象,调用方在调用方法时动态加载默认值。这意味着您只需更改设置类即可更新默认值。因此,此模式允许您更改默认值,而无需重新编译调用者以查看新值(如果需要)。

答案 7 :(得分:2)

我的建议:

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

    //...
}

顺便说一句 TimeSpan.FromSeconds(2.0)不等于new TimeSpan(2000) - 构造函数需要刻度。