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

150

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

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

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


14
和您所问的问题无关,但请注意 new TimeSpan(2000) 并不意味着 2000 毫秒,而是表示 2000 "ticks",即 0.2 毫秒,或两秒钟的一万分之一。 - Jeppe Stig Nielsen
10个回答

205

你可以非常容易地通过更改你的签名来解决这个问题。

void Foo(TimeSpan? span = null) {

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

   ...

}

我应该解释一下 - 你例子中那些表达式不是编译时常量的原因是,在编译时,编译器无法简单地执行 TimeSpan.FromSeconds(2.0),并将结果的字节放入到编译后的代码中。

举个例子,假设你尝试使用 DateTime.Now。DateTime.Now 的值每次执行都会改变。或者假设 TimeSpan.FromSeconds 考虑到了重力因素。这是一个荒谬的例子,但编译时常量的规则并不会因为我们知道 TimeSpan.FromSeconds 是确定性的而特别处理这种情况。


21
现在在<param>中记录默认值,因为它在函数签名中不可见。 - Colonel Panic
4
我不能这样做,我正在为其他事情使用特殊值 null。 - Colonel Panic
5
那么你需要提供一个重载的方法,或者将毫秒作为参数。 - Josh
19
在方法体中,您也可以使用可空类型 span = span ?? TimeSpan.FromSeconds(2.0);。或者使用 var realSpan = span ?? TimeSpan.FromSeconds(2.0); 来获取一个非可空的本地变量。 - Jeppe Stig Nielsen
9
我不喜欢这个的原因是它会给函数的使用者暗示这个函数可以“处理”空串。但实际上并非如此!就这个函数的逻辑而言,空串不是一个有效的值。我希望能有一种更好的方式来解决这个问题,而不像现在这样让人感到不舒服... - JoeCool
显示剩余4条评论

37

由于我的VB6背景,我对将“null值”和“缺失值”视为等效的想法感到不安。在大多数情况下,这可能是可以接受的,但您可能会产生意外的副作用,或者您可能会忽略异常条件(例如,如果的源是不应为空但实际为空的属性或变量)。

因此,我会重载该方法:

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

5
为了那种出色的技巧点赞。默认参数应该只用于 const 类型,不然就不可靠了。 - Lazlo
3
这是传统的方法,被默认值所取代,并且在这种情况下我认为这是最不难看的答案 ;) 但单独使用它可能不太适用于接口,因为您真正希望将默认值放在一个位置上。在这种情况下,我发现扩展方法是一个有用的工具:接口有一个包含所有参数的方法,然后在静态类中声明一系列扩展方法,以各种重载形式实现默认值。 - OlduwanSteve
1
很棒的方法!我真的比其他提到的所有方法都更喜欢它。它可以让你对默认值有更多的控制。 - Newteq Developer

32

这段代码是有效的:

void Foo(TimeSpan span = default(TimeSpan))

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


6
欢迎来到 Stack Overflow。您的回答似乎是说,只要使用编译器允许的某个非常具体的默认参数值,就可以提供默认参数值。我的理解是否正确?(您可以编辑您的答案以澄清。)如果展示如何利用编译器允许的内容来实现最初所需的任意其他“TimeSpan”值,比如由“new TimeSpan(2000)”给出的值,那么这将是一个更好的答案。 - Rob Kennedy
3
一个使用特定默认值的替代方案是使用私有的静态只读TimeSpan defaultTimespan = TimeSpan.FromSeconds(2),结合默认构造函数和带有TimeSpan参数的构造函数。public Foo() : this(defaultTimespan) 和 public Foo(TimeSpan ts)。 - johan mårtensson

17
可以用作默认值的值集合与属性参数相同。这是因为默认值编码到DefaultParameterValueAttribute中的元数据中。为什么不能在编译时确定它是由于编译时允许使用的值和表达式的集合列在官方的C#语言规范中:

C# 6.0-属性参数类型:

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

  • 以下类型之一:boolbytechardoublefloatintlongsbyteshortstringuintulongushort
  • 类型object
  • 类型System.Type
  • 枚举类型。
    (只要它具有公共可访问性,并且其中嵌套的类型(如果有)也具有公共可访问性)
  • 上述类型的单维数组。
类型TimeSpan不属于这些列表中的任何一个,因此不能用作常量。

2
小小的挑剔:调用静态方法不符合列表中的任何一个。TimeSpan 可以适用于此列表中的最后一个,default(TimeSpan) 是有效的。 - CodesInChaos
所以不能使用const的原因是因为C#文档这样说?这并不是一个很有说服力的论点。显然,时间段“两秒钟”在编译时是已知的,因为我可以将其写下来。这是语言的技术限制,不支持这样的用法。 - pooya13
所以不能使用const的原因是因为C#文档这样说?这不是一个很有说服力的论点。显然,"两秒"的时间跨度在编译时是已知的,因为我可以写下来。这是语言的技术限制,不支持这样的用法。 - undefined
任何 TimeSpan(<literal-number>) 都应该被支持为 const - pooya13
任何 TimeSpan(<literal-number>) 都应该被支持为 const - undefined

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,因此这样做应该会更好。


7

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

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

3

我的建议:

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

    //...
}

顺便提一下,TimeSpan.FromSeconds(2.0) 并不等于 new TimeSpan(2000) - 因为构造函数接受的是时钟周期。


2

其他答案已经给出了为什么可选参数不能是动态表达式的很好的解释。但是,简单概括一下,C#中的默认参数就像编译时常量一样工作。这意味着编译器必须能够计算它们并得出答案。有些人希望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#内置的默认方法参数被视为编译时常量并嵌入到调用站点中,因此只有在重新编译代码后,才会使用默认参数。通过实例化设置对象,调用者在调用您的方法时动态加载默认值。这意味着您可以通过仅更改设置类来更新默认值。因此,如果需要,此模式使您可以更改默认值而无需重新编译调用者以查看新值。


0
我知道这个问题很旧,而且早就有人回答了,但我发现自己还在阅读它...
看起来最好的方法是通过重载来“伪造”它。
void Foo(TimeSpan span = new TimeSpan(2000))

是“wanted”,您可以提供2个重载:
void Foo(int ticks = 2000) => Foo(new TimeSpan(ticks));
void Foo(TimeSpan span) => [implementation]

这样可以让对 Foo() 的调用变得更清晰,可以按照你的意愿进行操作。整数重载在一般情况下也是可以的。如果需要的话,你可以将整数重载改为以秒或其他单位为基准。
如果你已经有了复杂的参数列表,这种方法可能不适用,但在许多情况下,这似乎是在C#中前进的“唯一”方式。

-1

为了指定结构类型参数的默认值,我建议使用重载:

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

void Foo(TimeSpan time)
{
    // ...
}

这个答案是另一个早些时候发布的答案的复制 - https://dev59.com/kHI95IYBdhLWcg3wwwyY#4557502。甚至提供的代码示例也完全相同。将来请确保在发布自己的答案之前阅读其他答案。 - Kacper
哇...我没注意到,看起来像是巧合。我在sharplab.io上也有相同的代码示例。而且我碰巧使用了相同的参数:2.0... - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接