parámetro - optional parameter c# int
C#4.0: ¿Puedo usar un TimeSpan como un parámetro opcional con un valor predeterminado? (8)
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 el momento de la compilación? ¿Y hay una manera de especificar un valor predeterminado para un objeto TimeSpan opcional?
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 compilación. El conjunto de valores y expresiones sobre dichos valores permitidos en tiempo de compilació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á anidado (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.
Esto funciona bien:
void Foo(TimeSpan span = default(TimeSpan))
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)
{
//...
}
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.
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 compilación es porque en tiempo de 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ó 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 compilación no crean casos especiales solo porque sabemos que TimeSpan.FromSeconds es determinista.
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 compilación. Eso significa que el compilador debe ser capaz de evaluarlos y encontrar una respuesta. Hay algunas personas que desean que C # agregue compatibilidad para que el compilador 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<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 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 compilado más recientemente más opciones mientras sigue respaldando código compilado sin trabajo adicional. .
Además, dado que los parámetros de método predeterminados de C # se tratan como constantes de compilació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 compilar. 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 recompilar a las personas que llaman para ver los nuevos valores, si así lo desea.
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.