Esta questão foi o assunto do meu blog em 20 de setembro de 2010 . As respostas de Josh e Chad ("eles não agregam valor, então por que exigi-los?" E "para eliminar a redundância") são basicamente corretas. Para aprofundar um pouco mais:
O recurso de permitir que você elimine a lista de argumentos como parte do "recurso maior" dos inicializadores de objeto encontrou nosso limite para recursos "açucarados". Alguns pontos que consideramos:
- o custo de design e especificação era baixo
- iríamos mudar extensivamente o código do analisador que lida com a criação de objetos de qualquer maneira; o custo adicional de desenvolvimento para tornar a lista de parâmetros opcional não era grande em comparação com o custo do recurso maior
- a carga de teste era relativamente pequena em comparação com o custo do recurso maior
- a carga de documentação era relativamente pequena comparada ...
- a carga de manutenção era esperada para ser pequena; Não me lembro de nenhum bug relatado neste recurso nos anos desde que foi lançado.
- o recurso não apresenta nenhum risco imediatamente óbvio para recursos futuros nesta área. (A última coisa que queremos fazer é criar um recurso fácil e barato agora que torne muito mais difícil implementar um recurso mais atraente no futuro.)
- o recurso não adiciona novas ambiguidades à análise lexical, gramatical ou semântica do idioma. Isso não apresenta problemas para o tipo de análise de "programa parcial" que é executada pelo mecanismo "IntelliSense" do IDE enquanto você está digitando. E assim por diante.
- o recurso atinge um "ponto ideal" comum para o recurso de inicialização de objeto maior; normalmente, se você estiver usando um inicializador de objeto, é precisamente porque o construtor do objeto não permite que você defina as propriedades desejadas. É muito comum que esses objetos sejam simplesmente "bolsas de propriedades" que, em primeiro lugar, não têm parâmetros no ctor.
Por que então você não tornou opcionais os parênteses vazios na chamada do construtor padrão de uma expressão de criação de objeto que não tem um inicializador de objeto?
Dê uma outra olhada na lista de critérios acima. Um deles é que a mudança não introduz nenhuma nova ambigüidade na análise lexical, gramatical ou semântica de um programa. Sua mudança proposta faz introduzir uma ambigüidade 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 C.M(); // 2
}
}
A linha 1 cria um novo C, chama o construtor padrão e, a seguir, chama o método de instância M no novo objeto. A linha 2 cria uma nova instância de 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 propor uma regra que resolvesse a ambigüidade; não poderíamos cometer um erro porque isso seria uma alteração significativa que transforma um programa C # válido existente em um programa quebrado.
Portanto, a regra teria que ser muito complicada: essencialmente, os parênteses são opcionais apenas nos casos em que não introduzem 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.
Sob essa luz, volte e veja todos os custos que mencionei. Quantos deles agora se tornam grandes? Regras complicadas têm grandes custos de design, especificação, desenvolvimento, teste e documentação. Regras complicadas têm muito mais probabilidade de causar problemas com interações inesperadas com recursos no futuro.
Tudo para quê? Um pequeno benefício para o cliente que não adiciona nenhum novo poder de representação à linguagem, mas adiciona casos extremos malucos apenas esperando para gritar "peguei você" para alguma pobre alma ingênua que se depara com isso. Recursos como esse são eliminados imediatamente e colocados na lista "nunca faça isso".
Como você determinou essa ambiguidade em particular?
Aquele foi imediatamente claro; Estou bastante familiarizado com as regras em C # para determinar quando um nome pontilhado é esperado.
Ao considerar um novo recurso, como você determina se ele causa alguma ambiguidade? A mão, por prova formal, por análise de máquina, o quê?
Todos três. Na maioria das vezes, apenas olhamos para as especificações e o macarrão, como fiz acima. Por exemplo, suponha que desejamos adicionar um novo operador de prefixo ao C # chamado "frob":
x = frob 123 + 456;
(ATUALIZAÇÃO: frob
é claro await
; a análise aqui é essencialmente a análise que a equipe de design fez ao adicionar await
.)
"frob" aqui é como "novo" ou "++" - vem antes de uma expressão de algum tipo. Trabalharíamos na precedência e associatividade desejadas e assim por diante, e então começarí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 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 é válido se x
for int*
.)
G(frob + x)
Isso significa "frob o resultado do operador mais unário em x" ou "adicionar expressão frob a x"?
E assim por diante. Para resolver essas ambigüidades, podemos introduzir heurísticas. Quando você diz "var x = 10;" isso é ambíguo; pode significar "inferir o tipo de x" ou pode significar "x é do tipo var". Portanto, temos uma heurística: primeiro tentamos pesquisar um tipo denominado var, e apenas se não existir um inferimos o tipo de x.
Ou podemos alterar a sintaxe para que não seja ambígua. Quando projetaram o C # 2.0, eles tiveram este problema:
yield(x);
Isso significa "rendimento x em um iterador" ou "chamar o método de rendimento com o argumento x?" Mudando para
yield return(x);
agora é inequívoco.
No caso de parênteses opcionais em um inicializador de objeto, é direto raciocinar sobre se existem ambigüidades introduzidas ou não porque o número de situações nas quais é permitido introduzir algo que começa com {é muito pequeno . Basicamente, apenas vários contextos de instrução, lambdas de instrução, inicializadores de array e isso é tudo. É fácil raciocinar em todos os casos e mostrar que não há ambigüidade. Garantir que o IDE permaneça eficiente é um pouco mais difícil, mas pode ser feito sem muitos problemas.
Esse tipo de manipulação das especificações geralmente é suficiente. Se for um recurso particularmente complicado, retiramos ferramentas mais pesadas. Por exemplo, ao projetar o LINQ, um dos caras do compilador e um dos caras do IDE, ambos com experiência em teoria do analisador, construíram um gerador de analisador que poderia analisar gramáticas em busca de ambigüidades e, em seguida, alimentou nele as gramáticas C # propostas para compreensão de consultas ; fazendo isso, encontramos muitos casos em que as consultas eram ambíguas.
Ou, quando fizemos inferência de tipo avançada em lambdas em C # 3.0, escrevemos nossas propostas e as enviamos para a Microsoft Research em Cambridge, onde a equipe de idiomas era boa o suficiente para elaborar uma prova formal de que a proposta de inferência de tipo era teoricamente correto.
Existem ambigüidades em C # hoje?
Certo.
G(F<A, B>(0))
Em C # 1, fica claro o que isso significa. É o mesmo que:
G( (F<A), (B>0) )
Ou seja, ele chama G com dois argumentos que são bools. Em C # 2, isso pode significar o que significava em C # 1, mas também poderia significar "passe 0 para o método genérico F que usa os parâmetros de tipo A e B e, em seguida, passe o resultado de F para G". Adicionamos uma heurística complicada ao analisador que determina qual dos dois casos você provavelmente se refere.
Da mesma forma, os casts são ambíguos mesmo em C # 1.0:
G((T)-x)
Isso é "lançar -x em T" ou "subtrair x de T"? Novamente, temos uma heurística que dá uma boa estimativa.