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

Ambos generan un error que dice que deben ser una constante en tiempo de comstackció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 el momento de la comstackción? ¿Y hay una manera de especificar un valor predeterminado para un objeto TimeSpan opcional?

Puede solucionar esto muy fácilmente cambiando su firma.

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

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

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

Mi herencia de VB6 me inquieta con la idea de considerar el “valor nulo” y el “valor perdido” como 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 el origen del span es una propiedad o variable que no debería ser nula, pero sí lo es).

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

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

Esto funciona bien:

void Foo(TimeSpan span = default(TimeSpan))

El conjunto de valores que se pueden usar como valor predeterminado es el mismo que se puede 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 comstackción. El conjunto de valores y expresiones sobre dichos valores permitidos en tiempo de comstackción se detalla en la especificación oficial del lenguaje C # :

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

Los tipos de parámetros posicionales y nominales 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 de object
  • El tipo System.Type .
  • Un tipo enum.
    (siempre que tenga acceso público y los tipos en los que está nested (si lo hay) también tienen acceso público)
  • Arrays unidimensionales de los tipos anteriores.

El tipo TimeSpan no cabe en ninguna de estas listas y, por lo tanto, no puede usarse como una constante.

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

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

siempre que el new TimeSpan() no sea 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 las posibilidades de que el valor null sea ​​un valor válido para la función.

TimeSpan es un caso especial para DefaultValueAttribute y se especifica utilizando cualquier cadena que se pueda analizar a través del método TimeSpan.Parse .

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

Otras respuestas han dado grandes explicaciones sobre por qué un parámetro opcional no puede ser una expresión dinámica. Pero, para volver a contar, los parámetros predeterminados se comportan como constantes de tiempo de comstackción. Eso significa que el comstackdor debe ser capaz de evaluarlos y encontrar una respuesta. Hay algunas personas que desean que C # agregue compatibilidad para que el comstackdor evalúe expresiones dinámicas al encontrar declaraciones constantes; este tipo de característica se relacionaría con los métodos de marca “puro”, pero eso no es una realidad en este momento y es posible que nunca lo sea.

Una alternativa al uso de un parámetro predeterminado de C # para dicho método sería utilizar 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 con valores predeterminados en su método con un objeto de este tipo. Incluso haga que este objeto sea opcional especificando un valor predeterminado de null para é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). Lazy thing = new Lazy(() => new FatThing()); public IThing Thing { get { return thing.Value; } set { thing = new Lazy(() => 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 syntax 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 el TimeSpan anular y trate nulo como su valor predeterminado deseado funcionaría bien, hágalo en su lugar.

Además, si tiene una gran cantidad de parámetros o llama al método en un bucle cerrado, tendrá la sobrecarga de las instancias de clase. Por supuesto, si se llama a ese método en un círculo cerrado, podría ser natural e incluso muy fácil reutilizar una instancia del objeto FooSettings .

Beneficios

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

Además, dado que los parámetros de método predeterminados de C # se tratan como constantes de comstackción y se procesan en el callsite, los parámetros predeterminados solo serán utilizados por el código una vez que se vuelva a comstackr. Al crear una instancia de un objeto de configuración, la persona que llama carga dinámicamente los valores predeterminados al llamar 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 recomstackr a las personas que llaman para ver los nuevos valores, si así lo desea.

Mi sugerencia:

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

BTW TimeSpan.FromSeconds(2.0) no es igual a new TimeSpan(2000) – el constructor toma tics.