C # 4.0: ¿Puedo usar un TimeSpan como parámetro opcional con un valor predeterminado?

125

Ambos generan un error que dice que deben ser una constante en tiempo de compilación:

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

En primer lugar, ¿alguien puede explicar por qué estos valores no se pueden determinar en tiempo de compilación? ¿Y hay alguna manera de especificar un valor predeterminado para un objeto TimeSpan opcional?

Mike Pateras
fuente
11
No está relacionado con lo que pregunta, pero tenga en cuenta que new TimeSpan(2000)no significa 2000 milisegundos, significa 2000 "ticks", que son 0.2 milisegundos, o una 10,000-th de dos segundos.
Jeppe Stig Nielsen

Respuestas:

173

Puede solucionar este problema muy fácilmente cambiando su firma.

void Foo(TimeSpan? span = null) {

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

   ...

}

Debo explicar: la razón por la cual esas expresiones en su ejemplo no son constantes de tiempo de compilación es porque en el momento de la compilación, el compilador no puede simplemente ejecutar TimeSpan.FromSeconds (2.0) y pegar los bytes del resultado en su código compilado.

Como ejemplo, considere si intentó usar DateTime.Now en su lugar. El valor de DateTime.Now cambia cada vez que se ejecuta. O suponga que TimeSpan.FromSeconds tuvo en cuenta la gravedad. Es un ejemplo absurdo, pero las reglas de las constantes de tiempo de compilación no hacen casos especiales solo porque sabemos que TimeSpan.FromSeconds es determinista.

Josh
fuente
15
Ahora documente el valor predeterminado en <param>, porque no está visible en la firma.
Coronel Panic
3
No puedo hacer esto, estoy usando el valor especial nulo para otra cosa.
Coronel Panic
44
@MattHickford: luego deberá proporcionar un método sobrecargado o tomar milisegundos como parámetro.
Josh
19
También se puede usar span = span ?? TimeSpan.FromSeconds(2.0);con el tipo anulable, en el cuerpo del método. O var realSpan = span ?? TimeSpan.FromSeconds(2.0);para obtener una variable local que no sea anulable.
Jeppe Stig Nielsen
55
Lo que no me gusta de esto es que implica para el usuario de la función que esta función "funciona" con un intervalo nulo. Pero eso no es cierto! Nulo no es un valor válido para span en lo que respecta a la lógica real de la función. Desearía que hubiera una mejor manera que no pareciera un olor a código ...
JoeCool
31

Mi herencia VB6 me inquieta con la idea de considerar que el "valor nulo" y el "valor perdido" son equivalentes. En la mayoría de los casos, probablemente esté bien, pero puede tener un efecto secundario no deseado, o puede tragarse una condición excepcional (por ejemplo, si la fuente spanes una propiedad o variable que no debería ser nula, pero lo es).

Por lo tanto, sobrecargaría el método:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
fuente
1
+1 por esa gran técnica. Los parámetros predeterminados solo deben usarse con tipos const, realmente. De lo contrario, no es confiable.
Lazlo
2
Este es el enfoque `` tradicional '' que reemplazó los valores predeterminados, y para esta situación, creo que esta es la respuesta menos fea;) Por sí solo, no necesariamente funciona tan bien para las interfaces, porque realmente desea el valor predeterminado en un lugar. En este caso, he encontrado que los métodos de extensión son una herramienta útil: la interfaz tiene un método con todos los parámetros, luego una serie de métodos de extensión declarados en una clase estática junto con la interfaz implementan los valores predeterminados en varias sobrecargas.
OlduwanSteve
23

Esto funciona bien:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
fuente
44
Bienvenido a Stack Overflow. Parece que su respuesta es que puede proporcionar un valor de parámetro predeterminado, siempre que sea el único valor muy específico que permite el compilador. ¿Lo he entendido bien? (Puede editar su respuesta para aclararla). Esta sería una mejor respuesta si mostrara cómo aprovechar lo que el compilador permite llegar a lo que la pregunta buscaba originalmente, que era tener otros TimeSpan valores arbitrarios , como el dado por new TimeSpan(2000).
Rob Kennedy
2
Una alternativa que usa algún valor predeterminado específico sería usar un tiempo de lectura estático privado solo TimeSpan defaultTimespan = Timespan.FromSeconds (2) combinado con el constructor predeterminado y el constructor tomando un intervalo de tiempo. public Foo (): this (defaultTimespan) y public Foo (Timespan ts)
johan mårtensson
15

El conjunto de valores que se pueden usar como valor predeterminado son los mismos que se pueden usar para un argumento de atributo. La razón es que los valores predeterminados están codificados en metadatos dentro de DefaultParameterValueAttribute.

En cuanto a por qué no se puede determinar en tiempo de compilación. El conjunto de valores y expresiones sobre dichos valores permitidos en tiempo de compilación se enumera en la especificación oficial del lenguaje C # :

C # 6.0 - Tipos de parámetros de atributos :

Los tipos de parámetros posicionales y con nombre para una clase de atributo están limitados a los tipos de parámetros de atributo , que son:

  • Uno de los siguientes tipos: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • El tipo object.
  • El tipo System.Type.
  • Un tipo de enumeración.
    (siempre que tenga accesibilidad pública y los tipos en los que está anidado (si corresponde) también tienen accesibilidad pública)
  • Matrices unidimensionales de los tipos anteriores.

El tipo TimeSpanno cabe en ninguna de estas listas y, por lo tanto, no se puede usar como una constante.

JaredPar
fuente
2
Ligero error: llamar a un método estático no cabe en ninguna de la lista. TimeSpanpuede caber el último en esta lista default(TimeSpan)es válido.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

proporcionado default(TimeSpan)no es un valor válido para la función.

O

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

proporcionado new TimeSpan()no es un valor válido.

O

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

Esto debería ser mejor teniendo en cuenta que las posibilidades de que el nullvalor sea un valor válido para la función son poco frecuentes.

nawfal
fuente
4

TimeSpanes un caso especial para DefaultValueAttributey se especifica usando cualquier cadena que se pueda analizar a través del TimeSpan.Parsemétodo.

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

Mi sugerencia:

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

    //...
}

Por cierto, TimeSpan.FromSeconds(2.0)no es igual new TimeSpan(2000): el constructor toma las garrapatas.

tymtam
fuente
2

Otras respuestas han dado excelentes explicaciones de por qué un parámetro opcional no puede ser una expresión dinámica. Pero, para contar, los parámetros predeterminados se comportan como constantes de tiempo de compilación. Eso significa que el compilador debe poder evaluarlos y encontrar una respuesta. Hay algunas personas que desean que C # agregue soporte para el compilador que evalúa expresiones dinámicas cuando se encuentra con declaraciones constantes; este tipo de característica estaría relacionada con los métodos de marcado "puros", pero eso no es una realidad en este momento y puede que nunca lo sea.

Una alternativa al uso de un parámetro predeterminado de C # para tal método sería usar el patrón ejemplificado por XmlReaderSettings. En este patrón, defina una clase con un constructor sin parámetros y propiedades de escritura pública. Luego, reemplace todas las opciones por defecto en su método con un objeto de este tipo. Incluso haga que este objeto sea opcional especificando un valor predeterminado nullpara él. Por ejemplo:

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 llamar, use esa sintaxis extraña para crear instancias y asignar propiedades en una sola expresión:

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

Desventajas

Este es un enfoque realmente pesado para resolver este problema. Si está escribiendo una interfaz interna rápida y sucia y hace que la TimeSpananulación y el tratamiento nulo como su valor predeterminado deseado funcionen bien, hágalo en su lugar.

Además, si tiene una gran cantidad de parámetros o está llamando al método en un ciclo cerrado, esto tendrá la sobrecarga de las instancias de clase. Por supuesto, si se llama a un método de este tipo en un ciclo cerrado, puede ser natural e incluso muy fácil reutilizar una instancia del FooSettingsobjeto.

Beneficios

Como mencioné en el comentario del ejemplo, creo que este patrón es excelente para las API públicas. Agregar nuevas propiedades a una clase es un cambio ABI ininterrumpido, por lo que puede agregar nuevos parámetros opcionales sin cambiar la firma de su método utilizando este patrón, lo que brinda más opciones al código compilado más recientemente mientras continúa admitiendo el código compilado anterior sin trabajo adicional .

Además, debido a que los parámetros de método predeterminados integrados de C # se tratan como constantes de tiempo de compilación y se incorporan al sitio de llamadas, los parámetros predeterminados solo serán utilizados por el código una vez que se vuelva a compilar. Al crear una instancia de un objeto de configuración, la persona que llama carga dinámicamente los valores predeterminados cuando llama a su método. Esto significa que puede actualizar los valores predeterminados simplemente cambiando su clase de configuración. Por lo tanto, este patrón le permite cambiar los valores predeterminados sin tener que volver a compilar las personas que llaman para ver los nuevos valores, si así lo desea.

binki
fuente