parameter - Parámetros opcionales de C#en métodos reemplazados
default value method c# (9)
Parece que en .NET Framework hay un problema con los parámetros opcionales cuando anulas el método. La salida del siguiente código es: "bbb" "aaa". Pero la salida que estoy esperando es: "bbb" "bbb". ¿Hay alguna solución para esto? Sé que se puede resolver con sobrecarga de métodos, pero me pregunto la razón de esto. Además, el código funciona bien en Mono.
class Program
{
class AAA
{
public virtual void MyMethod(string s = "aaa")
{
Console.WriteLine(s);
}
public virtual void MyMethod2()
{
MyMethod();
}
}
class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}
public override void MyMethod2()
{
MyMethod();
}
}
static void Main(string[] args)
{
BBB asd = new BBB();
asd.MyMethod();
asd.MyMethod2();
}
}
De acuerdo en general con @Marc Gravell.
Sin embargo, me gustaría mencionar que el problema es lo suficientemente antiguo en C ++ world ( http://www.devx.com/tips/Tip/12737 ), y la respuesta parece "a diferencia de las funciones virtuales, que se resuelven al ejecutar tiempo, los argumentos predeterminados se resuelven estáticamente, es decir, en tiempo compilado ". Así que este comportamiento del compilador de C # había sido aceptado deliberadamente debido a la coherencia, a pesar de su inesperada, parece.
El comportamiento es definitivamente muy extraño; no está claro para mí si de hecho es un error en el compilador, pero podría ser.
El campus obtuvo una buena cantidad de nieve anoche y Seattle no es muy buena lidiando con la nieve. Mi autobús no se está ejecutando esta mañana, así que no voy a poder entrar a la oficina para comparar lo que C # 4, C # 5 y Roslyn dicen sobre este caso y si no están de acuerdo. Trataré de publicar un análisis más adelante esta semana una vez que regrese a la oficina y pueda usar las herramientas de depuración adecuadas.
Esto creo que se debe a que estos valores predeterminados están fijos en el tiempo de compilación. Si usa el reflector, verá lo siguiente para MyMethod2 en BBB.
public override void MyMethod2()
{
this.MyMethod("aaa");
}
Esto parece un error para mí. Creo que está bien especificado, y que debería comportarse de la misma manera que si llamas el método con this
prefijo explícito.
Simplifiqué el ejemplo para usar solo un único método virtual y mostrar tanto la implementación que se llama como el valor del parámetro:
using System;
class Base
{
public virtual void M(string text = "base-default")
{
Console.WriteLine("Base.M: {0}", text);
}
}
class Derived : Base
{
public override void M(string text = "derived-default")
{
Console.WriteLine("Derived.M: {0}", text);
}
public void RunTests()
{
M(); // Prints Derived.M: base-default
this.M(); // Prints Derived.M: derived-default
base.M(); // Prints Base.M: base-default
}
}
class Test
{
static void Main()
{
Derived d = new Derived();
d.RunTests();
}
}
Entonces, todo lo que tenemos que preocuparnos son las tres llamadas dentro de RunTests. Los bits importantes de la especificación para las dos primeras llamadas son la sección 7.5.1.1, que habla sobre la lista de parámetros que se utilizará al encontrar los parámetros correspondientes:
Para los métodos virtuales y los indexadores definidos en las clases, la lista de parámetros se selecciona de la declaración o anulación más específica del miembro de la función, comenzando por el tipo estático del receptor y buscando a través de sus clases base.
Y la sección 7.5.1.2:
Cuando se omiten los argumentos de un miembro de la función con los parámetros opcionales correspondientes, los argumentos predeterminados de la declaración del miembro de la función se pasan implícitamente.
El "parámetro opcional correspondiente" es el bit que une 7.5.2 a 7.5.1.1.
Para M()
y this.M()
, esa lista de parámetros debe ser la Derived
en Derived
ya que el tipo estático del receptor es Derived
. De hecho, se puede decir que el compilador lo trata como la lista de parámetros anterior en la compilación, como si hace que el parámetro sea obligatorio en Derived.M()
, ambas llamadas fallan, por lo que la llamada M()
requiere que el parámetro tenga un valor predeterminado en Derived
, ¡pero luego lo ignora!
De hecho, empeora: si proporciona un valor predeterminado para el parámetro en Derived
pero lo hace obligatorio en Base
, la llamada M()
termina usando null
como valor del argumento. Si nada más, diría que es un error: ese valor null
no puede venir de ningún lado válido. (Es null
debido a que es el valor predeterminado del tipo de string
sino que solo utiliza el valor predeterminado para el tipo de parámetro).
La Sección 7.6.8 de la especificación trata con base.M (), que dice que , además del comportamiento no virtual, la expresión se considera como ((Base) this).M()
; por lo tanto, es completamente correcto que el método base se use para determinar la lista de parámetros efectivos. Eso significa que la línea final es correcta.
Solo para facilitar las cosas a cualquiera que quiera ver el error realmente extraño descrito anteriormente, donde se usa un valor no especificado en ningún lugar:
using System;
class Base
{
public virtual void M(int x)
{
// This isn''t called
}
}
class Derived : Base
{
public override void M(int x = 5)
{
Console.WriteLine("Derived.M: {0}", x);
}
public void RunTests()
{
M(); // Prints Derived.M: 0
}
static void Main()
{
new Derived().RunTests();
}
}
Has probado:
public override void MyMethod2()
{
this.MyMethod();
}
Entonces le dices a tu programa que use el Método modificado.
Puede ser que esto se deba a la ambigüedad y el compilador esté dando prioridad a la base / superclase. El siguiente cambio al código de su clase BBB con la adición de referencia a this
palabra clave, da la salida ''bbb bbb'':
class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}
public override void MyMethod2()
{
this.MyMethod(); //added this keyword here
}
}
Una de las cosas que implica es que siempre debe usar la palabra clave this
cuando llame propiedades o métodos en la instancia actual de la clase como una práctica recomendada .
Me preocuparía si esta ambigüedad en el método base y secundario ni siquiera generara una advertencia de compilación (si no es un error), pero si lo hace, entonces eso no se vio, supongo.
=============================================== ================
EDITAR: Considere a continuación extractos de muestra de estos enlaces:
Peligro: los valores de los parámetros opcionales son tiempo de compilación. Hay una cosa y una cosa que debe tenerse en cuenta al usar parámetros opcionales. Si tiene esto en mente, es probable que pueda comprender y evitar cualquier peligro potencial con su uso: esa es una cosa: ¡los parámetros opcionales son el azúcar sintáctico en tiempo de compilación!
Peligro: Cuidado con los Parámetros por Defecto en la Herencia y la Implementación de la Interfaz
Ahora, el segundo riesgo potencial tiene que ver con la implementación de la herencia y la interfaz. Ilustraré con un acertijo:
1: public interface ITag
2: {
3: void WriteTag(string tagName = "ITag");
4: }
5:
6: public class BaseTag : ITag
7: {
8: public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
9: }
10:
11: public class SubTag : BaseTag
12: {
13: public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
14: }
15:
16: public static class Program
17: {
18: public static void Main()
19: {
20: SubTag subTag = new SubTag();
21: BaseTag subByBaseTag = subTag;
22: ITag subByInterfaceTag = subTag;
23:
24: // what happens here?
25: subTag.WriteTag();
26: subByBaseTag.WriteTag();
27: subByInterfaceTag.WriteTag();
28: }
29: }
¿Lo que pasa? Bueno, aunque el objeto en cada caso es SubTag cuya etiqueta es "SubTag", obtendrás:
1: SubTag 2: BaseTag 3: ITag
Pero recuerda asegurarte de que:
No inserte nuevos parámetros predeterminados en el medio de un conjunto existente de parámetros predeterminados, esto puede causar un comportamiento impredecible que puede no arrojar necesariamente un error de sintaxis: agregar al final de la lista o crear un nuevo método. Tenga mucho cuidado al usar los parámetros predeterminados en las jerarquías e interfaces de herencia: elija el nivel más apropiado para agregar los valores predeterminados según el uso esperado.
=============================================== ========================
Puedes desambiguar llamando al:
this.MyMethod();
(en MyMethod2()
)
Si es un error es complicado; pero parece inconsistente. Resharper le advierte que no debe tener cambios en el valor predeterminado en una anulación, si eso ayuda; p Por supuesto, el reafilamiento también le dice this.
es redundante, y se ofrece para quitárselo ... lo que cambia el comportamiento, por lo que el reafilado tampoco es perfecto.
Parece que podría calificar como un error del compilador, te lo concederé. Tendría que mirar muy cuidadosamente para estar seguro ... ¿dónde está Eric cuando lo necesitas, eh?
Editar:
El punto clave aquí es la especificación del idioma; echemos un vistazo a §7.5.3:
Por ejemplo, el conjunto de candidatos para una invocación de método no incluye los métodos marcados como anulación (§7.4), y los métodos en una clase base no son candidatos si se aplica cualquier método en una clase derivada (§7.6.5.1).
(y de hecho §7.4 omite claramente override
métodos de consideración)
Aquí hay algún conflicto ... indica que los métodos base no se usan si hay un método aplicable en una clase derivada, lo que nos llevaría al método derivado , pero al mismo tiempo dice que los métodos marcados son no considerado.
Pero, §7.5.1.1 luego dice:
Para los métodos virtuales y los indexadores definidos en las clases, la lista de parámetros se selecciona de la declaración o anulación más específica del miembro de la función, comenzando por el tipo estático del receptor y buscando a través de sus clases base.
y luego §7.5.1.2 explica cómo se evalúan los valores en el momento de la invocación:
Durante el procesamiento en tiempo de ejecución de una invocación de miembro de función (§7.5.4), las expresiones o referencias de variable de una lista de argumentos se evalúan en orden, de izquierda a derecha, de la siguiente manera:
...(recorte)...
Cuando se omiten los argumentos de un miembro de la función con los parámetros opcionales correspondientes, los argumentos predeterminados de la declaración del miembro de la función se pasan implícitamente. Debido a que estos son siempre constantes, su evaluación no afectará el orden de evaluación de los argumentos restantes.
Esto resalta explícitamente que está mirando la lista de argumentos, que se definió previamente en §7.5.1.1 como procedente de la declaración o anulación más específica . Parece razonable que esta es la "declaración de método" a la que se hace referencia en §7.5.1.2, por lo que el valor pasado debe ser del tipo más derivado hasta el estático.
Esto sugeriría: csc tiene un error, y debería usar la versión derivada ("bbb bbb"), a menos que esté restringido (a través de base.
, o conversión a un tipo de base) a mirar las declaraciones de método base (§7.6 .8).
Una cosa que vale la pena señalar aquí, es que la versión reemplazada se llama cada vez. Cambiar la anulación a:
public override void MyMethod(string s = "bbb")
{
Console.Write("derived: ");
base.MyMethod(s);
}
Y la salida es:
derived: bbb
derived: aaa
Un método en una clase puede hacer uno o dos de los siguientes:
- Define una interfaz para otro código para llamar.
- Define una implementación para ejecutar cuando se llama.
Puede que no haga ambas cosas, ya que un método abstracto solo hace lo primero.
Dentro de BBB
la llamada MyMethod()
llama a un método definido en AAA
.
Debido a que hay una anulación en BBB
, llamar a ese método da como resultado una implementación en BBB
.
Ahora, la definición en AAA
informa el código de llamada de dos cosas (bueno, algunas otras también que no importan aquí).
- La firma
void MyMethod(string)
. - (Para los idiomas que lo soportan) el valor predeterminado para el parámetro único es
"aaa"
y, por lo tanto, al compilar código de la formaMyMethod()
si no se puede encontrar ningún método que coincida conMyMethod()
, puede reemplazarlo con una llamada ` MyMethod ("aaa").
Entonces, eso es lo que hace la llamada en BBB
: el compilador ve una llamada a MyMethod()
, no encuentra un método MyMethod()
pero encuentra un método MyMethod(string)
. También ve que en el lugar donde está definido hay un valor predeterminado de "aaa", por lo que en tiempo de compilación lo cambia a una llamada a MyMethod("aaa")
.
Desde dentro de BBB
, AAA
se considera el lugar donde se definen los métodos de AAA
, incluso si están anulados en BBB
, de modo que se pueden omitir.
En tiempo de MyMethod(string)
, MyMethod(string)
se llama con el argumento "aaa". Debido a que hay una forma anulada, esa es la forma llamada, pero no se llama con "bbb" porque ese valor no tiene nada que ver con la implementación en tiempo de ejecución sino con la definición del tiempo de compilación.
Agregando this.
cambia qué definición se examina y, por lo tanto, cambia qué argumento se usa en la llamada.
Editar: Por qué esto me parece más intuitivo.
Personalmente, y dado que estoy hablando de lo que es intuitivo, solo puede ser personal, me resulta más intuitivo por la siguiente razón:
Si estuviera codificando BBB
entonces, ya sea llamando o reemplazando a MyMethod(string)
, pensaría en eso como "hacer cosas AAA
" - es la BBB
de BBB
de "hacer cosas AAA
", pero está haciendo las cosas de AAA
todos modos. Por lo tanto, ya sea llamando o anulando, voy a ser consciente del hecho de que fue AAA
que definió MyMethod(string)
.
Si estuviera llamando al código que usaba BBB
, pensaría en "usar material de BBB
". Puede que no sea muy consciente de cuál fue originalmente definido en AAA
, y quizás pensaría en esto como simplemente un detalle de implementación (si yo no usara también la interfaz AAA
cercana).
El comportamiento del compilador coincide con mi intuición, y es por eso que cuando leí por primera vez la pregunta, me pareció que Mono tenía un error. Tras considerarlo, no veo cómo cumple el comportamiento especificado mejor que el otro.
No obstante, aunque permanezca en un nivel personal, nunca usaría parámetros opcionales con métodos abstractos, virtuales o reemplazados, y si reemplazara a los de otra persona, igualaría los suyos.
De cualquier manera, necesita una reparación
Definitivamente lo consideraría un error, ya sea porque los resultados son incorrectos o si se esperan los resultados, entonces el compilador no debería permitirle declararlo como "anulación", o al menos proporcionar una advertencia.
Te recomendaría que informaras esto a Microsoft.Connect
¿Pero es correcto o incorrecto?
Sin embargo, con respecto a si este es el comportamiento esperado o no, analicemos primero los dos puntos de vista sobre él.
Considera que tenemos el siguiente código:
void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation
myfunc(); //Call using the default arguments
Hay dos formas de implementarlo:
Los argumentos opcionales se tratan como funciones sobrecargadas, lo que da como resultado lo siguiente:
void myfunc(int optional){ /* Some code here*/ } //Function implementation void myfunc(){ myfunc(5); } //Default arguments implementation myfunc(); //Call using the default arguments
Que el valor predeterminado está incrustado en la persona que llama, lo que da como resultado el siguiente código:
void myfunc(int optional){ /* Some code here*/ } //Function implementation myfunc(5); //Call and embed default arguments
Hay muchas diferencias entre los dos enfoques, pero primero veremos cómo lo interpreta .Net framework.
En .Net solo puede anular un método con un método que contiene la misma cantidad de argumentos, pero no puede anular con un método que contenga más argumentos, incluso si son todos opcionales (lo que daría lugar a que una llamada tenga la misma firma que el método anulado), por ejemplo, tiene:
class bassClass{ public virtual void someMethod()} class subClass :bassClass{ public override void someMethod()} //Legal //The following is illegal, although it would be called as someMethod(); //class subClass:bassClass{ public override void someMethod(int optional = 5)}
Puede sobrecargar un método con argumentos predeterminados con otro método sin argumentos (esto tiene implicaciones desastrosas, como lo comentaré en un momento), por lo que el siguiente código es legal:
void myfunc(int optional = 5){ /* Some code here*/ } //Function with default void myfunc(){ /* Some code here*/ } //No arguments myfunc(); //Call which one?, the one with no arguments!
cuando se utiliza la reflexión, uno siempre debe proporcionar un valor predeterminado.
Todo lo cual es suficiente para demostrar que .Net tomó la segunda implementación, por lo que el comportamiento que vio el OP fue correcto, al menos según .Net.
Problemas con el enfoque .Net
Sin embargo, hay problemas reales con el enfoque .Net.
Consistencia
Al igual que en el problema del OP al anular el valor predeterminado en un método heredado, los resultados pueden ser impredecibles
Cuando se modifique la implantación original del valor predeterminado y las personas que llaman no tengan que volver a compilarse, podríamos terminar con valores predeterminados que ya no son válidos.
- La reflexión requiere que proporcione el valor predeterminado, que la persona que llama no tiene que saber
Código de ruptura
Cuando tenemos una función con argumentos predeterminados y luego agregamos una función sin argumentos, todas las llamadas ahora encaminarán a la nueva función, rompiendo así todos los códigos existentes, sin ninguna notificación o advertencia.
Similarmente sucederá, si más tarde quitamos la función sin argumentos, entonces todas las llamadas se dirigirán automáticamente a la función con los argumentos predeterminados, nuevamente sin notificación ni advertencia. aunque esta podría no ser la intención del programador
Además, no tiene que ser un método de instancia regular, un método de extensión hará los mismos problemas, ya que un método de extensión sin parámetros tendrá prioridad sobre un método de instancia con parámetros predeterminados.
Resumen: MANTENTE ALEJADO DE ARGUMENTOS OPCIONALES, Y USO EN LUGAR DE SOBRECARGAS (COMO LO IMPONE EL MARCO .NET)