这两个都会生成一个错误,指出它们必须是编译时常量:
void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new TimeSpan(2000))
首先,有人能解释一下为什么这些值无法在编译时确定吗?还有没有一种方法可以为可选的TimeSpan对象指定默认值?
这两个都会生成一个错误,指出它们必须是编译时常量:
void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new TimeSpan(2000))
首先,有人能解释一下为什么这些值无法在编译时确定吗?还有没有一种方法可以为可选的TimeSpan对象指定默认值?
你可以非常容易地通过更改你的签名来解决这个问题。
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>
中记录默认值,因为它在函数签名中不可见。 - Colonel Panicspan = span ?? TimeSpan.FromSeconds(2.0);
。或者使用 var realSpan = span ?? TimeSpan.FromSeconds(2.0);
来获取一个非可空的本地变量。 - Jeppe Stig Nielsen由于我的VB6背景,我对将“null值”和“缺失值”视为等效的想法感到不安。在大多数情况下,这可能是可以接受的,但您可能会产生意外的副作用,或者您可能会忽略异常条件(例如,如果的源是不应为空但实际为空的属性或变量)。
因此,我会重载该方法:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
//...
}
这段代码是有效的:
void Foo(TimeSpan span = default(TimeSpan))
注意:default(TimeSpan) == TimeSpan.Zero
DefaultParameterValueAttribute
中的元数据中。为什么不能在编译时确定它是由于编译时允许使用的值和表达式的集合列在官方的C#语言规范中:
属性类的位置和命名参数类型限于属性参数类型,它们是:
bool
、byte
、char
、double
、float
、int
、long
、sbyte
、short
、string
、uint
、ulong
、ushort
。object
。System.Type
。TimeSpan
不属于这些列表中的任何一个,因此不能用作常量。TimeSpan
可以适用于此列表中的最后一个,default(TimeSpan)
是有效的。 - CodesInChaosTimeSpan(<literal-number>)
都应该被支持为 const
。 - pooya13TimeSpan(<literal-number>)
都应该被支持为 const
。 - undefinedvoid 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
,因此这样做应该会更好。
TimeSpan
是 DefaultValueAttribute
的一个特殊情况,可以使用任何可以通过 TimeSpan.Parse
方法解析的字符串来指定。
[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
我的建议:
void A( long spanInMs = 2000 )
{
var ts = TimeSpan.FromMilliseconds(spanInMs);
//...
}
顺便提一下,TimeSpan.FromSeconds(2.0)
并不等于 new TimeSpan(2000)
- 因为构造函数接受的是时钟周期。
其他答案已经给出了为什么可选参数不能是动态表达式的很好的解释。但是,简单概括一下,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#内置的默认方法参数被视为编译时常量并嵌入到调用站点中,因此只有在重新编译代码后,才会使用默认参数。通过实例化设置对象,调用者在调用您的方法时动态加载默认值。这意味着您可以通过仅更改设置类来更新默认值。因此,如果需要,此模式使您可以更改默认值而无需重新编译调用者以查看新值。
void Foo(TimeSpan span = new TimeSpan(2000))
void Foo(int ticks = 2000) => Foo(new TimeSpan(ticks));
void Foo(TimeSpan span) => [implementation]
Foo()
的调用变得更清晰,可以按照你的意愿进行操作。整数重载在一般情况下也是可以的。如果需要的话,你可以将整数重载改为以秒或其他单位为基准。为了指定结构类型参数的默认值,我建议使用重载:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan time)
{
// ...
}
new TimeSpan(2000)
并不意味着 2000 毫秒,而是表示 2000 "ticks",即 0.2 毫秒,或两秒钟的一万分之一。 - Jeppe Stig Nielsen