Différence entre C # et l'opérateur ternaire de Java (? :)

94

Je suis un débutant en C # et je viens de rencontrer un problème. Il y a une différence entre C # et Java lorsqu'il s'agit de l'opérateur ternaire ( ? :).

Dans le segment de code suivant, pourquoi la 4ème ligne ne fonctionne-t-elle pas? Le compilateur affiche un message d'erreur de there is no implicit conversion between 'int' and 'string'. La 5ème ligne ne fonctionne pas aussi bien. Les deux Listsont des objets, n'est-ce pas?

int two = 2;
double six = 6.0;
Write(two > six ? two : six); //param: double
Write(two > six ? two : "6"); //param: not object
Write(two > six ? new List<int>() : new List<string>()); //param: not object

Cependant, le même code fonctionne en Java:

int two = 2;
double six = 6.0;
System.out.println(two > six ? two : six); //param: double
System.out.println(two > six ? two : "6"); //param: Object
System.out.println(two > six ? new ArrayList<Integer>()
                   : new ArrayList<String>()); //param: Object

Quelle fonctionnalité de langage en C # est manquante? Le cas échéant, pourquoi n'est-il pas ajouté?

noirr1234
la source
4
Selon msdn.microsoft.com/en-us/library/ty67wk28.aspx "Soit le type de first_expression et second_expression doit être le même, soit une conversion implicite doit exister d'un type à l'autre."
Egor
2
Je n'ai pas fait C # depuis un moment, mais le message que vous citez ne le dit-il pas? Les deux branches du ternaire doivent renvoyer le même type de données. Vous lui donnez un int et une chaîne. C # ne sait pas comment convertir automatiquement un int en String. Vous devez y mettre une conversion de type explicite.
Jay
2
@ blackr1234 Parce que an intn'est pas un objet. C'est un primitif.
Code-Apprentice
3
@ Code-Apprentice oui, ils sont vraiment très différents - C # a une réification à l'exécution, donc les listes sont de types non liés. Oh, et vous pouvez également ajouter une covariance / contravariance d'interface générique si vous le souhaitez, pour introduire un certain niveau de relation;)
Lucas Trzesniewski
3
Pour le bénéfice de l'OP: l'effacement de type en Java signifie cela ArrayList<String>et ArrayList<Integer>devient juste ArrayListdans le bytecode. Cela signifie qu'ils semblent être exactement du même type au moment de l'exécution. Apparemment, en C #, ils sont de types différents.
Code-Apprentice

Réponses:

106

En parcourant la section 7.14 de la spécification du langage C # 5: Opérateur conditionnel, nous pouvons voir ce qui suit:

  • Si x a le type X et y le type Y alors

    • S'il existe une conversion implicite (§6.1) de X en Y, mais pas de Y en X, alors Y est le type de l'expression conditionnelle.

    • S'il existe une conversion implicite (§6.1) de Y en X, mais pas de X en Y, alors X est le type de l'expression conditionnelle.

    • Sinon, aucun type d'expression ne peut être déterminé et une erreur de compilation se produit

En d' autres termes: il essaie de trouver si oui ou non x et y peuvent être convertis en eachother et sinon, une erreur de compilation se produit. Dans notre cas intet stringn'ont pas de conversion explicite ou implicite donc il ne se compilera pas.

Comparez cela avec la section 15.25 de la spécification du langage Java 7: Opérateur conditionnel :

  • Si les deuxième et troisième opérandes ont le même type (qui peut être le type nul), alors c'est le type de l'expression conditionnelle. ( NON )
  • Si l'un des deuxième et troisième opérandes est de type primitif T, et que le type de l'autre est le résultat de l'application de la conversion boxing (§5.1.7) en T, alors le type de l'expression conditionnelle est T. ( NON )
  • Si l'un des deuxième et troisième opérandes est de type nul et que le type de l'autre est un type référence, le type de l'expression conditionnelle est ce type référence. ( NON )
  • Sinon, si les deuxième et troisième opérandes ont des types convertibles (§5.1.8) en types numériques, alors il y a plusieurs cas: ( NON )
  • Sinon, les deuxième et troisième opérandes sont respectivement de types S1 et S2. Soit T1 le type qui résulte de l'application de la conversion de boxe à S1, et soit T2 le type qui résulte de l'application de la conversion de boxe à S2.
    Le type de l'expression conditionnelle est le résultat de l'application de la conversion de capture (§5.1.10) en lub (T1, T2) (§15.12.2.7). ( OUI )

Et, en regardant la section 15.12.2.7. Inférence d'arguments de type basés sur des arguments réels, nous pouvons voir qu'il essaie de trouver un ancêtre commun qui servira de type utilisé pour l'appel avec lequel il aboutitObject . Object est un argument acceptable pour que l'appel fonctionne.

Jeroen Vannevel
la source
1
J'ai lu la spécification C # comme disant qu'il doit y avoir une conversion implicite dans au moins une direction. L'erreur du compilateur se produit uniquement lorsqu'il n'y a pas de conversion implicite dans les deux sens.
Code-Apprentice
1
Bien sûr, c'est toujours la réponse la plus complète puisque vous citez directement à partir du cahier des charges.
Code-Apprentice
Cela n'a en fait été changé qu'avec Java 5 (je pense que cela aurait pu être 6). Avant cela, Java avait exactement le même comportement que C #. En raison de la façon dont les énumérations sont implémentées, cela a probablement causé encore plus de surprises en Java qu'en C #, donc un bon changement tout au long.
Voo le
@Voo: FYI, Java 5 et Java 6 ont la même version de spécification ( La spécification du langage Java , troisième édition), donc cela n'a certainement pas changé dans Java 6.
ruakh
86

Les réponses données sont bonnes; J'ajouterais que cette règle de C # est une conséquence d'une directive de conception plus générale. Lorsqu'on lui a demandé de déduire le type d'une expression à partir de l'un des nombreux choix, C # choisit le meilleur d'entre eux . Autrement dit, si vous donnez à C # des choix tels que "Girafe, Mammifère, Animal", il peut choisir le plus général - Animal - ou il peut choisir le plus spécifique - Girafe - selon les circonstances. Mais il doit choisir l' un des choix qui lui a été fait . C # ne dit jamais "mes choix sont entre le chat et le chien, donc je vais en déduire que l'animal est le meilleur choix". Ce n'était pas un choix donné, donc C # ne peut pas le choisir.

Dans le cas de l'opérateur ternaire, C # essaie de choisir le type le plus général d'int et de chaîne, mais le type le plus général n'est pas non plus. Plutôt que de choisir un type qui n'était pas un choix en premier lieu, comme object, C # décide qu'aucun type ne peut être déduit.

Je note également que cela est conforme à un autre principe de conception de C #: si quelque chose ne va pas, dites-le au développeur. Le langage ne dit pas "Je vais deviner ce que vous vouliez dire et me débrouiller si je peux". Le langage dit "Je pense que vous avez écrit quelque chose de déroutant ici, et je vais vous en parler."

De plus, je note que C # ne raisonne pas de la variable à la valeur assignée , mais plutôt dans l'autre sens. C # ne dit pas "vous assignez à une variable objet donc l'expression doit être convertible en objet, donc je vais m'assurer que c'est". Au contraire, C # dit "cette expression doit avoir un type, et je dois être capable de déduire que le type est compatible avec object". Comme l'expression n'a pas de type, une erreur est générée.

Eric Lippert
la source
6
J'ai dépassé le premier paragraphe en me demandant pourquoi l'intérêt soudain de covariance / contravariance, puis j'ai commencé à être plus d'accord sur le deuxième paragraphe, puis j'ai vu qui avait écrit la réponse ... J'aurais dû deviner plus tôt. Avez-vous déjà remarqué à quel point vous utilisez «Girafe» comme exemple ? Vous devez vraiment aimer les girafes.
Pharap
21
Les girafes sont géniales!
Eric Lippert
9
@Pharap: Sérieusement, il y a des raisons pédagogiques pour lesquelles j'utilise autant la girafe. Tout d'abord, les girafes sont bien connues dans le monde entier. Deuxièmement, c'est une sorte de mot amusant à dire, et ils sont si étranges que c'est une image mémorable. Troisièmement, l'utilisation, disons, de «girafe» et de «tortue» dans la même analogie souligne fortement «ces deux classes, bien qu'elles puissent partager une classe de base, sont des animaux très différents avec des caractéristiques très différentes». Les questions sur les systèmes de types ont souvent des exemples où les types sont logiquement très similaires, ce qui entraîne votre intuition dans le mauvais sens.
Eric Lippert le
7
@Pharap: Autrement dit, la question est généralement quelque chose comme "pourquoi une liste d'usines de fournisseurs de factures clients ne peut-elle pas être utilisée comme une liste d'usines de fournisseurs de factures?" et votre intuition dit "ouais, c'est fondamentalement la même chose", mais quand vous dites "pourquoi une pièce remplie de girafes ne peut-elle pas être utilisée comme une pièce pleine de mammifères?" alors cela devient beaucoup plus une analogie intuitive de dire "parce qu'une pièce pleine de mammifères peut contenir un tigre, une pièce pleine de girafes ne peut pas".
Eric Lippert le
6
@Eric J'ai rencontré ce problème à l'origine avec int? b = (a != 0 ? a : (int?) null). Il y a aussi cette question , ainsi que toutes les questions liées sur le côté. Si vous continuez à suivre les liens, il y en a plusieurs. En comparaison, je n'ai jamais entendu parler de quelqu'un qui se heurte à un problème réel avec la façon dont Java le fait.
BlueRaja - Danny Pflughoeft
24

Concernant la partie génériques:

two > six ? new List<int>() : new List<string>()

En C #, le compilateur essaie de convertir les parties d'expression de droite en un type commun; puisque List<int>et List<string>sont deux types construits distincts, l'un ne peut pas être converti en l'autre.

En Java, le compilateur essaie de trouver un supertype commun au lieu de le convertir, donc la compilation du code implique l'utilisation implicite de caractères génériques et l' effacement de type ;

two > six ? new ArrayList<Integer>() : new ArrayList<String>()

a le type de compilation de ArrayList<?> (en fait, cela peut être aussi ArrayList<? extends Serializable>ou ArrayList<? extends Comparable<?>>, selon le contexte d'utilisation, car ils sont tous les deux des supertypes génériques communs) et le type d'exécution de raw ArrayList(puisque c'est le supertype raw commun).

Par exemple (testez-le vous-même) ,

void test( List<?> list ) {
    System.out.println("foo");
}

void test( ArrayList<Integer> list ) { // note: can't use List<Integer> here
                                 // since both test() methods would clash after the erasure
    System.out.println("bar");
}

void test() {
    test( true ? new ArrayList<Object>() : new ArrayList<Object>() ); // foo
    test( true ? new ArrayList<Integer>() : new ArrayList<Object>() ); // foo 
    test( true ? new ArrayList<Integer>() : new ArrayList<Integer>() ); // bar
} // compiler automagically binds the correct generic QED
Mathieu Guindon
la source
En fait, Write(two > six ? new List<object>() : new List<string>());cela ne fonctionne pas non plus.
blackr1234
2
@ blackr1234 exactement: je ne voulais pas répéter ce que les autres réponses ont déjà dit, mais l'expression ternaire doit s'évaluer en un type, et le compilateur n'essaiera pas de trouver le "plus petit dénominateur commun" des deux.
Mathieu Guindon
2
En fait, il ne s'agit pas d'effacement de type, mais de types génériques. Les effacements de ArrayList<String>et ArrayList<Integer>seraient ArrayList(le type brut), mais le type déduit pour cet opérateur ternaire est ArrayList<?>(le type générique). Plus généralement, le fait que l'environnement d' exécution Java implémente des génériques via l'effacement de type n'a aucun effet sur le type au moment de la compilation d'une expression.
meriton
1
@vaxquis merci! Je suis un gars C #, les génériques Java me confondent ;-)
Mathieu Guindon
2
En fait, le type de two > six ? new ArrayList<Integer>() : new ArrayList<String>()est ArrayList<? extends Serializable&Comparable<?>>ce qui signifie que vous pouvez l'affecter à une variable de type ArrayList<? extends Serializable>ainsi qu'à une variable de type ArrayList<? extends Comparable<?>>, mais bien sûr ArrayList<?>, ce qui équivaut à ArrayList<? extends Object>, fonctionne également.
Holger
6

En Java et C # (et dans la plupart des autres langages), le résultat d'une expression a un type. Dans le cas de l'opérateur ternaire, il existe deux sous-expressions possibles évaluées pour le résultat et les deux doivent avoir le même type. Dans le cas de Java, une intvariable peut être convertie en un Integerpar autoboxing. Maintenant, puisque les deux Integeret Stringhéritent de Object, ils peuvent être convertis au même type par une simple conversion de rétrécissement.

Par contre, en C #, an intest une primitive et il n'y a pas de conversion implicite enstring ou autre object.

Code-Apprenti
la source
Merci d'avoir répondu. Je pense actuellement à la boxe automatique. C # n'autorise pas la détection automatique?
blackr1234
2
Je ne pense pas que ce soit correct car la boxing d'un int à un objet est parfaitement possible en C #. La différence ici est, je crois, que C # adopte simplement une approche très décontractée pour déterminer s'il est autorisé ou non.
Jeroen Vannevel le
9
Cela n'a rien à voir avec la boxe automatique, ce qui se produirait tout autant en C #. La différence est que C # n'essaye pas de trouver un supertype commun alors que Java (depuis Java 5 uniquement!) Le fait. Vous pouvez facilement tester cela en essayant de voir ce qui se passe si vous l'essayez avec 2 classes personnalisées (qui héritent toutes deux de l'objet évidemment). Il existe également une conversion implicite de intà object.
Voo le
3
Et pour pinailler un peu plus: la conversion de Integer/Stringen Objectn'est pas une conversion restrictive, c'est exactement le contraire :-)
Voo
1
Integeret Stringaussi implémenter Serializableet Comparabledonc les affectations à l'un ou l'autre fonctionneront également, par exemple Comparable<?> c=condition? 6: "6";ou List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();sont du code Java légal.
Holger le
5

C'est assez simple. Il n'y a pas de conversion implicite entre string et int. l'opérateur ternaire a besoin que les deux derniers opérandes aient le même type.

Essayer:

Write(two > six ? two.ToString() : "6");
ChiralMichael
la source
11
La question est de savoir ce qui fait la différence entre Java et C #. Cela ne répond pas à la question.
Jeroen Vannevel