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

Posted

技术标签:

【中文标题】C# 4.0:我可以使用 TimeSpan 作为具有默认值的可选参数吗?【英文标题】:C# 4.0: Can I use a TimeSpan as an optional parameter with a default value? 【发布时间】:2011-01-11 05:54:38 【问题描述】:

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

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

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

【问题讨论】:

与您的要求无关,但请注意,new TimeSpan(2000) 并不表示 2000 毫秒,而是表示 2000 个“滴答”,即 0.2 毫秒,或两秒的万分之一。跨度> 【参考方案1】:

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

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> 中记录默认值,因为它在签名中不可见。 我不能这样做,我正在使用特殊值 null 来做其他事情。 @MattHickford - 然后你必须提供一个重载的方法或以毫秒为参数。 也可以在方法体中使用span = span ?? TimeSpan.FromSeconds(2.0); 和可空类型。或var realSpan = span ?? TimeSpan.FromSeconds(2.0); 获取一个不可为空的局部变量。 我不喜欢这个的一点是,它向函数的用户暗示这个函数在空跨度下“工作”。但这不是真的!就函数的实际逻辑而言,Null 不是 span 的有效值。我希望有一种更好的方法,看起来不像代码味道......【参考方案2】:

我的 VB6 传统让我对将“空值”和“缺失值”视为等价的想法感到不安。在大多数情况下,它可能没问题,但您可能会产生意想不到的副作用,或者您可能会吞下异常情况(例如,如果span 的源是一个不应为空但为空的属性或变量)。

因此我会重载该方法:

void Foo()

    Foo(TimeSpan.FromSeconds(2.0));

void Foo(TimeSpan span)

    //...

【讨论】:

+1 为这项伟大的技术。实际上,默认参数只能与 const 类型一起使用。否则,它是不可靠的。 这是默认值替换的“历史悠久”的方法,对于这种情况,我认为这是最不难看的答案;)虽然它本身并不一定适用于接口,因为您真的希望将默认值放在一个地方。在这种情况下,我发现扩展方法是一个有用的工具:接口有一个包含所有参数的方法,然后在接口旁边的静态类中声明的一系列扩展方法在各种重载中实现默认值。 好方法!我真的更喜欢这个,而不是已经提到的所有其他人。它使您可以更好地控制默认值【参考方案3】:

这很好用:

void Foo(TimeSpan span = default(TimeSpan))

注意:default(TimeSpan) == TimeSpan.Zero

【讨论】:

欢迎来到 Stack Overflow。您的答案似乎是您可以提供一个默认参数值,只要它是编译器允许的一个非常具体的值。我理解对了吗? (你可以edit你的答案来澄清。)如果它展示了如何利用编译器允许的东西来解决最初寻求的问题,这将是一个更好的答案,即任意 other i> TimeSpan 值,如new TimeSpan(2000) 给出的值。 使用某些特定默认值的替代方法是使用私有静态只读 TimeSpan defaultTimespan = Timespan.FromSeconds(2) 结合默认构造函数和采用时间跨度的构造函数。 public Foo() : this(defaultTimespan) 和 public Foo(Timespan ts)【参考方案4】:

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

至于为什么在编译时无法确定。官方C# language spec中列出了编译时允许的值和表达式集:

C# 6.0 - Attribute parameter types:

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

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

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

【讨论】:

轻微挑剔:调用静态方法不适合任何列表。 TimeSpan 可以容纳此列表中的最后一个 default(TimeSpan) 有效。【参考方案5】:
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 值作为函数有效值的可能性很小,这应该会更好。

【讨论】:

【参考方案6】:

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

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

【讨论】:

【参考方案7】:

我的建议:

void A( long spanInMs = 2000 )

    var ts = TimeSpan.FromMilliseconds(spanInMs);

    //...

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

【讨论】:

【参考方案8】:

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

【讨论】:

以上是关于C# 4.0:我可以使用 TimeSpan 作为具有默认值的可选参数吗?的主要内容,如果未能解决你的问题,请参考以下文章

C#格式化TimeSpan以显示日期[重复]

C# Timespan - 用户定义的客户格式说明符

C# Stopwatch与TimeSpan详解(转)

在 C# 中处理 TimeSpan 异常

使用 SqlDataAdapter 时获取 SQL Server DATEDIFF 列作为 TimeSpan

在 javascript 中打印 C# TimeSpan 值