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

114

Parece que la sintaxis del inicializador de objetos C # 3.0 permite excluir el par de paréntesis abrir / cerrar 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 saber por qué el par de paréntesis de apertura / cierre del constructor es opcional aquí después XTypeName.

James Dunne
fuente
9
Aparte, encontramos esto en una revisión de código la semana pasada: var list = new List <Foo> {}; Si se puede abusar de algo ...
blu
@blu Esa es una de las razones por las que quería hacer esta pregunta. Noté la inconsistencia en nuestro código. La inconsistencia en general me molesta, así que pensé en ver si había una buena razón detrás de la opcionalidad en la sintaxis. :)
James Dunne

Respuestas:

143

Esta pregunta fue el tema de mi blog el 20 de septiembre de 2010 . Las respuestas de Josh y Chad ("no añaden valor, entonces, ¿por qué las requieren?" 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 "característica más grande" de los inicializadores de objetos cumplió con nuestro estándar de características "dulces". Algunos puntos que consideramos:

  • el costo de diseño y especificación fue bajo
  • íbamos a cambiar mucho 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 fue 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 era relativamente pequeña en comparación ...
  • se anticipó que la carga de mantenimiento sería pequeña; No recuerdo ningún error informado en esta función en los años desde que se envió.
  • la función no presenta ningún riesgo obvio inmediato para funciones futuras en esta área. (Lo último que queremos hacer es crear una función fácil y barata ahora que dificulte mucho la implementación de una función más atractiva en el futuro).
  • la característica no añade nuevas ambigüedades al análisis léxico, gramatical o semántico de la lengua. No plantea problemas para el tipo de análisis de "programa parcial" que realiza el motor "IntelliSense" del IDE mientras escribe. Y así.
  • la función alcanza un "punto óptimo" común para la función de inicialización de objetos más grandes; normalmente, si está utilizando un inicializador de objeto, 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.

Entonces, ¿por qué no hizo que los paréntesis vacíos también fueran opcionales en la llamada al constructor predeterminada de una expresión de creación de objeto que no tiene un inicializador de objeto?

Eche otro vistazo a la lista de criterios anterior. Uno de ellos es que el cambio no introduce ninguna nueva ambigüedad en el análisis léxico, gramatical o semántico de un programa. Su cambio propuesto hace introducir un análisis semántico ambigüedad:

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 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 de la línea 1 fueran opcionales, la línea 2 sería ambigua. Entonces tendríamos que proponer una regla que resolviera la ambigüedad; no podríamos convertirlo en un error porque entonces sería un cambio importante que cambia un programa C # legal existente en un programa roto.

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

En ese sentido, retroceda y observe 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 funciones 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 agrega casos de esquina locas que solo esperan gritar "te pillé" a alguna pobre alma desprevenida que se encuentre con él. Características como esa se eliminan de inmediato y se incluyen en la lista de "nunca hacer esto".

¿Cómo determinó esa ambigüedad en particular?

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

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

Los tres. En general, solo miramos las especificaciones y los fideos, como hice anteriormente. Por ejemplo, supongamos que quisiéramos agregar un nuevo operador de prefijo a C # llamado "frob":

x = frob 123 + 456;

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

"frob" aquí es como "nuevo" o "++" - viene antes de una expresión de algún tipo. Calcularíamos la precedencia y la asociatividad deseadas, y así sucesivamente, y luego comenzaríamos a hacer preguntas como "¿y si el programa ya tiene un tipo, campo, propiedad, evento, método, constante o local llamado frob?" Eso conducirí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 frotar 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 lo xes int*).

G(frob + x)

¿Eso significa "frob el resultado del operador unario más en x" o "agregar la expresión frob ax"?

Y así. Para resolver estas ambigüedades, podríamos introducir heurísticas. 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 uno no existe inferimos el tipo de x.

O podríamos cambiar la sintaxis 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 rendimiento con el argumento x"? Cambiándolo a

yield return(x);

ahora es inequívoco.

En el caso de parens opcionales en un inicializador de objeto, es sencillo razonar sobre si se introducen ambigüedades o no porque el número de situaciones en las que está permitido introducir algo que comience con {es muy pequeño . Básicamente, solo varios contextos de declaración, lambdas de declaración, inicializadores de matriz y eso es todo. Es fácil razonar todos los casos y demostrar 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 jugueteo con la especificación suele ser suficiente. Si es una característica particularmente complicada, sacamos herramientas más pesadas. Por ejemplo, al diseñar LINQ, uno de los chicos del compilador y uno de los chicos del IDE, ambos con experiencia en la teoría del analizador sintáctico, construyeron ellos mismos un generador de analizadores sintácticos que podría analizar gramáticas en busca de ambigüedades y luego introdujo gramáticas C # propuestas para la comprensión de consultas en él. ; al hacerlo, se encontraron muchos casos en los que las consultas eran ambiguas.

O, cuando hicimos una inferencia de tipo avanzada en lambdas en C # 3.0, escribimos nuestras propuestas y luego las enviamos al estanque a Microsoft Research en Cambridge, donde el equipo de idiomas fue lo suficientemente bueno como para elaborar una prueba formal de que la propuesta de inferencia de tipos era teóricamente sólido.

¿Hay ambigüedades en C # hoy?

Por supuesto.

G(F<A, B>(0))

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

G( (F<A), (B>0) )

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, las conversiones son ambiguas incluso en C # 1.0:

G((T)-x)

¿Eso es "lanzar -x a T" o "restar x de T"? Nuevamente, tenemos una heurística que hace una buena suposición.

Eric Lippert
fuente
3
Oh, lo siento, lo olvidé ... El enfoque de la señal de murciélago, si bien parece funcionar, se prefiere a (IMO) un medio de contacto directo mediante el cual uno no obtendría la exposición pública deseada para la educación pública en forma de una publicación SO que es indexable, se puede buscar y se puede consultar fácilmente. ¿Deberíamos en cambio contactarnos directamente para coreografiar un baile SO post / answer escenificado? :)
James Dunne
5
Te recomiendo que evites organizar una publicación. Eso no sería justo para otros que puedan tener más información sobre la pregunta. Un mejor enfoque sería publicar la pregunta y luego enviar un enlace por correo electrónico solicitando su participación.
chilltemp
1
@James: He actualizado mi respuesta para abordar su pregunta de seguimiento.
Eric Lippert
8
@Eric, ¿es posible que puedas bloguear sobre esta lista de "nunca hagas esto"? Tengo curiosidad por ver otros ejemplos que nunca serán parte del lenguaje C # :)
Ilya Ryzhenkov
2
@Eric: Realmente aprecio mucho tu paciencia conmigo :) ¡Gracias! Muy informativo.
James Dunne
12

Porque así se especificó el idioma. No añaden valor, entonces, ¿por qué incluirlos?

También es muy similar a las matrices de tipo implícito

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

CaffGeek
fuente
1
No agregan ningún valor en el sentido de que debería ser obvio cuál es la intención, pero rompe con la coherencia en el hecho de que ahora tenemos dos sintaxis de construcción de objetos dispares, una con paréntesis requeridos (y expresiones de argumento delimitadas por comas) y otra sin .
James Dunne
1
@James Dunne, en realidad es una sintaxis muy similar a la sintaxis de matriz tipada implícitamente, vea mi edición. No hay ningún tipo, ni constructor, y la intención es obvia, por lo que no es necesario declararlo
CaffGeek
7

Esto se hizo para simplificar la construcción de objetos. Los diseñadores del lenguaje no han dicho específicamente (que yo sepa) por qué creen que esto es útil, aunque se menciona explícitamente en la página de especificaciones de la versión 3.0 de C # :

Una expresión de creación de objeto puede omitir la lista de argumentos del constructor y entre paréntesis, siempre que incluya un inicializador de objeto o colección. Omitir la lista de argumentos del constructor y encerrar 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 del objeto muestra la intención de construir y establecer las propiedades del objeto.

Reed Copsey
fuente
4

En su primer ejemplo, el compilador infiere 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, llamas explícitamente al constructor predeterminado.

También puede usar esa sintaxis 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 = "!"};
Justin Niessner
fuente
Correcto. En el primer y segundo caso de su ejemplo, estos son funcionalmente idénticos, ¿correcto?
James Dunne
1
@James Dunne - Correcto. Esa es la parte especificada por la especificación de idioma. Los paréntesis vacíos son redundantes, pero aún puede proporcionarlos.
Justin Niessner
1

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

Josh
fuente
Correcto, es redundante, pero tengo curiosidad por saber por qué la repentina introducción de ellos es opcional. Parece romper con la coherencia de la sintaxis del lenguaje. Si no tuviera la llave abierta para indicar un bloque inicializador, entonces esta debería ser una sintaxis ilegal. Es curioso que mencione al Sr. Lippert, yo estaba pescando públicamente su respuesta para que yo y otros nos beneficiamos de una curiosidad ociosa. :)
James Dunne