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

125

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

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

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

Майк Патерас
источник
11
Не имеет отношения к тому, о чем вы спрашиваете, но имейте в new TimeSpan(2000)виду, что это не означает 2000 миллисекунд, это означает 2000 «тактов», что составляет 0,2 миллисекунды или одну 10 000-ю из двух секунд.
Jeppe Stig Nielsen

Ответы:

173

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

void Foo(TimeSpan? span = null) {

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

   ...

}

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

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

мистифицировать
источник
15
Теперь задокументируйте значение по умолчанию <param>, потому что оно не отображается в подписи.
Colonel Panic
3
Я не могу этого сделать, я использую специальное значение null для чего-то другого.
Colonel Panic
4
@MattHickford - тогда вам нужно будет предоставить перегруженный метод или использовать миллисекунды в качестве параметра.
Джош
19
Также можно использовать span = span ?? TimeSpan.FromSeconds(2.0);с типом, допускающим значение NULL, в теле метода. Или var realSpan = span ?? TimeSpan.FromSeconds(2.0);получить локальную переменную, не допускающую значения NULL.
Jeppe Stig Nielsen
5
Что мне не нравится в этом, так это то, что это подразумевает для пользователя функции, что эта функция «работает» с нулевым диапазоном. Но это неправда! Null не является допустимым значением для диапазона с точки зрения фактической логики функции. Я бы хотел, чтобы был способ получше, который не казался бы запахом кода ...
JoeCool
31

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

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

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
источник
1
+1 за эту отличную технику. На самом деле параметры по умолчанию следует использовать только с константными типами. В противном случае это ненадежно.
Lazlo
2
Это `` проверенный временем '' подход, в котором значения по умолчанию заменены, и для этой ситуации я думаю, что это наименее уродливый ответ;) Сам по себе он не обязательно работает так хорошо для интерфейсов, потому что вам действительно нужно значение по умолчанию в одно место. В этом случае я обнаружил, что методы расширения являются полезным инструментом: интерфейс имеет один метод со всеми параметрами, затем ряд методов расширения, объявленных в статическом классе вместе с интерфейсом, реализуют значения по умолчанию в различных перегрузках.
OlduwanSteve
23

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

void Foo(TimeSpan span = default(TimeSpan))

Елена Лавриненко
источник
4
Добро пожаловать в Stack Overflow. Кажется, ваш ответ заключается в том, что вы можете указать значение параметра по умолчанию, если это одно очень конкретное значение, которое позволяет компилятор. Я правильно понял? (Вы можете отредактировать свой ответ, чтобы уточнить.) Это был бы лучший ответ, если бы он показал, как воспользоваться преимуществами того, что компилятор позволяет получить то, что изначально искал вопрос, который должен был иметь произвольные другие TimeSpan значения, например, заданные new TimeSpan(2000),
Роб Кеннеди
2
Альтернативой, использующей какое-то конкретное значение по умолчанию, было бы использование частного статического TimeSpan defaultTimespan = Timespan.FromSeconds (2) только для чтения в сочетании с конструктором по умолчанию и конструктором, занимающим определенный промежуток времени. public Foo (): this (defaultTimespan) и public Foo (Timespan ts)
Йохан Мартенссон
15

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

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

C # 6.0 - Типы параметров атрибутов :

Типы позиционных и именованных параметров для класса атрибута ограничены типами параметров атрибута , а именно:

  • Один из следующих типов: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Тип object.
  • Тип System.Type.
  • Тип перечисления.
    (при условии, что он общедоступен, а типы, в которые он вложен (если есть), также имеют публичный доступ)
  • Одномерные массивы вышеперечисленных типов.

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

JaredPar
источник
2
Небольшая придирка: вызов статического метода не входит ни в один список. TimeSpanможет поместиться последний в этом списке default(TimeSpan).
CodesInChaos 08
12
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значение является допустимым значением для функции, редка.

Навфал
источник
4

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

[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
dahall
источник
3

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

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

    //...
}

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

tymtam
источник
2

Другие ответы дали отличные объяснения того, почему необязательный параметр не может быть динамическим выражением. Но, повторюсь, параметры по умолчанию ведут себя как константы времени компиляции. Это означает, что компилятор должен уметь их оценивать и давать ответ. Есть люди, которые хотят, чтобы в 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

Downsides

Это действительно тяжелый подход к решению этой проблемы. Если вы пишете быстрый и грязный внутренний интерфейс и делаете TimeSpannullable и обрабатываете null, как желаемое значение по умолчанию, все будет работать нормально, сделайте это вместо этого.

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

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

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

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

Бинки
источник