C# 4.0: Могу ли я использовать TimeSpan в качестве необязательного параметра со значением по умолчанию?
Оба они генерируют ошибку, говоря, что они должны быть константой времени компиляции:
void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new TimeSpan(2000))
Прежде всего, кто-то может объяснить, почему эти значения не могут быть определены во время компиляции? И есть ли способ указать значение по умолчанию для необязательного объекта TimeSpan?
9 ответов
Вы можете обойти это очень легко, изменив свою подпись.
void Foo(TimeSpan? span = null) {
if (span == null) { span = TimeSpan.FromSeconds(2); }
...
}
Я должен уточнить - причина того, что эти выражения в вашем примере не являются константами времени компиляции, заключается в том, что во время компиляции компилятор не может просто выполнить TimeSpan.FromSeconds(2.0) и вставить байты результата в ваш скомпилированный код.
В качестве примера рассмотрим, пытались ли вы использовать DateTime.Now вместо этого. Значение DateTime.Now изменяется каждый раз, когда оно выполняется. Или предположим, что TimeSpan.FromSeconds учитывает гравитацию. Это абсурдный пример, но правила констант времени компиляции не делают особых случаев только потому, что мы знаем, что TimeSpan.FromSeconds является детерминированным.
Мое наследие VB6 заставляет меня чувствовать себя неловко из-за того, что "нулевая ценность" и "недостающая ценность" считаются эквивалентными В большинстве случаев это, вероятно, хорошо, но у вас может быть непреднамеренный побочный эффект, или вы можете проглотить исключительное состояние (например, если источник span
это свойство или переменная, которая не должна быть нулевой, но есть).
Поэтому я бы перегружал метод:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
//...
}
Это отлично работает:
void Foo(TimeSpan span = default(TimeSpan))
Набор значений, которые можно использовать в качестве значения по умолчанию, такой же, как и для аргумента атрибута. Причина в том, что значения по умолчанию кодируются в метаданные внутри DefaultParameterValueAttribute
,
Что касается того, почему это не может быть определено во время компиляции. Набор значений и выражений для таких значений, разрешенных во время компиляции, указан в официальной спецификации языка C#:
C# 6.0 - Типы параметров атрибута:
Типы позиционных и именованных параметров для класса атрибута ограничены типами параметров атрибута:
- Один из следующих типов:
bool
,byte
,char
,double
,float
,int
,long
,sbyte
,short
,string
,uint
,ulong
,ushort
,- Тип
object
,- Тип
System.Type
,- Тип перечисления.
(при условии, что он имеет общедоступный доступ и типы, в которые он вложен (если есть), также имеют общедоступный доступ)- Одномерные массивы вышеуказанных типов.
Тип TimeSpan
не вписывается ни в один из этих списков и, следовательно, не может использоваться как константа.
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
значение, являющееся допустимым значением для функции, встречается редко.
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# добавлял поддержку компилятора, оценивающего динамические выражения, при обнаружении константных объявлений - такая функция была бы связана с маркировкой методов "pure", но сейчас это не является реальностью и никогда не может быть.
Одной альтернативой использованию параметра 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
Downsides
Это действительно тяжеловесный подход к решению этой проблемы. Если вы пишете быстрый и грязный внутренний интерфейс и делаетеTimeSpan
nullable и рассматривает null как желаемое значение по умолчанию, будет работать нормально, вместо этого
Кроме того, если у вас есть большое количество параметров или вы вызываете метод в узком цикле, это приведет к дополнительным затратам на создание экземпляров классов. Конечно, если вызов такого метода в узком цикле, это может быть естественным и даже очень легко повторно использовать экземпляр FooSettings
объект.
Выгоды
Как я уже упоминал в комментарии к примеру, я думаю, что этот шаблон отлично подходит для публичных API. Добавление новых свойств в класс - это непрерывное изменение ABI, поэтому вы можете добавлять новые необязательные параметры без изменения сигнатуры вашего метода, используя этот шаблон - предоставляя более недавно скомпилированному коду больше опций, продолжая при этом поддерживать старый скомпилированный код без дополнительной работы.,
Кроме того, поскольку встроенные в C# методы метода по умолчанию обрабатываются как константы времени компиляции и запекаются в месте вызова, параметры по умолчанию будут использоваться кодом только после его перекомпиляции. Создавая объект настроек, вызывающая сторона динамически загружает значения по умолчанию при вызове вашего метода. Это означает, что вы можете обновить настройки по умолчанию, просто изменив свой класс настроек. Таким образом, этот шаблон позволяет вам изменять значения по умолчанию без необходимости перекомпиляции вызывающих, чтобы увидеть новые значения, если это необходимо.
Чтобы указать значение по умолчанию для параметров типа структуры, я бы предложил использовать перегрузку:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan time)
{
// ...
}