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: frob
c'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 x
c'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.