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

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

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

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

61710 次浏览

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

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

C # 6.0-属性参数类型 :

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

  • 下列类型之一: boolbytechardoublefloatintlongsbyteshortstringbyte0、 byte1、 byte2。
  • object型。
  • System.Type型。
  • 枚举类型。
    (只要它具有公共可访问性,并且它所嵌套的类型(如果有的话)也具有公共可访问性)
  • 上述类型的单维数组。

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

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

void Foo(TimeSpan? span = null) {


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


...


}

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

例如,考虑是否尝试使用 DateTime。而现在。DateTime 的值。现在每次执行都会更改。或者假设时间跨度。从秒考虑重力。这是一个荒谬的例子,但是编译时常量的规则不会因为我们碰巧知道 TimeSpan 而产生特殊情况。FromSecond 是确定性的。

我的 VB6遗产使我对于将“空值”和“缺失值”等同起来的想法感到不安。在大多数情况下,这可能没什么问题,但是您可能会产生意想不到的副作用,或者您可能会吞下一个异常条件(例如,如果 span的源是一个不应该为 null 而是为 null 的属性或变量)。

因此,我会让这个方法超载:

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

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

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

这种方法很有效:

void Foo(TimeSpan span = default(TimeSpan))

注: default(TimeSpan) == TimeSpan.Zero

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

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

我的建议是:

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


//...
}

TimeSpan.FromSeconds(2.0)不等于 new TimeSpan(2000)-构造函数采用刻度。