c# - sentencia - que es c sharp
¿Por qué el compilador de C#elimina una cadena de llamadas a métodos cuando la última es condicional? (3)
Considere las siguientes clases:
public class A {
public B GetB() {
Console.WriteLine("GetB");
return new B();
}
}
public class B {
[System.Diagnostics.Conditional("DEBUG")]
public void Hello() {
Console.WriteLine("Hello");
}
}
Ahora, si tuviéramos que llamar a los métodos de esta manera:
var a = new A();
var b = a.GetB();
b.Hello();
En una versión de lanzamiento (es decir, sin indicador
DEBUG
), solo veríamos
GetB
impreso en la consola, ya que el compilador
GetB
la llamada a
Hello()
.
En una compilación de depuración, aparecerían ambas impresiones.
Ahora encadenemos las llamadas al método:
a.GetB().Hello();
El comportamiento en una compilación de depuración no ha cambiado; sin embargo, obtenemos un resultado diferente si no se establece el indicador: se omiten ambas llamadas y no aparecen impresiones en la consola. Un vistazo rápido a IL muestra que no se compiló toda la línea.
Según el
último estándar de ECMA para C #
(ECMA-334, es decir, C # 5.0), el comportamiento esperado cuando se coloca el atributo
Conditional
en el método es el siguiente (énfasis mío):
Se incluye una llamada a un método condicional si uno o más de sus símbolos de compilación condicional asociados se definen en el punto de llamada; de lo contrario, se omite la llamada . (§22.5.3)
Esto no parece indicar que se deba ignorar toda la cadena, de ahí mi pregunta. Dicho esto, el borrador de la especificación C # 6.0 de Microsoft ofrece un poco más de detalle:
Si se define el símbolo, se incluye la llamada; de lo contrario, se omite la llamada (incluida la evaluación del receptor y los parámetros de la llamada).
El hecho de que los parámetros de la llamada no se evalúen está bien documentado, ya que es una de las razones por las cuales las personas usan esta función en lugar de
#if
directivas
#if
en el cuerpo de la función.
Sin embargo, la parte sobre la "evaluación del receptor" es nueva: parece que no puedo encontrarla en otro lugar y parece explicar el comportamiento anterior.
A la luz de esto, mi pregunta es:
¿cuál es la razón detrás del compilador de C # que no evalúa
a.GetB()
en esta situación?
¿Debería realmente comportarse de manera diferente en función de si el receptor de la llamada condicional se almacena en una variable temporal o no?
¿Debería realmente comportarse de manera diferente en función de si el receptor de la llamada condicional se almacena en una variable temporal o no?
Sí.
¿Cuál es la razón detrás del compilador de C # que no evalúa
a.GetB()
en esta situación?
Las respuestas de Marc y Søren son básicamente correctas. Esta respuesta es solo para documentar claramente la línea de tiempo.
- La característica fue diseñada en 1999, y la intención de la característica siempre fue eliminar toda la declaración.
- Las notas de diseño de 2003 indican que el equipo de diseño se dio cuenta de que la especificación no estaba clara en este punto. Hasta este punto, la especificación solo decía que los argumentos no serían evaluados. Observo que la especificación comete el error común de llamar a los argumentos "parámetros", aunque, por supuesto, uno podría suponer que significaban "parámetros reales" en lugar de "parámetros formales".
- Se suponía que se debía crear un elemento de trabajo para corregir la especificación de ECMA en este punto; aparentemente eso nunca sucedió.
- La primera vez que el texto corregido aparece en cualquier especificación de C # fue la especificación de C # 4.0, que creo que fue 2010. (No recuerdo si esta fue una de mis correcciones, o si alguien más la encontró).
- Si la especificación ECMA 2017 no contiene esta corrección, entonces ese es un error que debería corregirse en la próxima versión. Mejor 15 años tarde que nunca, supongo.
Todo se reduce a la frase:
(incluida la evaluación del receptor y los parámetros de la llamada) se omite.
En la expresion:
a.GetB().Hello();
la "evaluación del receptor" es:
a.GetB()
.
Entonces: eso se omite
según la especificación
, y es un truco útil que permite que
[Conditional]
evite los gastos generales para las cosas que
no se usan
.
Cuando lo pones en un local:
var b = a.GetB();
b.Hello();
entonces la "evaluación del receptor" es solo la
b
local, pero la
var b = a.GetB();
original
var b = a.GetB();
aún se evalúa (incluso si el
b
local termina siendo eliminado).
Esto
puede
tener consecuencias no deseadas, por lo tanto: use
[Conditional]
con mucho cuidado.
Pero las razones son para que cosas como el registro y la depuración se puedan agregar y eliminar de manera trivial.
Tenga en cuenta que los parámetros
también
pueden ser problemáticos si se tratan ingenuamente:
LogStatus("added: " + engine.DoImportantStuff());
y:
var count = engine.DoImportantStuff();
LogStatus("added: " + count);
puede ser
muy
diferente si
LogStatus
está marcado
[Conditional]
, con el resultado de que sus "cosas importantes" reales no se realizaron.
Investigué un poco y descubrí que la especificación del lenguaje C # 5.0 ya contenía su segunda cita en la sección 17.4.2 El atributo Condicional en la página 424.
La respuesta de Marc Gravell ya muestra que este comportamiento es intencionado y lo que significa en la práctica. También preguntó acerca de la razón detrás de esto, pero parece estar insatisfecho por la mención de Marc de eliminar los gastos generales.
¿Quizás se pregunte por qué se considera una sobrecarga que se puede eliminar?
a.GetB().Hello();
no ser llamado en absoluto en su escenario con la
Hello()
puede parecer extraño a su valor nominal.
No conozco los fundamentos de la decisión, pero encontré algunos razonamientos plausibles. Quizás también te pueda ayudar.
El encadenamiento de métodos
solo es posible si cada método anterior tiene un valor de retorno.
Esto tiene sentido cuando desea hacer algo con estos valores, es decir,
a.GetFoos().MakeBars().AnnounceBars();
Si tiene una función que solo hace algo sin devolver un valor, no puede encadenar algo detrás de él, pero puede ponerlo al final de la cadena del método, como es el caso con su método condicional, ya que debe tener el tipo de retorno nulo.
También tenga en cuenta que el
resultado
de las llamadas a métodos anteriores se
descarta
, por lo que en su ejemplo de
a.GetB().Hello();
su el resultado de
GetB()
no tiene ninguna razón para vivir después de que se ejecute esta declaración.
Básicamente,
implica
que necesita el resultado de
GetB()
solo para usar
Hello()
.
Si se omite
Hello()
¿por qué necesita
GetB()
entonces?
Si omite
Hello()
su línea se reduce a
a.GetB();
sin ninguna asignación y muchas herramientas le darán una advertencia de que no está utilizando el valor de retorno porque esto rara vez es algo que desea hacer.
La razón por la que parece no estar de acuerdo con esto es que su método no solo está tratando de hacer lo necesario para devolver un cierto valor, sino que también tiene un
efecto secundario
, es decir, E / S.
Si, en cambio, tuviera una
función pura
,
realmente
no habría razón para
GetB()
si omite la llamada posterior, es decir, si no va a hacer nada con el resultado.
Si asigna el resultado de
GetB()
a una variable, esta es una declaración en sí misma y se ejecutará de todos modos.
Entonces este razonamiento explica por qué en
var b = a.GetB();
b.Hello();
solo se omite la llamada a
Hello()
mientras que cuando se usa el método de encadenamiento se omite toda la cadena.
También puede buscar un lugar completamente diferente para obtener una mejor perspectiva: ¿el
operador condicional nulo
o el
operador de elvis
?
introducido en C # 6.0.
Aunque solo es azúcar sintáctico para una expresión más compleja con comprobaciones nulas, le permite construir algo así como una cadena de métodos con la opción de cortocircuito basado en la comprobación nula.
Por ejemplo,
GetFoos()?.MakeBars()?.AnnounceBars();
solo llegará a su fin si los métodos anteriores no devuelven
null
, de lo contrario se omiten las llamadas posteriores.
Puede ser contrario a la intuición, pero intente pensar en su escenario como el inverso de esto: el compilador omite sus llamadas antes de
Hello()
en su
a.GetB().Hello();
cadena ya que no estás llegando al final de la cadena de todos modos.
Renuncia
Todo esto ha sido un razonamiento del sillón, así que tome esto y la analogía con el operador de Elvis con un grano de sal.