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

114

Parece que a sintaxe do inicializador de objeto C # 3.0 permite excluir o par abrir / fechar de parênteses no construtor quando existe um construtor sem parâmetros. Exemplo:

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

Ao contrário de:

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

Estou curioso para saber por que o par de parênteses de abertura / fechamento do construtor é opcional aqui depois XTypeName?

James Dunne
fonte
9
À parte, descobrimos isso em uma revisão de código na semana passada: var list = new List <Foo> {}; Se algo pode ser abusado ...
blu
@blu Esse é um dos motivos pelos quais eu queria fazer essa pergunta. Percebi a inconsistência em nosso código. Inconsistência em geral me incomoda, então pensei em ver se havia um bom fundamento lógico por trás da opcionalidade na sintaxe. :)
James Dunne

Respostas:

143

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 xfor 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.

Eric Lippert
fonte
3
Oh, desculpe, esqueci ... A abordagem do sinal do morcego, embora pareça funcionar, é preferível (IMO) a um meio de contato direto pelo qual não se obteria a exposição pública desejada para a educação pública na forma de um post SO que é indexável, pesquisável e facilmente consultável. Em vez disso, devemos entrar em contato diretamente para coreografar uma dança pós / resposta encenada do SO? :)
James Dunne
5
Eu recomendo que você evite encenar um post. Isso não seria justo com outras pessoas que possam ter uma visão adicional sobre a questão. Uma abordagem melhor seria postar a pergunta e, em seguida, enviar por e-mail um link pedindo sua participação.
chilltemp
1
@James: Eu atualizei minha resposta para abordar sua pergunta de acompanhamento.
Eric Lippert,
8
@Eric, é possível que você blogue sobre essa lista de "nunca faça isso"? Estou curioso para ver outros exemplos que nunca farão parte da linguagem C # :)
Ilya Ryzhenkov
2
@Eric: Eu realmente realmente realmente agradecemos a sua paciência comigo :) Obrigado! Muito informativo.
James Dunne,
12

Porque é assim que o idioma foi especificado. Eles não agregam 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/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
fonte
1
Eles não agregam valor no sentido de que deveria ser óbvio qual é a intenção, mas quebra a consistência porque agora temos duas sintaxes de construção de objeto díspares, uma com parênteses obrigatórios (e expressões de argumento delimitadas por vírgulas) e outra sem .
James Dunne,
1
@James Dunne, na verdade é uma sintaxe muito semelhante à sintaxe de array tipado implicitamente, veja minha edição. Não há nenhum tipo, nenhum construtor e a intenção é óbvia, portanto, não há necessidade de declará-lo
CaffGeek
7

Isso foi feito para simplificar a construção de objetos. Os designers de linguagem não disseram (que eu saiba) especificamente por que achavam que isso era útil, embora seja explicitamente mencionado na página de especificações do C # versão 3.0 :

Uma expressão de criação de objeto pode omitir a lista de argumentos do construtor e os parênteses, desde que inclua um inicializador de objeto ou 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, neste caso, não eram necessários para mostrar a intenção do desenvolvedor, uma vez que o inicializador de objetos mostra a intenção de construir e definir as propriedades do objeto.

Reed Copsey
fonte
4

Em seu primeiro exemplo, o compilador infere que você está chamando o construtor padrão (a especificação da linguagem C # 3.0 declara 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 sintaxe para definir propriedades enquanto passa explicitamente os valores para o construtor. Se você tivesse a seguinte definição de classe:

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 afirmaçõ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 = "!"};
Justin Niessner
fonte
Certo. No primeiro e no segundo caso em seu exemplo, eles são funcionalmente idênticos, correto?
James Dunne,
1
@James Dunne - correto. Essa é a parte especificada pela especificação do idioma. Parênteses vazios são redundantes, mas você ainda pode fornecê-los.
Justin Niessner
1

Não sou Eric Lippert, então não posso dizer com certeza, mas presumo que seja porque o parêntese vazio não é necessário ao compilador para inferir a construção de inicialização. Portanto, torna-se uma informação redundante e desnecessária.

Josh
fonte
Certo, é redundante, mas estou curioso para saber por que a introdução repentina deles sendo opcional? Parece quebrar a consistência da sintaxe da linguagem. Se eu não tivesse a chave aberta para indicar um bloco inicializador, então esta deveria ser uma sintaxe ilegal. Engraçado você mencionar o Sr. Lippert, eu estava meio que pescando publicamente sua resposta para que eu e outros pudéssemos nos beneficiar de uma mera curiosidade. :)
James Dunne