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


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

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

A diferencia de:

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

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

Author: James Dunne, 2010-09-07

5 answers

Esta pregunta fue el tema de mi blog el 20 de septiembre de 2010 . Las respuestas de Josh y Chad ("no agregan valor, así que ¿por qué requerirlas?"y" eliminar la redundancia") son básicamente correctas. Para darle más detalles:

La característica de permitirle eludir la lista de argumentos como parte de la "característica más grande" de los inicializadores de objetos se encontró con nuestra barra de características "azucaradas". Algunos puntos que consideramos:

  • el costo de diseño y especificación fue bajo
  • nosotros de todos modos, el costo adicional de desarrollo 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 pruebas fue relativamente pequeña en comparación con el costo de la característica más grande
  • la carga de documentación fue relativamente pequeña en comparación...
  • se anticipó que la carga de mantenimiento sería pequeña; no recuerdo ningún error reportado en esta característica en los años desde que se envió.
  • la característica no plantea ningún riesgo inmediatamente obvio para las características futuras en esta área. (Lo último que queremos hacer es hacer una función barata y fácil ahora que hace que sea mucho más difícil implementar 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 se realiza por el IDE "IntelliSense" motor mientras está escribiendo. Y así sucesivamente.
  • la entidad alcanza un "punto dulce" común para la entidad de inicialización de objetos más grande; normalmente, si está utilizando un inicializador de objetos es precisamente porque el constructor del objeto hace 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 no hiciste también vacío paréntesis opcional 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 esa lista de criterios anterior. Una de ellas es que el cambio no introduce ninguna nueva ambigüedad en el análisis léxico, gramatical o semántico de un programa. 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 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 B. M y llama a su constructor predeterminado. Si los paréntesis en la línea 1 fueran opcionales entonces la línea 2 sería ambigua. Entonces tendríamos que llegar a una regla que resuelva la ambigüedad; no podríamos convertirlo en un error porque eso sería un cambio radical que cambia un programa legal existente de C# en un programa roto.

Por lo tanto, la regla tendría que 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 introduzcan ambigüedades y luego escribir código en el compilador para detectarlas.

En esa luz, regresa y mira todos los costos que menciono. ¿Cuántos de ellos ahora se hacen grandes? Las reglas complicadas tienen grandes costos de diseño, especificaciones, desarrollo, pruebas y documentación. Reglas complicadas son mucho más propensos a causar problemas con inesperados interacciones con características en el futuro.

Todo para qué? Un pequeño beneficio para el cliente que no agrega ningún nuevo poder de representación al lenguaje, pero agrega casos de esquina locos esperando para gritar "te tengo" a algún pobre alma desprevenida que se encuentra con él. Características como esa se cortan inmediatamente y se ponen en la lista "nunca hagas esto".

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

Esa fue inmediatamente clara; estoy bastante familiarizado con las reglas en 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, por pruebas formales, por análisis de máquinas, ¿qué?

Los tres. Sobre todo nos limitamos a mirar la especificación y fideos en él, como hice anteriormente. Por ejemplo, supongamos que queremos agregar un nuevo operador de prefijo a C# llamado "frob":

x = frob 123 + 456;

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

"frob" aquí es como "new" o "++" - viene antes de una expresión de algún tipo. Trabajaríamos la precedencia deseada y la asociatividad y así sucesivamente, y luego comenzaríamos a hacer preguntas como "¿qué pasa si el programa 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 en 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í sucesivamente. 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 significa "x es de tipo var". Así que 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 de rendimiento con el argumento x?"Al cambiarlo a

yield return(x);

Ahora es inequívoco.

En el caso de los paréntesis facultativos 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 declaración, lambdas de declaración, inicializadores de matriz y eso es todo. Es fácil razonar a través de 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 demasiado muchos problemas.

Este tipo de jugueteo con la especificación por lo general es suficiente. Si es una característica particularmente difícil, sacamos herramientas más pesadas. Por ejemplo, al diseñar LINQ, uno de los chicos del compilador y uno de los chicos del IDE que tienen experiencia en la teoría del analizador se construyeron un generador de analizador que podría analizar gramáticas en busca de ambigüedades, y luego alimentaron gramáticas propuestas de C# para la comprensión de consultas; al hacerlo, encontró muchos casos donde las consultas ambiguo.

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

¿Hay ambigüedades en C# hoy en día?

Claro.

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

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

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

Que es decir, llama a G con dos argumentos que son bolos. 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 se refería.

De manera similar, los moldes son ambiguos incluso en C# 1.0:

G((T)-x)

¿Es "cast-x a T" o "restar x de T"? Una vez más, tenemos una heurística que hace una buena suposición.

 135
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-22 14:10:20

Porque así es como se especificó el lenguaje. No añaden ningún valor, así que ¿por qué incluirlos?

También es muy similar a los arrays de tipo implicity

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

 10
Author: CaffGeek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-27 15:39:06

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

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

Supongo que sintieron que los paréntesis, en este caso, no eran necesarios para mostrar la intención del desarrollador, ya que el inicializador de objetos muestra la intención de construir y establece las propiedades del objeto en su lugar.

 7
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-07 17:25:58

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

En el segundo, se llama 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 = "!"};
 4
Author: Justin Niessner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-07 17:26:18

No soy Eric Lippert, así que no puedo asegurarlo, pero asumiría 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 es necesario.

 1
Author: Josh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-22 14:12:59