С# 4.0: Можно ли использовать TimeSpan в качестве необязательного параметра со значением по умолчанию?

Оба из них генерируют ошибку, говоря, что они должны быть константой времени компиляции:

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

Прежде всего, может кто-нибудь объяснить, почему эти значения не могут быть определены во время компиляции? И есть ли способ указать значение по умолчанию для необязательного объекта TimeSpan?

Ответ 1

Вы можете легко обойти это, изменив свою подпись.

void Foo(TimeSpan? span = null) {

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

   ...

}

Я должен уточнить - причина, по которой эти выражения в вашем примере не являются константами времени компиляции, заключается в том, что во время компиляции компилятор не может просто выполнить TimeSpan.FromSeconds(2.0) и привязать байты результата к скомпилированному коду.

В качестве примера рассмотрим, пытались ли вы использовать DateTime.Now. Значение DateTime.Now изменяется каждый раз, когда оно выполняется. Или предположим, что TimeSpan.FromSeconds учитывает гравитацию. Это абсурдный пример, но правила констант времени компиляции не делают особых случаев только потому, что мы знаем, что TimeSpan.FromSeconds детерминирован.

Ответ 2

Мое наследие VB6 заставляет меня беспокоиться о том, чтобы считать, что значение "null value" и "missing value" эквивалентно. В большинстве случаев это, вероятно, хорошо, но у вас может быть непреднамеренный побочный эффект, или вы можете усвоить исключительное условие (например, если источником span является свойство или переменная, которая не должна быть нулевой, но есть).

Поэтому я бы перегрузил метод:

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

Ответ 3

Это отлично работает:

void Foo(TimeSpan span = default(TimeSpan))

Ответ 4

Набор значений, которые могут использоваться как значение по умолчанию, такие же, как и для аргумента атрибута. Причина в том, что значения по умолчанию закодированы в метаданные внутри DefaultParameterValueAttribute.

В чем причина этого не может быть определена во время компиляции. Набор значений и выражений над такими значениями, разрешенными во время компиляции, указан в разделе 7.18 спецификации С# lang. В терминах допустимых значений оно ограничено

  • Литералы, включая null
  • Ссылки на другие значения const
  • Значения перечисления
  • Выражение по умолчанию

Тип TimeSpan не вписывается ни в один из этих списков и, следовательно, не может использоваться как константа.

Ответ 5

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 является допустимым значением для функции, встречается редко.

Ответ 6

TimeSpan является специальным случаем для DefaultValueAttribute и указывается с использованием любой строки, которая может быть проанализирована с помощью метода TimeSpan.Parse.

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

Ответ 7

Другие ответы дали большие объяснения относительно того, почему необязательный параметр не может быть динамическим выражением. Но, чтобы пересчитать, параметры по умолчанию ведут себя как константы времени компиляции. Это означает, что компилятор должен иметь возможность оценить их и придумать ответ. Есть некоторые люди, которые хотят, чтобы С# добавляла поддержку компилятору, оценивая динамические выражения, когда сталкивалась с объявлениями констант - такая функция была бы связана с методами маркировки "чистая", но это не реальность прямо сейчас и, возможно, никогда не будет.

Альтернативой использованию параметра С# по умолчанию для такого метода будет использование шаблона, приведенного в качестве 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 и обрабатывает нуль, как ваше желаемое значение по умолчанию, сделайте это вместо этого.

Кроме того, если у вас есть большое количество параметров или вы вызываете метод в узком цикле, это будет иметь накладные расходы на экземпляры классов. Конечно, если вы вызываете такой метод в замкнутом цикле, может быть естественным и даже очень простым повторное использование экземпляра объекта FooSettings.

Преимущества

Как я упоминал в комментарии в примере, я думаю, что этот шаблон отлично подходит для публичных API. Добавление новых свойств в класс является изменением ABI без изменений, поэтому вы можете добавлять новые необязательные параметры без изменения подписи вашего метода с помощью этого шаблона, предоставляя более недавно скомпилированный код дополнительных параметров, продолжая поддерживать старый скомпилированный код без дополнительной работы.

Кроме того, поскольку встроенные в С# параметры параметров по умолчанию рассматриваются как константы компиляции и выпекаются в callsite, параметры по умолчанию будут использоваться кодом только после его перекомпиляции. Создавая экземпляр объекта настроек, вызывающий динамически загружает значения по умолчанию при вызове вашего метода. Это означает, что вы можете обновлять значения по умолчанию, просто изменив класс настроек. Таким образом, этот шаблон позволяет изменять значения по умолчанию без необходимости перекомпилировать вызывающих абонентов для просмотра новых значений, если это необходимо.

Ответ 8

Мое предложение:

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

            //...
        }

BTW TimeSpan.FromSeconds(2.0) не равен new TimeSpan(2000) - конструктор принимает тики.