¿Por qué una expresión de invocación de método tiene un tipo dynamic incluso cuando solo hay un posible tipo de retorno?

Inspirado por esta pregunta .

Versión corta: ¿Por qué el comstackdor no puede deducir el tipo de M(dynamic arg) de tiempo de comstackción M(dynamic arg) si solo hay una sobrecarga de M o todas las sobrecargas de M tienen el mismo tipo de retorno?

Según la especificación, §7.6.5:

Una invocación-expresión está vinculada dinámicamente (§7.2.2) si se cumple al menos una de las siguientes características:

  • La expresión primaria tiene un tipo dynamic en tiempo de comstackción.

  • Al menos un argumento de la lista de argumentos opcional tiene un tipo dynamic en tiempo de comstackción y la expresión primaria no tiene un tipo de delegado.

Tiene sentido que para

 class Foo { public int M(string s) { return 0; } public string M(int s) { return String.Empty; } } 

el comstackdor no puede descifrar el tipo de tiempo de comstackción

 dynamic d = // dynamic var x = new Foo().M(d); 

porque no sabrá hasta el tiempo de ejecución qué sobrecarga de M se invoca.

Sin embargo, ¿por qué el comstackdor no puede deducir el tipo de tiempo de comstackción si M tiene solo una sobrecarga o todas las sobrecargas de M devuelven el mismo tipo?

Estoy tratando de entender por qué la especificación no permite que el comstackdor escriba estas expresiones estáticamente en tiempo de comstackción.

ACTUALIZACIÓN: Esta pregunta fue el tema de mi blog el 22 de octubre de 2012 . Gracias por la gran pregunta!


¿Por qué el comstackdor no puede deducir el tipo de comstackción M(dynamic_expression) si solo hay una sobrecarga de M o todas las sobrecargas de M tienen el mismo tipo de retorno?

El comstackdor puede averiguar el tipo de tiempo de comstackción; el tipo de tiempo de comstackción es dynamic , y el comstackdor se da cuenta de eso con éxito.

Creo que la pregunta que pretendía hacer es:

¿Por qué el tipo de tiempo de comstackción de M(dynamic_expression) siempre es dynamic, incluso en el raro y poco probable caso de que realice una llamada dinámica completamente innecesaria a un método M que siempre se elegirá independientemente del tipo de argumento?

Cuando formula la pregunta de esa manera, se responde a sí misma. 🙂

Razón uno:

Los casos que imaginas son raros; para que el comstackdor pueda realizar el tipo de inferencia que describe, se debe conocer suficiente información para que el comstackdor pueda hacer casi un análisis de tipo estático completo de la expresión. Pero si estás en ese escenario, ¿por qué estás usando la dinámica en primer lugar? Haría mucho mejor simplemente decir:

 object d = whatever; Foo foo = new Foo(); int x = (d is string) ? foo.M((string)d) : foo((int)d); 

Obviamente, si solo hay una sobrecarga de M, es aún más fácil: lanzar el objeto al tipo deseado . Si falla en tiempo de ejecución porque el lanzamiento fue malo, ¡bueno, la dinámica también habría fallado!

Simplemente no hay necesidad de dinámica en primer lugar en este tipo de escenarios, así que ¿por qué haríamos un montón de trabajo de inferencia de tipo costoso y difícil en el comstackdor para habilitar un escenario que no queremos que use dynamic para en primer lugar ?

Razón dos:

Supongamos que dijéramos que la resolución de sobrecarga tiene reglas muy especiales si se sabe que el grupo de métodos contiene un método. Estupendo. Ahora acabamos de agregar un nuevo tipo de fragilidad al lenguaje. Ahora agregar una nueva sobrecarga cambia el tipo de devolución de una llamada a un tipo completamente diferente , un tipo que no solo genera semántica dinámica, sino también tipos de valores de cuadros. ¡Pero espera, se pone peor!

 // Foo corporation: class B { } // Bar corporation: class D : B { public int M(int x) { return x; } } // Baz corporation: dynamic dyn = whatever; D d = new D(); var q = dM(dyn); 

Supongamos que implementamos su función requiest e inferimos que q es int, según su lógica. Ahora la corporación Foo agrega:

 class B { public string M(string x) { return x; } } 

Y de repente cuando Baz Corporation vuelve a comstackr su código, de repente el tipo de q silenciosamente cambia a dynamic, porque no sabemos en tiempo de comstackción que dyn no es una cadena. ¡Es un cambio extraño e inesperado en el análisis estático! ¿Por qué un tercero agregar un nuevo método a una clase base hace que el tipo de una variable local cambie en un método completamente diferente en una clase completamente diferente que se escribe en una compañía diferente, una compañía que ni siquiera usa B directamente, pero solo a través de D?

Esta es una nueva forma del problema de clase base frágil, y buscamos minimizar los problemas de la clase de base frágil en C #.

O, ¿qué pasaría si Foo corp dijera:

 class B { protected string M(string x) { return x; } } 

Ahora, por tu lógica,

 var q = dM(dyn); 

da q el tipo int cuando el código de arriba está fuera de un tipo que hereda de D, pero

 var q = this.M(dyn); 

da el tipo de q como dynamic cuando dentro de un tipo hereda de D! Como desarrollador, me parece bastante sorprendente.

Razón tres:

Ya hay demasiada habilidad en C #. Nuestro objective no es construir un motor lógico que pueda resolver todas las restricciones de tipo posibles en todos los valores posibles dado un progtwig en particular. Preferimos tener reglas generales, comprensibles y comprensibles que se puedan anotar fácilmente e implementar sin errores. La especificación ya tiene ochocientas páginas y escribir un comstackdor sin errores es increíblemente difícil. No lo hagamos más difícil. Por no mencionar el costo de probar todos esos casos locos.

Razón cuatro:

Además: el idioma te brinda muchas oportunidades de aprovecharte del analizador de tipo estático. Si está utilizando la dinámica, está pidiendo específicamente que ese analizador difiera su acción hasta el tiempo de ejecución . No debería ser una sorpresa que el uso de la característica “dejar de hacer análisis de tipo estático en tiempo de comstackción” haga que el análisis de tipo estático no funcione muy bien en tiempo de comstackción.

Un diseño inicial de la característica dynamic tenía soporte para algo como esto. El comstackdor aún tendría una resolución de sobrecarga estática e introdujo una “sobrecarga fantasma” que representa la resolución de sobrecarga dinámica solo si es necesario .

  • Publicación de blog que introduce métodos fantasmas
  • Detalles sobre métodos fantasmas

Como puede ver en la segunda publicación, este enfoque introduce mucha complejidad (el segundo artículo habla sobre cómo la inferencia de tipos debería modificarse para que el enfoque funcione). No me sorprende que el equipo de C # haya decidido usar la idea más simple de usar siempre una resolución de sobrecarga dynamic cuando se trata de dynamic .

Sin embargo, ¿por qué el comstackdor no puede deducir el tipo de tiempo de comstackción si M tiene solo una sobrecarga o todas las sobrecargas de M devuelven el mismo tipo?

El comstackdor podría hacer esto, pero el equipo de idioma decidió no hacerlo funcionar de esta manera.

Todo el propósito de la dynamic es tener todas las expresiones que utilizan ejecución dinámica con “su resolución se difiere hasta que se ejecuta el progtwig” (C # spec, 4.2.3). El comstackdor no realiza explícitamente el enlace estático (que sería necesario para obtener el comportamiento que desea aquí) para las expresiones dinámicas.

Tener una alternativa al enlace estático si solo hubiera una opción de enlace obligaría al comstackdor a verificar este caso, que no se agregó. En cuanto a por qué el equipo de idioma no quiso hacerlo, sospecho que la respuesta de Eric Lippert se aplica aquí :

Me preguntan “¿por qué C # no implementa la función X?” todo el tiempo. La respuesta es siempre la misma: porque nadie diseñó, especificó, implementó, probó, documentó y envió esa característica.

Creo que el caso de poder determinar de forma estática el único tipo de retorno posible de una resolución de método dynamic es tan estrecho que sería más confuso e incoherente si lo hiciera el comstackdor de C #, en lugar de tener un comportamiento general.

Incluso con su ejemplo, ¿qué pasa si Foo es parte de un dll diferente, Foo podría ser una versión más nueva en tiempo de ejecución de una redirección vinculante con M adicionales que tienen un tipo de devolución diferente, y entonces el comstackdor habría adivinado mal porque el tiempo de ejecución la resolución devolvería un tipo diferente.

¿Qué pasa si Foo es un IDynamicMetaObjectProvider d podría no coincidir con ninguno de los argumentos estáticos y, por lo tanto, se recurriría a su comportamiento dynamic que posiblemente podría devolver un tipo diferente.