c# - parametros - ¿Por qué no puedo dar un valor predeterminado como parámetro opcional excepto nulo?
parametros c# (4)
Quiero tener un parámetro opcional y configurarlo en el valor predeterminado que determine, cuando hago esto:
private void Process(Foo f = new Foo())
{
}
Recibo el siguiente error ( Foo
es una clase):
''f'' es un tipo de Foo. Un parámetro predeterminado de un tipo de referencia distinto de la cadena solo se puede inicializar con nulo.
Si cambio Foo
a struct
entonces funciona pero solo con el constructor predeterminado sin parámetros .
Leí la documentación y claramente dice que no puedo hacer esto, pero no menciona por qué. , ¿Por qué existe esta restricción y por qué se excluye la string
de esto? ¿Por qué el valor de un parámetro opcional tiene que ser una constante de compilación ? Si eso no fuera una constante, ¿cuáles serían los efectos secundarios?
Es exactamente la forma en que funciona el idioma, no puedo decir por qué lo hacen (y este sitio no es un sitio para discusiones como esa , si quieres discutirlo, llévalo al chat ).
Puedo mostrarle cómo solucionarlo, simplemente hacer dos métodos y sobrecargarlo (modificó ligeramente su ejemplo para mostrar cómo devolvería los resultados también).
private Bar Process()
{
return Process(new Foo());
}
private Bar Process(Foo f)
{
//Whatever.
}
Los parámetros predeterminados manipulan a la persona que llama de manera que cuando usted proporciona un parámetro predeterminado, cambiará la firma de sus métodos en el momento de la compilación. Por eso, debe proporcionar un Valor constante, que en su caso "new Foo ()" no lo es.
Es por eso que necesitas una constante.
Porque no hay otra constante de compilación que nula. Para cadenas, los literales de cadena son tales constantes de tiempo de compilación.
Creo que algunas de las decisiones de diseño detrás de esto pueden haber sido:
- Simplicidad de implementación
- Eliminación de comportamientos ocultos / inesperados.
- Claridad del contrato de método, esp. en escenarios de montaje cruzado
Vamos a detallar un poco más estos tres para tener una idea del problema:
1. Simplicidad de implementación.
Cuando se limitan a valores constantes, tanto el compilador como el trabajo de CLR son bastante fáciles. Los valores constantes se pueden almacenar fácilmente en metadatos de ensamblaje, y el compilador puede fácilmente. Cómo se hace esto se describe en la respuesta de Hans Passant .
Pero, ¿qué podrían hacer CLR y el compilador para implementar valores predeterminados no constantes? Hay dos opciones:
Almacene las expresiones de inicialización y compílelas allí:
// seen by the developer in the source code Process(); // actually done by the compiler Process(new Foo());
Generar thunks:
// seen by the developer in the source code Process(); … void Process(Foo arg = new Foo()) { … } // actually done by the compiler Process_Thunk(); … void Process_Thunk() { Process(new Foo()); } void Process() { … }
Ambas soluciones introducen muchos más metadatos nuevos en los ensamblajes y requieren un manejo complejo por parte del compilador. Además, si bien la solución (2) se puede ver como un tecnicismo oculto (así como (1)), tiene consecuencias con respecto al comportamiento percibido. El desarrollador espera que los argumentos se evalúen en el sitio de la llamada, no en otro lugar. Esto puede imponer problemas adicionales a resolver (ver parte relacionada con el contrato de método).
2. Eliminación de comportamientos ocultos / inesperados.
La expresión de inicialización podría haber sido arbitrariamente compleja. De ahí una simple llamada como esta:
Process();
se desenrollaría en un cálculo complejo realizado en el sitio de la llamada . Por ejemplo:
Process(new Foo(HorriblyComplexCalculation(SomeStaticVar) * Math.Power(GetCoefficient, 17)));
Esto puede ser bastante inesperado desde el punto de vista del lector que no inspecciona a fondo la declaración de ''Proceso''. Se desordena el código, hace que sea menos legible.
3. Claridad del contrato de método, esp. en escenarios de montaje cruzado
La firma de un método junto con los valores por defecto impone un contrato. Este contrato vive en un contexto particular. Si la expresión de inicialización requiriera enlaces a otros ensamblajes, ¿qué requeriría eso de la persona que llama? ¿Qué tal este ejemplo, donde el método ''CalculateInput'' es de ''Other.Assembly'':
void Process(Foo arg = new Foo(Other.Assembly.Namespace.CalculateInput()))
Este es el punto en el que la forma en que esto se implementaría desempeña un papel fundamental al pensar si se trata de un problema o una nota. En la sección de "simplicidad" he descrito los métodos de implementación (1) y (2). Por lo tanto, si se eligiera (1), se requeriría que la persona que llama se vincule a ''Otro.Asamblaje''. Por otro lado, si se eligió (2), existe una necesidad mucho menor, desde el punto de vista de la implementación, de dicha regla, porque el Process_Thunk
generado por el compilador se declara en el mismo lugar que Process
y, por lo tanto, naturalmente tiene una referencia a Other.Aseembly
. Sin embargo , un diseñador de lenguaje sano incluso impondría tal regla, porque son posibles múltiples implementaciones de la misma cosa, y en aras de la estabilidad y claridad del contrato de método.
Sin embargo, los escenarios de ensamblaje cruzado impondrían referencias de ensamblaje que no se ven claramente en el código fuente en el sitio de la llamada. Y ese es un problema de usabilidad y legibilidad, otra vez.
Un punto de partida es que el CLR no tiene soporte para esto. Debe ser implementado por el compilador. Algo que puedes ver desde un pequeño programa de prueba:
class Program {
static void Main(string[] args) {
Test();
Test(42);
}
static void Test(int value = 42) {
}
}
Que descompila a:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 15 (0xf)
.maxstack 8
IL_0000: ldc.i4.s 42
IL_0002: call void Program::Test(int32)
IL_0007: ldc.i4.s 42
IL_0009: call void Program::Test(int32)
IL_000e: ret
} // end of method Program::Main
.method private hidebysig static void Test([opt] int32 ''value'') cil managed
{
.param [1] = int32(0x0000002A)
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} // end of method Program::Test
Tenga en cuenta que no hay diferencia alguna entre las dos declaraciones de llamada después de que el compilador haya terminado con eso. Fue el compilador el que aplicó el valor predeterminado y lo hizo en el sitio de la llamada.
También tenga en cuenta que esto todavía debe funcionar cuando el método Test () realmente vive en otro conjunto. Lo que implica que el valor predeterminado debe estar codificado en los metadatos. Observe cómo la directiva .param
hizo esto. La especificación CLI (Ecma-335) lo documenta en la sección II.15.4.1.4
Esta directiva almacena en los metadatos un valor constante asociado con el número de parámetro de método Int32, consulte §II.22.9. Si bien el CLI requiere que se proporcione un valor para el parámetro, algunas herramientas pueden usar la presencia de este atributo para indicar que la herramienta, en lugar del usuario, está destinada a proporcionar el valor del parámetro. A diferencia de las instrucciones CIL, .param usa el índice 0 para especificar el valor de retorno del método, el índice 1 para especificar el primer parámetro del método, el índice 2 para especificar el segundo parámetro del método, y así sucesivamente.
[Nota: la CLI no adjunta ningún tipo de semántica a estos valores; es totalmente responsabilidad de los compiladores implementar cualquier semántica que deseen (por ejemplo, los llamados valores de argumento predeterminados). nota final]
La sección citada II.22.9 entra en el detalle de lo que significa un valor constante. La parte más relevante:
El tipo debe ser exactamente uno de los siguientes: ELEMENT_TYPE_BOOLEAN, ELEMENT_TYPE_Craqueo de los derechos de los animales. o ELEMENT_TYPE_CLASS con un valor de cero
Así que ahí es donde se detiene el dinero, no es una buena manera de hacer referencia incluso a un método de ayuda anónimo, por lo que algún tipo de truco de elevación de código tampoco puede funcionar.
Notable es que simplemente no es un problema, siempre puede implementar un valor predeterminado arbitrario para un argumento de un tipo de referencia. Por ejemplo:
private void Process(Foo f = null)
{
if (f == null) f = new Foo();
}
Lo cual es bastante razonable. Y el tipo de código que desea en el método en lugar del sitio de llamada.