Por que os parênteses de construtor de inicializador de objects C # 3.0 são opcionais?

Parece que a syntax de inicializador de object C # 3.0 permite excluir um par de parênteses de abertura / fechamento no construtor quando há um construtor sem parâmetros existente. Exemplo:

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

Ao contrário de:

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

Estou curioso porque o par de parênteses de abertura / fechamento do construtor é opcional aqui após XTypeName ?

Esta questão foi o assunto do meu blog em 20 de setembro de 2010 . As respostas de Josh e Chad (“eles não acrescentam nenhum valor, então por que requerê-las?” E “para eliminar a redundância”) estão basicamente corretas. Para detalhar isso um pouco mais:

O recurso de permitir que você elide a lista de argumentos como parte do “recurso maior” de inicializadores de objects encontrou nossa barra para resources “açucarados”. Alguns pontos que consideramos:

  • o custo de design e especificação foi baixo
  • nós estaríamos mudando bastante o código do analisador que lida com a criação de objects de qualquer maneira; o custo adicional de desenvolvimento de tornar a lista de parâmetros opcional não era grande em comparação com o custo do recurso maior
  • a carga de teste foi relativamente pequena em comparação com o custo do recurso maior
  • a carga de documentação foi relativamente pequena em comparação …
  • a carga de manutenção foi antecipada para ser pequena; Não me lembro de nenhum bug relatado neste recurso nos anos desde que foi enviado.
  • o recurso não apresenta riscos óbvios imediatos para resources futuros nessa área. (A última coisa que queremos fazer é criar um recurso fácil e barato agora que dificulte muito mais a implementação de um recurso mais atraente no futuro.)
  • o recurso não adiciona novas ambigüidades à análise léxica, gramatical ou semântica da linguagem. Ele não apresenta problemas para o tipo de análise de “programa parcial” que é executada pelo mecanismo “IntelliSense” do IDE enquanto você digita. E assim por diante.
  • o recurso atinge um “ponto ideal” comum para o recurso de boot de object maior; Normalmente, se você estiver usando um inicializador de object, é precisamente porque o construtor do object não permite que você defina as propriedades desejadas. É muito comum que esses objects sejam simplesmente “sacolas de propriedades” que não têm parâmetros no ctor em primeiro lugar.

Por que, então, você também não tornou parênteses vazios opcionais na chamada de construtor padrão de uma expressão de criação de object que não possui um inicializador de object?

Dê outra olhada nessa lista de critérios acima. Uma delas é que a mudança não introduz qualquer nova ambigüidade na análise léxica, gramatical ou semântica de um programa. Sua mudança proposta introduz uma ambiguidade na análise semântica:

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

A linha 1 cria um novo C, chama o construtor padrão e, em seguida, chama o método de instância M no novo object. A linha 2 cria uma nova instância do BM e chama seu construtor padrão. Se os parênteses na linha 1 fossem opcionais, a linha 2 seria ambígua. Teríamos então que criar uma regra para resolver a ambiguidade; nós não poderíamos torná-lo um erro, porque isso seria então uma mudança que altera um programa C # legal existente em um programa quebrado.

Portanto, a regra teria que ser muito complicada: essencialmente, os parênteses são apenas opcionais nos casos em que não introduzam ambigüidades. Teríamos que analisar todos os casos possíveis que introduzem ambigüidades e, em seguida, escrever código no compilador para detectá-los.

À luz disso, volte e veja todos os custos que menciono. Quantos deles agora se tornam grandes? Regras complicadas têm grandes custos de projeto, especificação, desenvolvimento, testes e documentação. É mais provável que regras complicadas causem problemas com interações inesperadas com resources no futuro.

Tudo por quê? Um minúsculo benefício para o cliente que não agrega nenhum novo poder representacional à linguagem, mas acrescenta casos loucos de esquina apenas esperando para gritar “pegadinha” com uma pobre alma desavisada que a encontra. Recursos como esse são cortados imediatamente e colocados na lista “nunca faça isso”.

Como você determinou essa ambiguidade específica?

Aquela foi imediatamente clara; Eu estou bem familiarizado com as regras em C # para determinar quando um nome pontilhado é esperado.

Ao considerar um novo recurso, como você determina se isso causa alguma ambigüidade? À mão, por prova formal, por análise de máquina, o que?

Todos três. Na maioria das vezes apenas olhamos para o spec e noodle nele, como eu fiz acima. Por exemplo, suponha que desejamos adicionar um novo operador de prefixo ao C # chamado “frob”:

 x = frob 123 + 456; 

(UPDATE: frob é, obviamente, await ; a análise aqui é essencialmente a análise que a equipe de projeto passou ao adicionar o await .)

“frob” aqui é como “novo” ou “++” – vem antes de uma expressão de algum tipo. Trabalhamos com a precedência e associação desejadas e assim por diante, e então começamos a fazer perguntas como “e se o programa já tiver um tipo, campo, propriedade, evento, método, constante ou local chamado frob?” Isso levaria imediatamente a casos como:

 frob x = 10; 

Isso significa “fazer a operação de frob no resultado de x = 10, ou criar uma variável do tipo frob chamada x e atribuir 10 a ela?” (Ou, se frobbing produz uma variável, poderia ser uma atribuição de 10 a frob x . Afinal, *x = 10; analisa e é legal se x é int* .)

 G(frob + x) 

Isso significa “frob o resultado do operador unário mais em x” ou “adicionar expressão frob a x”?

E assim por diante. Para resolver essas ambiguidades, poderíamos introduzir heurísticas. Quando você diz “var x = 10;” isso é ambíguo; poderia significar “inferir o tipo de x” ou poderia significar “x é do tipo var”. Portanto, temos uma heurística: primeiro tentamos procurar um tipo chamado var e, se não existir, inferimos o tipo de x.

Ou podemos mudar a syntax para que ela não seja ambígua. Quando eles projetaram o C # 2.0, eles tiveram esse problema:

 yield(x); 

Isso significa “yield x em um iterador” ou “chame o método yield com argumento x?” Alterando-o para

 yield return(x); 

agora é inequívoco.

No caso de parênteses opcionais em um inicializador de objects, é fácil raciocinar sobre se existem ambigüidades introduzidas ou não, porque o número de situações em que é permitido introduzir algo que começa com {é muito pequeno . Basicamente apenas vários contextos de instrução, instrução lambdas, inicializadores de array e é sobre isso. É fácil raciocinar em todos os casos e mostrar que não há ambiguidade. Certificar-se de que o IDE permaneça eficiente é um pouco mais difícil, mas pode ser feito sem muitos problemas.

Este tipo de mexer com a especificação geralmente é suficiente. Se for um recurso particularmente complicado, nós retiramos ferramentas mais pesadas. Por exemplo, ao projetar LINQ, um dos caras do compilador e um dos caras do IDE que possuem experiência na teoria do analisador construíram um gerador de analisador que poderia analisar gramáticas procurando ambigüidades e, em seguida, alimentou as gramáticas C # propostas para as compreensões de consulta ; Fazendo isso, encontramos muitos casos em que as consultas eram ambíguas.

Ou, quando fizemos inferência de tipo avançada sobre lambdas em C # 3.0, escrevemos nossas propostas e as enviamos para o Microsoft Research em Cambridge, onde a equipe de idiomas era boa o suficiente para provar formalmente que a proposta de inferência de tipos era teoricamente som.

Existem ambiguidades em c # hoje?

Certo.

 G(F(0)) 

Em C # 1, fica claro o que isso significa. É o mesmo que:

 G( (F0) ) 

Isto é, chama G com dois argumentos que são bools. Em C # 2, isso poderia significar o que significava em C # 1, mas também poderia significar “passar 0 para o método genérico F, que recebe os parâmetros de tipo A e B e depois passar o resultado de F para G”. Adicionamos uma heurística complicada ao analisador, que determina qual dos dois casos você provavelmente quis dizer.

Da mesma forma, os castings são ambíguos, mesmo no C # 1.0:

 G((T)-x) 

Isso é “casting -x para T” ou “subtrair x de T”? Mais uma vez, temos uma heurística que faz um bom palpite.

Porque é assim que a linguagem foi especificada. Eles não acrescentam nenhum valor, então por que incluí-los?

Também é muito semelhante a matrizes digitadas implicitamente

 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 

Referência: http://msdn.microsoft.com/pt-br/library/ms364047%28VS.80%29.aspx

Isso foi feito para simplificar a construção de objects. Os projetistas de idiomas não disseram (ao meu conhecimento) especificamente por que eles achavam que isso era útil, embora seja explicitamente mencionado na página de Especificação da Versão 3.0 do C # :

Uma expressão de criação de object pode omitir a lista de argumentos do construtor e colocar parênteses, desde que inclua um object ou inicializador de coleção. Omitir a lista de argumentos do construtor e colocar parênteses é equivalente a especificar uma lista de argumentos vazia.

Suponho que eles sentiram que os parênteses, nesse caso, não eram necessários para mostrar a intenção do desenvolvedor, uma vez que o inicializador de object mostra a intenção de construir e definir as propriedades do object.

Em seu primeiro exemplo, o compilador infere que você está chamando o construtor padrão (o C # 3.0 Language Specification informa que, se nenhum parêntese for fornecido, o construtor padrão será chamado).

No segundo, você chama explicitamente o construtor padrão.

Você também pode usar essa syntax para definir propriedades ao passar explicitamente valores para o construtor. Se você tivesse a seguinte definição de class:

 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; } } 

Todas as três declarações são 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 = "!"}; 

Eu não sou Eric Lippert, então não posso dizer com certeza, mas eu diria que é porque o parêntese vazio não é necessário pelo compilador para inferir a construção de boot. Portanto, torna-se informação redundante e não é necessária.