C # 4.0: Posso usar um TimeSpan como um parâmetro opcional com um valor padrão?

125

Ambos geram um erro dizendo que devem ser uma constante em tempo de compilação:

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

Antes de tudo, alguém pode explicar por que esses valores não podem ser determinados em tempo de compilação? E existe uma maneira de especificar um valor padrão para um objeto TimeSpan opcional?

Mike Pateras
fonte
11
Não relacionado ao que você pergunta, mas lembre-se de que new TimeSpan(2000)isso não significa 2000 milissegundos, significa 2000 "ticks", que são 0,2 milissegundos ou um 10.000ésimo de dois segundos.
Jeppe Stig Nielsen

Respostas:

173

Você pode contornar isso com muita facilidade alterando sua assinatura.

void Foo(TimeSpan? span = null) {

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

   ...

}

Devo elaborar - o motivo pelo qual essas expressões no seu exemplo não são constantes em tempo de compilação é porque, em tempo de compilação, o compilador não pode simplesmente executar TimeSpan.FromSeconds (2.0) e colar os bytes do resultado em seu código compilado.

Como exemplo, considere se você tentou usar DateTime.Now. O valor de DateTime.Now muda toda vez que é executado. Ou suponha que TimeSpan.FromSeconds levasse em conta a gravidade. É um exemplo absurdo, mas as regras das constantes em tempo de compilação não produzem casos especiais apenas porque sabemos que o TimeSpan.FromSeconds é determinístico.

Josh
fonte
15
Agora documente o valor padrão <param>, porque não está visível na assinatura.
Coronel Panic
3
Não posso fazer isso, estou usando o valor especial null para outra coisa.
Coronel Panic
4
@ MattHickford - Então você terá que fornecer um método sobrecarregado ou levar milissegundos como parâmetro.
Josh
19
Também pode usar span = span ?? TimeSpan.FromSeconds(2.0);com o tipo anulável, no corpo do método. Ou var realSpan = span ?? TimeSpan.FromSeconds(2.0);para obter uma variável local que não seja anulável.
Jeppe Stig Nielsen
5
O que eu não gosto sobre isso é que isso implica para o usuário da função que essa função "funcione" com uma extensão nula. Mas isso não é verdade! Nulo não é um valor válido para span no que diz respeito à lógica real da função. Eu gostaria que houvesse uma maneira melhor que não parece ser um cheiro de código ...
JoeCool
31

Minha herança VB6 me deixa desconfortável com a idéia de considerar "valor nulo" e "valor ausente" como equivalente. Na maioria dos casos, provavelmente é bom, mas você pode ter um efeito colateral não intencional ou pode engolir uma condição excepcional (por exemplo, se a origem de spanfor uma propriedade ou variável que não deve ser nula, mas é).

Portanto, sobrecarregaria o método:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
fonte
1
+1 para essa ótima técnica. Parâmetros padrão só devem ser usados ​​com tipos const, na verdade. Senão, não é confiável.
Lazlo
2
Essa é a abordagem 'honrada' que os valores padrão substituíram e, para essa situação, acho que é a resposta menos feia;) Por si só, não necessariamente funciona tão bem para interfaces, porque você realmente deseja o valor padrão em um lugar. Nesse caso, achei os métodos de extensão uma ferramenta útil: a interface possui um método com todos os parâmetros e, em seguida, uma série de métodos de extensão declarados em uma classe estática ao lado da interface implementa os padrões em várias sobrecargas.
OlduwanSteve
23

Isso funciona bem:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
fonte
4
Bem-vindo ao Stack Overflow. Sua resposta parece ser que você pode fornecer um valor de parâmetro padrão, desde que esse seja um valor muito específico que o compilador permita. Eu entendi isso certo? (Você pode editar sua resposta para esclarecer.) Essa seria uma resposta melhor se mostrasse como tirar proveito do que o compilador permite obter para o que a pergunta originalmente buscava, que deveria ter outros TimeSpan valores arbitrários , como o fornecido por new TimeSpan(2000).
Rob Kennedy
2
Uma alternativa que usa algum valor padrão específico seria usar um estático privado somente leitura TimeSpan defaultTimespan = Timespan.FromSeconds (2) combinado com o construtor padrão e o construtor tomando um intervalo de tempo. pública Foo (): esta (defaultTimespan) e Foo pública (ts timeSpan)
Mårtensson johan
15

O conjunto de valores que pode ser usado como valor padrão é o mesmo que pode ser usado para um argumento de atributo. A razão é que os valores padrão são codificados em metadados dentro do DefaultParameterValueAttribute.

Por que não pode ser determinado em tempo de compilação. O conjunto de valores e expressões sobre esses valores permitidos no tempo de compilação está listado nas especificações oficiais da linguagem C # :

C # 6.0 - Tipos de parâmetros de atributo :

Os tipos de parâmetros posicionais e nomeados para uma classe de atributo são limitados aos tipos de parâmetros de atributo , que são:

  • Um dos seguintes tipos: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • O tipo object.
  • O tipo System.Type.
  • Um tipo de enumeração.
    (desde que tenha acessibilidade pública e os tipos em que está aninhado (se houver) também tenham acessibilidade pública)
  • Matrizes unidimensionais dos tipos acima.

O tipo TimeSpannão se encaixa em nenhuma dessas listas e, portanto, não pode ser usado como uma constante.

JaredPar
fonte
2
Ligeira nit-pick: chamar um método estático não se encaixa em nenhuma das listas. TimeSpanpode caber o último nesta lista default(TimeSpan)é válido.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

fornecido default(TimeSpan)não é um valor válido para a função.

Ou

//this works only for value types which TimeSpan is
void Foo(TimeSpan span = new TimeSpan())
{
    if (span == new TimeSpan()) 
        span = TimeSpan.FromSeconds(2); 
}

fornecido new TimeSpan()não é um valor válido.

Ou

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

Isso deve ser melhor, considerando que as chances de o nullvalor ser válido para a função são raras.

nawfal
fonte
4

TimeSpané um caso especial para o DefaultValueAttributee é especificado usando qualquer sequência que possa ser analisada por meio do TimeSpan.Parsemétodo

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

Minha sugestão:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0)não é igual new TimeSpan(2000)- o construtor leva carrapatos.

timtim
fonte
2

Outras respostas deram grandes explicações sobre por que um parâmetro opcional não pode ser uma expressão dinâmica. Mas, para recontar, os parâmetros padrão se comportam como constantes de tempo de compilação. Isso significa que o compilador deve ser capaz de avaliá-los e apresentar uma resposta. Existem pessoas que desejam que o C # adicione suporte ao compilador que avalia expressões dinâmicas ao encontrar declarações constantes - esse tipo de recurso estaria relacionado aos métodos de marcação como "puros", mas isso não é uma realidade agora e talvez nunca seja.

Uma alternativa para usar um parâmetro padrão C # para esse método seria usar o padrão exemplificado por XmlReaderSettings. Nesse padrão, defina uma classe com um construtor sem parâmetros e propriedades publicamente graváveis. Em seguida, substitua todas as opções pelos padrões do seu método por um objeto desse tipo. Mesmo torne esse objeto opcional, especificando um padrão nullpara ele. Por exemplo:

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);
    }
}

Para chamar, use essa sintaxe estranha para instanciar e atribuir propriedades em uma única expressão:

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

Desvantagens

Essa é uma abordagem realmente pesada para resolver esse problema. Se você estiver escrevendo uma interface interna rápida e suja e tornando o valor TimeSpannulo e o tratamento nulo como o valor padrão desejado funcionaria bem, faça isso.

Além disso, se você tiver um grande número de parâmetros ou estiver chamando o método em um loop restrito, isso terá a sobrecarga das instanciações de classe. Obviamente, se chamar esse método em um loop apertado, pode ser natural e até muito fácil reutilizar uma instância do FooSettingsobjeto.

Benefícios

Como mencionei no comentário no exemplo, acho que esse padrão é ótimo para APIs públicas. Adicionar novas propriedades a uma classe é uma alteração ABI ininterrupta, portanto, você pode adicionar novos parâmetros opcionais sem alterar a assinatura do seu método usando esse padrão - fornecendo mais opções ao código compilado mais recentemente, continuando a suportar o código compilado antigo sem trabalho extra .

Além disso, como os parâmetros de método padrão integrados do C # são tratados como constantes de compilação e inseridos no site de chamada, os parâmetros padrão serão usados ​​apenas pelo código quando forem recompilados. Ao instanciar um objeto de configurações, o chamador carrega dinamicamente os valores padrão ao chamar seu método. Isso significa que você pode atualizar os padrões apenas alterando sua classe de configurações. Portanto, esse padrão permite alterar os valores padrão sem a necessidade de recompilar os chamadores para ver os novos valores, se desejado.

binki
fonte