Diferencia entre C # y el operador ternario de Java (? :)

94

Soy un novato de C # y acabo de encontrar un problema. Existe una diferencia entre C # y Java cuando se trata del operador ternario ( ? :).

En el siguiente segmento de código, ¿por qué no funciona la cuarta línea? El compilador muestra un mensaje de error de there is no implicit conversion between 'int' and 'string'. La quinta línea no funciona tan bien. Ambos Listson objetos, ¿no?

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

Sin embargo, el mismo código funciona 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

¿Qué característica de lenguaje en C # falta? Si existe, ¿por qué no se agrega?

blackr1234
fuente
4
Según msdn.microsoft.com/en-us/library/ty67wk28.aspx "El tipo de first_expression y second_expression deben ser iguales, o debe existir una conversión implícita de un tipo a otro".
Egor
2
No he hecho C # en un tiempo, pero ¿no lo dice el mensaje que cita? Las dos ramas del ternario deben devolver el mismo tipo de datos. Le estás dando un int y un String. C # no sabe cómo convertir automágicamente un int en un String. Debe ponerle una conversión de tipo explícita.
Jay
2
@ blackr1234 Porque an intno es un objeto. Es un primitivo.
Code-Apprentice
3
@ Code-Apprentice, sí, son muy diferentes , C # tiene reificación en tiempo de ejecución, por lo que las listas son de tipos no relacionados. Ah, y también podría
agregar
3
Para el beneficio del OP: el borrado de tipo en Java significa eso ArrayList<String>y se ArrayList<Integer>convierte solo ArrayListen el código de bytes. Esto significa que parecen ser exactamente del mismo tipo en tiempo de ejecución. Aparentemente en C # son de diferentes tipos.
Code-Apprentice

Respuestas:

106

Mirando a través de la sección 7.14 de la Especificación del lenguaje C # 5: Operador condicional , podemos ver lo siguiente:

  • Si x tiene el tipo X e y tiene el tipo Y, entonces

    • Si existe una conversión implícita (§6.1) de X a Y, pero no de Y a X, entonces Y es el tipo de expresión condicional.

    • Si existe una conversión implícita (§6.1) de Y a X, pero no de X a Y, entonces X es el tipo de expresión condicional.

    • De lo contrario, no se puede determinar ningún tipo de expresión y se produce un error en tiempo de compilación.

En otras palabras: trata de encontrar si xey se pueden convertir entre o no y, de no ser así, se produce un error de compilación. En nuestro caso int, y stringno tienen la conversión explícita o implícita por lo que no se compilará.

Compare esto con la sección 15.25 de la Especificación del lenguaje Java 7: Operador condicional :

  • Si el segundo y tercer operandos tienen el mismo tipo (que puede ser el tipo nulo), ese es el tipo de expresión condicional. ( NO )
  • Si uno de los operandos segundo y tercero es de tipo primitivo T, y el tipo del otro es el resultado de aplicar conversión de boxeo (§5.1.7) a T, entonces el tipo de expresión condicional es T. ( NO )
  • Si uno de los operandos segundo y tercero es de tipo nulo y el tipo del otro es un tipo de referencia, entonces el tipo de la expresión condicional es ese tipo de referencia. ( NO )
  • De lo contrario, si el segundo y tercer operandos tienen tipos que son convertibles (§5.1.8) a tipos numéricos, entonces hay varios casos: ( NO )
  • De lo contrario, el segundo y tercer operandos son de los tipos S1 y S2 respectivamente. Sea T1 el tipo que resulta de aplicar la conversión de boxeo a S1, y sea T2 el tipo que resulta de aplicar la conversión de boxeo a S2.
    El tipo de expresión condicional es el resultado de aplicar la conversión de captura (§5.1.10) a lub (T1, T2) (§15.12.2.7). ( SI )

Y, mirando la sección 15.12.2.7. Inferir argumentos de tipo basados ​​en argumentos reales , podemos ver que intenta encontrar un ancestro común que sirva como el tipo utilizado para la llamada con la que aterriza Object. Object es un argumento aceptable para que la llamada funcione.

Jeroen Vannevel
fuente
1
Leí la especificación de C # diciendo que debe haber una conversión implícita en al menos una dirección. El error del compilador ocurre solo cuando no hay una conversión implícita en ninguna dirección.
Code-Apprentice
1
Por supuesto, esta sigue siendo la respuesta más completa, ya que cita directamente las especificaciones.
Code-Apprentice
En realidad, esto solo se cambió con Java 5 (creo que podría haber sido 6). Antes de eso, Java tenía exactamente el mismo comportamiento que C #. Debido a la forma en que se implementan las enumeraciones, esto probablemente causó aún más sorpresas en Java que en C #, aunque fue un buen cambio desde el principio.
Voo
@Voo: FYI, Java 5 y Java 6 tienen la misma versión de especificación ( The Java Language Specification , Third Edition), por lo que definitivamente no cambió en Java 6.
ruakh
86

Las respuestas dadas son buenas; Les agregaría que esta regla de C # es una consecuencia de una guía de diseño más general. Cuando se le pide que infiera el tipo de expresión a partir de una de varias opciones, C # elige la mejor de ellas . Es decir, si le da a C # algunas opciones como "Jirafa, Mamífero, Animal", entonces podría elegir la más general - Animal - o podría elegir la más específica - Jirafa - dependiendo de las circunstancias. Pero debe elegir una de las opciones que realmente se le dieron . C # nunca dice "mis opciones son entre gato y perro, por lo tanto, deduciré que Animal es la mejor opción". Esa no fue una opción dada, por lo que C # no puede elegirla.

En el caso del operador ternario, C # intenta elegir el tipo más general de int y string, pero tampoco el tipo más general. En lugar de elegir un tipo que no era una opción en primer lugar, como object, C # decide que no se puede inferir ningún tipo.

También observo que esto está de acuerdo con otro principio de diseño de C #: si algo parece mal, dígaselo al desarrollador. El lenguaje no dice "Voy a adivinar lo que quisiste decir y seguir adelante si puedo". El lenguaje dice "Creo que has escrito algo confuso aquí, y te lo voy a contar".

Además, observo que C # no razona de la variable de hasta el valor asignado , sino más bien la otra dirección. C # no dice "estás asignando a una variable de objeto, por lo tanto, la expresión debe ser convertible en objeto, por lo tanto, me aseguraré de que lo sea". Más bien, C # dice que "esta expresión debe tener un tipo, y debo poder deducir que el tipo es compatible con el objeto". Dado que la expresión no tiene tipo, se produce un error.

Eric Lippert
fuente
6
Pasé el primer párrafo preguntándome por qué el repentino interés de covarianza / contravarianza, luego comencé a estar más de acuerdo en el segundo párrafo, luego vi quién escribió la respuesta ... Debería haberlo adivinado antes. ¿Alguna vez ha notado exactamente cuánto usa 'Jirafa' como ejemplo ? Realmente te deben gustar las jirafas.
Pharap
21
¡Las jirafas son increíbles!
Eric Lippert
9
@Pharap: Con toda seriedad, hay razones pedagógicas por las que uso tanto la jirafa. En primer lugar, las jirafas son muy conocidas en todo el mundo. En segundo lugar, es una especie de palabra divertida de decir, y tienen un aspecto tan extraño que es una imagen memorable. En tercer lugar, el uso de, digamos, "jirafa" y "tortuga" en la misma analogía enfatiza fuertemente "estas dos clases, aunque pueden compartir una clase básica, son animales muy diferentes con características muy diferentes". Las preguntas sobre sistemas de tipos a menudo tienen ejemplos en los que los tipos son lógicamente muy similares, lo que impulsa su intuición al revés.
Eric Lippert
7
@Pharap: Es decir, la pregunta suele ser algo así como "¿por qué no se puede utilizar una lista de fábricas de proveedores de facturas de clientes como una lista de fábricas de proveedores de facturas?" y tu intuición dice "sí, son básicamente lo mismo", pero cuando dices "¿por qué no se puede usar una habitación llena de jirafas como una habitación llena de mamíferos?" entonces se vuelve mucho más una analogía intuitiva decir "porque una habitación llena de mamíferos puede contener un tigre, una habitación llena de jirafas no puede".
Eric Lippert
6
@Eric Originalmente encontré este problema con int? b = (a != 0 ? a : (int?) null). También está esta pregunta , junto con todas las preguntas vinculadas al lado. Si continúa siguiendo los enlaces, hay bastantes. En comparación, nunca he oído que alguien se encuentre con un problema del mundo real con la forma en que Java lo hace.
BlueRaja - Danny Pflughoeft
24

Respecto a la parte de genéricos:

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

En C #, el compilador intenta convertir las partes de expresión de la derecha a algún tipo común; dado que List<int>y List<string>son dos tipos construidos distintos, uno no se puede convertir en el otro.

En Java, el compilador intenta encontrar un supertipo común en lugar de convertir, por lo que la compilación del código implica el uso implícito de comodines y borrado de tipos ;

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

tiene el tipo de compilación de ArrayList<?>(en realidad, puede ser también ArrayList<? extends Serializable>o ArrayList<? extends Comparable<?>>, dependiendo del contexto de uso, ya que ambos son supertipos genéricos comunes) y el tipo de tiempo de ejecución de raw ArrayList(ya que es el supertipo raw común).

Por ejemplo (pruébelo usted mismo) ,

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
fuente
En realidad Write(two > six ? new List<object>() : new List<string>());tampoco funciona.
blackr1234
2
@ blackr1234 exactamente: no quería repetir lo que ya dijeron otras respuestas, pero la expresión ternaria debe evaluarse a un tipo, y el compilador no intentará encontrar el "mínimo común denominador" de los dos.
Mathieu Guindon
2
En realidad, no se trata de borrar tipos, sino de tipos comodín. Los borrados de ArrayList<String>y ArrayList<Integer>serían ArrayList(el tipo sin formato), pero el tipo inferido para este operador ternario es ArrayList<?>(el tipo comodín). De manera más general, que el tiempo de ejecución de Java implemente genéricos mediante el borrado de tipos no tiene ningún efecto sobre el tipo de tiempo de compilación de una expresión.
meriton
1
@vaxquis gracias! Soy un chico de C #, los genéricos de Java me confunden ;-)
Mathieu Guindon
2
En realidad, el tipo de two > six ? new ArrayList<Integer>() : new ArrayList<String>()es lo ArrayList<? extends Serializable&Comparable<?>>que significa que puede asignarlo a una variable de tipo ArrayList<? extends Serializable>así como a una variable de tipo ArrayList<? extends Comparable<?>>, pero por supuesto ArrayList<?>, lo que es equivalente a ArrayList<? extends Object>, también funciona.
Holger
6

Tanto en Java como en C # (y la mayoría de los otros lenguajes), el resultado de una expresión tiene un tipo. En el caso del operador ternario, existen dos posibles subexpresiones evaluadas para el resultado y ambas deben tener el mismo tipo. En el caso de Java, una intvariable se puede convertir en un Integerautoboxing. Ahora, dado que ambos Integery Stringheredan Object, se pueden convertir al mismo tipo mediante una simple conversión de restricción.

Por otro lado, en C #, an intes una primitiva y no hay conversión implícita a stringni ninguna otra object.

Aprendiz de código
fuente
Gracias por la respuesta. Autoboxing es en lo que estoy pensando actualmente. ¿C # no permite el autoboxing?
blackr1234
2
No creo que esto sea correcto ya que encajonar un int a un objeto es perfectamente posible en C #. La diferencia aquí es, creo, que C # simplemente adopta un enfoque muy relajado para determinar si está permitido o no.
Jeroen Vannevel
9
Esto no tiene nada que ver con el boxeo automático, que sucedería tanto en C #. La diferencia es que C # no intenta encontrar un supertipo común mientras que Java (¡ya que solo Java 5!) Lo hace. Puede probarlo fácilmente tratando de ver qué sucede si lo intenta con 2 clases personalizadas (que obviamente ambas heredan del objeto). También hay una conversión implícita de inta object.
Voo
3
Y para ser un poco más quisquilloso: la conversión de Integer/Stringa Objectno es una conversión de reducción, es exactamente lo contrario :-)
Voo
1
Integery Stringtambién implementar Serializabley Comparableasí las asignaciones a cualquiera de ellos también funcionarán, por ejemplo, Comparable<?> c=condition? 6: "6";o List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();son código Java legal.
Holger
5

Esto es bastante sencillo. No hay conversión implícita entre string e int. el operador ternario necesita que los dos últimos operandos tengan el mismo tipo.

Tratar:

Write(two > six ? two.ToString() : "6");
QuiralMichael
fuente
11
La pregunta es qué causa la diferencia entre Java y C #. Esto no responde a la pregunta.
Jeroen Vannevel