Pourquoi les parenthèses de constructeur d'initialisation d'objet C # 3.0 sont-elles facultatives?

114

Il semble que la syntaxe de l'initialiseur d'objet C # 3.0 permet d'exclure la paire de parenthèses ouverture / fermeture dans le constructeur lorsqu'il existe un constructeur sans paramètre. Exemple:

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

Par opposition à:

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

Je suis curieux de savoir pourquoi la paire de parenthèses ouvertes / fermées du constructeur est facultative ici après XTypeName?

James Dunne
la source
9
En passant, nous avons trouvé ceci dans une revue de code la semaine dernière: var list = new List <Foo> {}; Si quelque chose peut être abusé ...
blu
@blu C'est l'une des raisons pour lesquelles je voulais poser cette question. J'ai remarqué l'incohérence dans notre code. L'incohérence en général me dérange donc j'ai pensé voir s'il y avait une bonne justification derrière le caractère facultatif de la syntaxe. :)
James Dunne

Réponses:

143

Cette question a fait l'objet de mon blog le 20 septembre 2010 . Les réponses de Josh et Chad ("ils n'ajoutent aucune valeur alors pourquoi les exiger?" Et "pour éliminer la redondance") sont fondamentalement correctes. Pour étoffer un peu plus cela:

La fonctionnalité de vous permettre d'élider la liste d'arguments dans le cadre de la «fonctionnalité plus large» des initialiseurs d'objets a rencontré notre barre pour les fonctionnalités «sucrées». Quelques points que nous avons considérés:

  • le coût de conception et de spécification était faible
  • nous allions changer en profondeur le code de l'analyseur qui gère la création d'objets de toute façon; le coût de développement supplémentaire pour rendre la liste de paramètres facultative n'était pas élevé par rapport au coût de la fonctionnalité plus large
  • la charge de test était relativement faible par rapport au coût de la fonctionnalité plus grande
  • le fardeau de la documentation était relativement faible comparé ...
  • on prévoyait que le fardeau de la maintenance serait faible; Je ne me souviens d'aucun bogue signalé dans cette fonctionnalité dans les années qui ont suivi son expédition.
  • la fonctionnalité ne présente pas de risques immédiatement évidents pour les fonctionnalités futures dans ce domaine. (La dernière chose que nous voulons faire est de créer une fonctionnalité simple et bon marché maintenant qui rendra beaucoup plus difficile la mise en œuvre d'une fonctionnalité plus convaincante à l'avenir.)
  • la fonctionnalité n'ajoute aucune nouvelle ambiguïté à l'analyse lexicale, grammaticale ou sémantique de la langue. Cela ne pose aucun problème pour le genre d'analyse de "programme partiel" qui est effectuée par le moteur "IntelliSense" de l'EDI pendant que vous tapez. Etc.
  • la fonction atteint un "point idéal" commun pour la fonction d'initialisation d'objet plus grand; généralement si vous utilisez un objet est précisément ce initialiseur parce que le constructeur de l'objet ne pas vous permet de définir les propriétés que vous voulez. Il est très courant que de tels objets soient simplement des «sacs de propriétés» qui n'ont pas de paramètres dans le ctor en premier lieu.

Pourquoi alors n'avez-vous pas également rendu les parenthèses vides facultatives dans l'appel de constructeur par défaut d'une expression de création d'objet qui n'a pas d'initialiseur d'objet?

Jetez un autre coup d'œil à cette liste de critères ci-dessus. L'un d'eux est que le changement n'introduit aucune nouvelle ambiguïté dans l'analyse lexicale, grammaticale ou sémantique d'un programme. Votre changement proposé n'introduire une ambiguïté d'analyse sémantique:

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 ligne 1 crée un nouveau C, appelle le constructeur par défaut, puis appelle la méthode d'instance M sur le nouvel objet. La ligne 2 crée une nouvelle instance de BM et appelle son constructeur par défaut. Si les parenthèses sur la ligne 1 étaient facultatives, la ligne 2 serait ambiguë. Il faudrait alors trouver une règle résolvant l'ambiguïté; nous ne pouvions pas en faire une erreur car ce serait alors un changement de rupture qui transforme un programme C # légal existant en un programme cassé.

Par conséquent, la règle devrait être très compliquée: essentiellement que les parenthèses ne sont facultatives que dans les cas où elles n'introduisent pas d'ambiguïtés. Il faudrait analyser tous les cas possibles qui introduisent des ambiguïtés, puis écrire du code dans le compilateur pour les détecter.

Dans cette optique, revenez en arrière et regardez tous les coûts que je mentionne. Combien d'entre eux deviennent maintenant grands? Les règles compliquées ont des coûts de conception, de spécifications, de développement, de test et de documentation importants. Les règles compliquées sont beaucoup plus susceptibles de provoquer des problèmes d'interactions inattendues avec des fonctionnalités à l'avenir.

Tout pour quoi? Un petit avantage pour le client qui n'ajoute aucun nouveau pouvoir de représentation à la langue, mais qui ajoute des cas de coin fous qui n'attendent que de crier "gotcha" à une pauvre âme sans méfiance qui s'y heurte. Des fonctionnalités comme celle-ci sont immédiatement supprimées et inscrites sur la liste «ne jamais faire ça».

Comment avez-vous déterminé cette ambiguïté particulière?

Celui-là était immédiatement clair; Je connais assez bien les règles en C # pour déterminer quand un nom en pointillé est attendu.

Lorsque vous envisagez une nouvelle fonctionnalité, comment déterminez-vous si elle provoque une ambiguïté? À la main, par preuve formelle, par analyse machine, quoi?

Tous les trois. La plupart du temps, nous regardons simplement les spécifications et les nouilles, comme je l'ai fait ci-dessus. Par exemple, supposons que nous voulions ajouter un nouvel opérateur de préfixe à C # appelé "frob":

x = frob 123 + 456;

(MISE À JOUR: frobc'est bien sûr await; l'analyse ici est essentiellement l'analyse que l'équipe de conception a traversée lors de l'ajout await.)

"frob" ici est comme "new" ou "++" - il vient avant une expression quelconque. Nous travaillerions sur la priorité et l'associativité souhaitées, etc., puis nous commencerions à poser des questions telles que "et si le programme avait déjà un type, un champ, une propriété, un événement, une méthode, une constante ou un local appelé frob?" Cela conduirait immédiatement à des cas comme:

frob x = 10;

cela signifie-t-il "faire l'opération frob sur le résultat de x = 10, ou créer une variable de type frob appelée x et lui affecter 10?" (Ou, si le frobbing produit une variable, il peut s'agir d'une affectation de 10 à frob x. Après tout, *x = 10;analyse et est légal si xc'est int*.)

G(frob + x)

Cela signifie-t-il "frob le résultat de l'opérateur unaire plus sur x" ou "ajouter une expression frob à x"?

Etc. Pour résoudre ces ambiguïtés, nous pourrions introduire des heuristiques. Quand vous dites "var x = 10;" c'est ambigu; cela pourrait signifier "inférer le type de x" ou cela pourrait signifier "x est de type var". Nous avons donc une heuristique: nous essayons d'abord de rechercher un type nommé var, et ce n'est que s'il n'en existe pas que nous déduisons le type de x.

Ou, nous pourrions changer la syntaxe pour qu'elle ne soit pas ambiguë. Quand ils ont conçu C # 2.0, ils ont eu ce problème:

yield(x);

Cela signifie-t-il "yield x dans un itérateur" ou "appeler la méthode yield avec l'argument x?" En le changeant en

yield return(x);

il est désormais sans ambiguïté.

Dans le cas de parens optionnelles dans un initialiseur d'objet, il est facile de raisonner pour savoir s'il y a des ambiguïtés introduites ou non car le nombre de situations dans lesquelles il est permis d'introduire quelque chose qui commence par {est très petit . Fondamentalement, juste divers contextes d'instruction, lambdas d'instruction, initialiseurs de tableau et c'est à peu près tout. Il est facile de raisonner à travers tous les cas et de montrer qu'il n'y a pas d'ambiguïté. S'assurer que l'EDI reste efficace est un peu plus difficile mais peut être fait sans trop de problèmes.

Ce genre de bidouillage avec les spécifications est généralement suffisant. Si c'est une fonctionnalité particulièrement délicate, nous sortons des outils plus lourds. Par exemple, lors de la conception de LINQ, l'un des gars du compilateur et l'un des gars de l'EDI qui ont tous deux une formation en théorie des parseurs se sont construits un générateur d'analyseurs qui pourrait analyser les grammaires à la recherche d'ambiguïtés, puis ont alimenté les grammaires C # proposées pour la compréhension des requêtes. ; cela a permis de trouver de nombreux cas où les requêtes étaient ambiguës.

Ou, lorsque nous avons fait une inférence de type avancée sur les lambdas en C # 3.0, nous avons rédigé nos propositions, puis nous les avons envoyées au-dessus de l'étang à Microsoft Research à Cambridge où l'équipe des langues était suffisamment bonne pour élaborer une preuve formelle que la proposition d'inférence de type était théoriquement solide.

Y a-t-il des ambiguïtés dans C # aujourd'hui?

Sûr.

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

En C # 1, ce que cela signifie est clair. C'est la même chose que:

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

Autrement dit, il appelle G avec deux arguments qui sont des booléens. En C # 2, cela pourrait signifier ce que cela signifiait en C # 1, mais cela pourrait aussi signifier "passer 0 à la méthode générique F qui prend les paramètres de type A et B, puis passer le résultat de F à G". Nous avons ajouté une heuristique compliquée à l'analyseur qui détermine lequel des deux cas vous avez probablement voulu dire.

De même, les casts sont ambigus même en C # 1.0:

G((T)-x)

Est-ce "cast -x to T" ou "soustraire x de T"? Encore une fois, nous avons une heuristique qui fait une bonne estimation.

Eric Lippert
la source
3
Oh désolé, j'ai oublié ... L'approche du signal de chauve-souris, bien qu'elle semble fonctionner, est préférée à (IMO) un moyen de contact direct par lequel on n'obtiendrait pas l'exposition publique voulue pour l'éducation publique sous la forme d'un message SO qui est indexable, consultable et facilement accessible. Devons-nous plutôt contacter directement afin de chorégraphier une mise en scène SO post / answer dance? :)
James Dunne
5
Je vous recommande d'éviter de mettre en scène un message. Ce ne serait pas juste pour les autres qui pourraient avoir un aperçu supplémentaire de la question. Une meilleure approche consisterait à publier la question, puis à envoyer par e-mail un lien demandant la participation.
chilltemp
1
@James: J'ai mis à jour ma réponse pour répondre à votre question de suivi.
Eric Lippert
8
@Eric, est-il possible que vous puissiez bloguer sur cette liste "ne jamais faire ça"? Je suis curieux de voir d'autres exemples qui ne feront jamais partie du langage C # :)
Ilya Ryzhenkov
2
@Eric: J'ai vraiment vraiment apprécié votre patience avec moi :) Merci! Très instructif.
James Dunne
12

Parce que c'est ainsi que la langue a été spécifiée. Ils n'ajoutent aucune valeur, alors pourquoi les inclure?

Il est également très similaire aux tableaux typés par implicité

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

Référence: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
la source
1
Ils n'ajoutent aucune valeur dans le sens où l'intention devrait être évidente, mais cela rompt avec la cohérence en ce que nous avons maintenant deux syntaxes de construction d'objets disparates, une avec des parenthèses requises (et des expressions d'argument délimitées par des virgules) et une sans .
James Dunne
1
@James Dunne, c'est en fait une syntaxe très similaire à la syntaxe de tableau typée implicitement, voir ma modification. Il n'y a pas de type, pas de constructeur et l'intention est évidente, il n'est donc pas nécessaire de le déclarer
CaffGeek
7

Cela a été fait pour simplifier la construction des objets. Les concepteurs de langage n'ont pas (à ma connaissance) expliqué spécifiquement pourquoi ils pensaient que c'était utile, bien que cela soit explicitement mentionné dans la page de spécification C # version 3.0 :

Une expression de création d'objet peut omettre la liste d'arguments du constructeur et les parenthèses, à condition qu'elle inclue un initialiseur d'objet ou de collection. Omettre la liste d'arguments du constructeur et insérer des parenthèses équivaut à spécifier une liste d'arguments vide.

Je suppose qu'ils ont estimé que les parenthèses, dans ce cas, n'étaient pas nécessaires pour montrer l'intention du développeur, car l'initialiseur d'objet montre plutôt l'intention de construire et de définir les propriétés de l'objet.

Reed Copsey
la source
4

Dans votre premier exemple, le compilateur déduit que vous appelez le constructeur par défaut (la spécification du langage C # 3.0 indique que si aucune parenthèse n'est fournie, le constructeur par défaut est appelé).

Dans le second, vous appelez explicitement le constructeur par défaut.

Vous pouvez également utiliser cette syntaxe pour définir des propriétés tout en transmettant explicitement des valeurs au constructeur. Si vous aviez la définition de classe suivante:

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

Les trois déclarations sont valides:

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
la source
Droite. Dans le premier et le second cas de votre exemple, ils sont fonctionnellement identiques, n'est-ce pas?
James Dunne
1
@James Dunne - C'est exact. C'est la partie spécifiée par la spécification de langue. Les parenthèses vides sont redondantes, mais vous pouvez toujours les fournir.
Justin Niessner
1

Je ne suis pas Eric Lippert, donc je ne peux pas le dire avec certitude, mais je suppose que c'est parce que la parenthèse vide n'est pas nécessaire au compilateur pour déduire la construction d'initialisation. Par conséquent, cela devient une information redondante et inutile.

Josh
la source
C'est vrai, c'est redondant, mais je suis juste curieux de savoir pourquoi leur introduction soudaine est facultative? Cela semble rompre avec la cohérence de la syntaxe du langage. Si je n'avais pas l'accolade ouverte pour indiquer un bloc d'initialisation, cela devrait être une syntaxe illégale. C'est drôle que vous mentionniez M. Lippert, je cherchais en quelque sorte publiquement sa réponse pour que les autres et moi-même profitions d'une vaine curiosité. :)
James Dunne