Both of these generate an error saying they must be a compile-time constant:
void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new
Other answers have given great explanations as to why an optional parameter cannot be a dynamic expression. But, to recount, default parameters behave like compile time constants. That means the compiler has to be able to evaluate them and come up with an answer. There are some people who want C# to add support for the compiler evaluating dynamic expressions when encountering constant declarations—this sort of feature would be related to marking methods “pure”, but that isn’t a reality right now and might never be.
One alternative to using a C# default parameter for such a method would be to use the pattern exemplified by XmlReaderSettings. In this pattern, define a class with a parameterless constructor and publicly writable properties. Then replace all options with defaults in your method with an object of this type. Even make this object optional by specifying a default of null
for it. For example:
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);
}
}
To call, use that one weird syntax for instantiating and assigning properties all in one expression:
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
This is a really heavyweight approach to solving this problem. If you are writing a quick and dirty internal interface and making the TimeSpan nullable and treating null like your desired default value would work fine, do that instead.
Also, if you have a large number of parameters or are calling the method in a tight loop, this will have the overhead of class instantiations. Of course, if calling such a method in a tight loop, it might be natural and even very easy to reuse an instance of the FooSettings
object.
As I mentioned in the comment in the example, I think this pattern is great for public APIs. Adding new properties to a class is a non-breaking ABI change, so you can add new optional parameters without changing the signature of your method using this pattern—giving more recently compiled code more options while continuing to support old compiled code with no extra work.
Also, because C#’s built in default method parameters are treated as compiletime constants and baked into the callsite, default parameters will only be used by code once it is recompiled. By instantiating a settings object, the caller dynamically loads the default values when calling your method. This means that you can update defaults by just changing your settings class. Thus, this pattern lets you change default values without having to recompile callers to see the new values, if that is desired.
My suggestion:
void A( long spanInMs = 2000 )
{
var ts = TimeSpan.FromMilliseconds(spanInMs);
//...
}
BTW
TimeSpan.FromSeconds(2.0)
does not equal new TimeSpan(2000)
- the constructor takes ticks.
The set of values which can be used as a default value are the same as can be used for an attribute argument. The reason being that default values are encoded into metadata inside of the DefaultParameterValueAttribute
.
As to why it can't be determined at compile time. The set of values and expressions over such values allowed at compile time is listed in official C# language spec:
C# 6.0 - Attribute parameter types:
The types of positional and named parameters for an attribute class are limited to the attribute parameter types, which are:
- One of the following types:
bool
,byte
,char
,double
,float
,int
,long
,sbyte
,short
,string
,uint
,ulong
,ushort
.- The type
object
.- The type
System.Type
.- An enum type.
(provided it has public accessibility and the types in which it is nested (if any) also have public accessibility)- Single-dimensional arrays of the above types.
The type TimeSpan
does not fit into any of these lists and hence cannot be used as a constant.
TimeSpan
is a special case for the DefaultValueAttribute
and is specified using any string that can be parsed via the TimeSpan.Parse
method.
[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
void Foo(TimeSpan span = default(TimeSpan))
{
if (span == default(TimeSpan))
span = TimeSpan.FromSeconds(2);
}
provided default(TimeSpan)
is not a valid value for the function.
Or
//this works only for value types which TimeSpan is
void Foo(TimeSpan span = new TimeSpan())
{
if (span == new TimeSpan())
span = TimeSpan.FromSeconds(2);
}
provided new TimeSpan()
is not a valid value.
Or
void Foo(TimeSpan? span = null)
{
if (span == null)
span = TimeSpan.FromSeconds(2);
}
This should be better considering chances of null
value being a valid value for the function are rare.
You can work around this very easily by changing your signature.
void Foo(TimeSpan? span = null) {
if (span == null) { span = TimeSpan.FromSeconds(2); }
...
}
I should elaborate - the reason those expressions in your example are not compile-time constants is because at compile time, the compiler can't simply execute TimeSpan.FromSeconds(2.0) and stick the bytes of the result into your compiled code.
As an example, consider if you tried to use DateTime.Now instead. The value of DateTime.Now changes every time it's executed. Or suppose that TimeSpan.FromSeconds took into account gravity. It's an absurd example but the rules of compile-time constants don't make special cases just because we happen to know that TimeSpan.FromSeconds is deterministic.