¿Por qué los paréntesis del constructor del inicializador de objetos C # 3.0 son opcionales?

Parece que la syntax del inicializador de objetos C # 3.0 permite excluir el par abierto / cerrado de paréntesis en el constructor cuando existe un constructor sin parámetros. Ejemplo:

var x = new XTypeName { PropA = value, PropB = value }; 

Opuesto a:

 var x = new XTypeName() { PropA = value, PropB = value }; 

Tengo curiosidad por qué el par constructor abrir / cerrar paréntesis es opcional aquí después de XTypeName ?

Esta pregunta fue el tema de mi blog el 20 de septiembre de 2010 . Las respuestas de Josh y Chad (“no agregan ningún valor, ¿por qué las necesitan?” Y “para eliminar la redundancia”) son básicamente correctas. Para desarrollar eso un poco más:

La característica de permitirle eludir la lista de argumentos como parte de la “función más amplia” de los inicializadores de objetos cumplió con nuestra barra para las características “azucaradas”. Algunos puntos que consideramos:

  • el costo de diseño y especificación fue bajo
  • íbamos a cambiar ampliamente el código del analizador que maneja la creación de objetos de todos modos; el costo de desarrollo adicional de hacer que la lista de parámetros sea opcional no era grande en comparación con el costo de la función más grande
  • la carga de prueba fue relativamente pequeña en comparación con el costo de la función más grande
  • la carga de documentación fue relativamente pequeña en comparación …
  • se anticipó que la carga de mantenimiento era pequeña; No recuerdo ningún error reportado en esta característica en los años desde que se envió.
  • la función no presenta ningún riesgo inmediatamente obvio para las características futuras en esta área. (Lo último que queremos hacer es crear una característica barata y fácil ahora que hace que sea mucho más difícil implementar una característica más convincente en el futuro).
  • la función no agrega nuevas ambigüedades al análisis léxico, gtwigtical o semántico del lenguaje. No plantea problemas para el tipo de análisis de “progtwig parcial” que realiza el motor “IntelliSense” del IDE mientras escribe. Y así.
  • la característica alcanza un “punto óptimo” común para la característica de inicialización de objetos más grande; por lo general, si está utilizando un inicializador de objetos, es precisamente porque el constructor del objeto no le permite establecer las propiedades que desea. Es muy común que tales objetos sean simplemente “bolsas de propiedades” que no tienen parámetros en el ctor en primer lugar.

¿Por qué entonces tampoco hizo que los paréntesis vacíos fueran opcionales en la llamada de constructor predeterminada de una expresión de creación de objetos que no tiene un inicializador de objetos?

Eche otro vistazo a la lista de criterios anterior. Una de ellas es que el cambio no introduce ninguna nueva ambigüedad en el análisis léxico, gtwigtical o semántico de un progtwig. El cambio propuesto introduce una ambigüedad en el análisis semántico:

 class P { class B { public class M { } } class C : B { new public void M(){} } static void Main() { new C().M(); // 1 new CM(); // 2 } } 

La línea 1 crea una nueva C, llama al constructor predeterminado y luego llama al método de instancia M en el nuevo objeto. La línea 2 crea una nueva instancia de BM y llama a su constructor predeterminado. Si los paréntesis en la línea 1 fueran opcionales, la línea 2 sería ambigua. Entonces tendríamos que idear una regla que resuelva la ambigüedad; no podríamos convertirlo en un error porque eso sería un cambio radical que cambia un progtwig legal existente de C # en un progtwig roto.

Por lo tanto, la regla debería ser muy complicada: esencialmente que los paréntesis son solo opcionales en los casos en que no introducen ambigüedades. Tendríamos que analizar todos los casos posibles que introducen ambigüedades y luego escribir código en el comstackdor para detectarlos.

En ese sentido, retroceda y mire todos los costos que menciono. ¿Cuántos de ellos ahora se vuelven grandes? Las reglas complicadas tienen grandes costos de diseño, especificaciones, desarrollo, pruebas y documentación. Es mucho más probable que las reglas complicadas causen problemas con interacciones inesperadas con características en el futuro.

Todo por qué? Un pequeño beneficio para el cliente que no agrega un nuevo poder de representación al lenguaje, pero sí agrega casos de esquinas locas esperando gritar “gotcha” a algún pobre alma desprevenida que se topa con él. Características como esa se cortan inmediatamente y se ponen en la lista “nunca hacer esto”.

¿Cómo determinaste esa ambigüedad particular?

Ese fue inmediatamente claro; Estoy bastante familiarizado con las reglas en C # para determinar cuándo se espera un nombre punteado.

Al considerar una nueva característica, ¿cómo se determina si causa alguna ambigüedad? A mano, por prueba formal, por análisis de máquina, ¿qué?

Los tres. Sobre todo, solo miramos las especificaciones y fideos, como lo hice anteriormente. Por ejemplo, supongamos que quisiéramos agregar un nuevo operador de prefijo a C # llamado “frob”:

 x = frob 123 + 456; 

(ACTUALIZACIÓN: frob está por supuesto frob , el análisis aquí es esencialmente el análisis que el equipo de diseño realizó al agregar await ).

“frob” aquí es como “nuevo” o “++” – viene antes de una expresión de algún tipo. Trabajaríamos con la precedencia y la asociatividad deseadas, y así sucesivamente, y luego comenzaremos a hacer preguntas como “¿y si el progtwig ya tiene un tipo, campo, propiedad, evento, método, constante o local llamado frob?” Eso llevaría inmediatamente a casos como:

 frob x = 10; 

¿eso significa “hacer la operación frob sobre el resultado de x = 10, o crear una variable de tipo frob llamada x y asignarle 10?” (O, si frobbing produce una variable, podría ser una asignación de 10 a frob x . Después de todo, *x = 10; analiza y es legal si x es int* .)

 G(frob + x) 

¿Eso significa “frob el resultado del operador unario plus en x” o “agregar expresión frob a x”?

Y así. Para resolver estas ambigüedades, podríamos introducir la heurística. Cuando dices “var x = 10;” eso es ambiguo; podría significar “inferir el tipo de x” o podría significar “x es de tipo var”. Entonces tenemos una heurística: primero intentamos buscar un tipo llamado var, y solo si no existe, inferimos el tipo de x.

O bien, podríamos cambiar la syntax para que no sea ambigua. Cuando diseñaron C # 2.0 tuvieron este problema:

 yield(x); 

¿Eso significa “rendimiento x en un iterador” o “llamar al método de rendimiento con argumento x?” Al cambiarlo a

 yield return(x); 

ahora es inequívoco.

En el caso de parens opcionales en un inicializador de objetos, es sencillo razonar si hay ambigüedades introducidas o no, porque el número de situaciones en las que es permisible introducir algo que comienza con {es muy pequeño . Básicamente solo varios contextos de statement, statement lambdas, inicializadores de matriz y eso es todo. Es fácil razonar a través de todos los casos y mostrar que no hay ambigüedad. Asegurarse de que el IDE se mantenga eficiente es algo más difícil, pero se puede hacer sin demasiados problemas.

Este tipo de juguetear con las especificaciones por lo general es suficiente. Si es una característica particularmente complicada, sacamos herramientas más pesadas. Por ejemplo, al diseñar LINQ, uno de los chicos del comstackdor y uno de los chicos del IDE que tienen experiencia en teoría del analizador construyeron un analizador sintáctico que podía analizar gramáticas en busca de ambigüedades, y luego introdujeron las gramáticas propuestas de C # para la comprensión de las consultas. ; al hacerlo, encontró muchos casos en los que las consultas eran ambiguas.

O bien, cuando hicimos una inferencia tipo avanzada sobre lambdas en C # 3.0, redactamos nuestras propuestas y luego las enviamos a Microsoft Research en Cambridge, donde el equipo de idiomas allí era lo suficientemente bueno como para elaborar una prueba formal de que la propuesta de inferencia de tipo era teóricamente sano.

¿Hay ambigüedades en C # hoy?

Por supuesto.

 G(F(0)) 

En C # 1 está claro lo que eso significa. Es lo mismo que:

 G( (F0) ) 

Es decir, llama a G con dos argumentos que son bools. En C # 2, eso podría significar lo que significaba en C # 1, pero también podría significar “pasar 0 al método genérico F que toma los parámetros de tipo A y B, y luego pasar el resultado de F a G”. Agregamos una heurística complicada al analizador que determina cuál de los dos casos probablemente quiso decir.

Del mismo modo, los lanzamientos son ambiguos incluso en C # 1.0:

 G((T)-x) 

¿Eso es “cast -x a T” o “resta x de T”? De nuevo, tenemos una heurística que hace una buena suposición.

Porque así es como se especificó el idioma. No agregan ningún valor, entonces, ¿por qué incluirlos?

También es muy similar a las matrices implícitamente tipadas

 var a = new[] { 1, 10, 100, 1000 }; // int[] var b = new[] { 1, 1.5, 2, 2.5 }; // double[] var c = new[] { "hello", null, "world" }; // string[] var d = new[] { 1, "one", 2, "two" }; // Error 

Referencia: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

Esto fue hecho para simplificar la construcción de objetos. Los diseñadores de idiomas no han dicho (por lo que sé) específicamente por qué pensaron que esto era útil, aunque se menciona explícitamente en la página de especificaciones de C # Versión 3.0 :

Una expresión de creación de objetos puede omitir la lista de argumentos del constructor y adjuntar paréntesis, siempre que incluya un objeto o un inicializador de colección. Omitir la lista de argumentos del constructor y adjuntar paréntesis equivale a especificar una lista de argumentos vacía.

Supongo que sintieron que el paréntesis, en este caso, no era necesario para mostrar la intención del desarrollador, ya que el inicializador de objetos muestra la intención de construir y establecer las propiedades del objeto.

En su primer ejemplo, el comstackdor deduce que está llamando al constructor predeterminado (la especificación del lenguaje C # 3.0 establece que si no se proporcionan paréntesis, se llama al constructor predeterminado).

En el segundo, llama explícitamente al constructor predeterminado.

También puede usar esa syntax para establecer propiedades mientras pasa valores explícitamente al constructor. Si tuviera la siguiente definición de clase:

 public class SomeTest { public string Value { get; private set; } public string AnotherValue { get; set; } public string YetAnotherValue { get; set;} public SomeTest() { } public SomeTest(string value) { Value = value; } } 

Las tres declaraciones son válidas:

 var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" }; var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"}; var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"}; 

No soy Eric Lippert, así que no puedo decirlo con certeza, pero supongo que es porque el comstackdor no necesita el paréntesis vacío para inferir el constructo de inicialización. Por lo tanto, se convierte en información redundante y no necesaria.