Diferença entre C # e operador ternário de Java (? :)

94

Sou um novato em C # e acabei de encontrar um problema. Há uma diferença entre C # e Java ao lidar com o operador ternário (? : ).

No segmento de código a seguir, por que a 4ª linha não funciona? O compilador mostra uma mensagem de erro de there is no implicit conversion between 'int' and 'string'. A 5ª linha não funciona tão bem. Ambos Listsão objetos, não são?

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

No entanto, o mesmo código funciona em 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

Qual recurso de linguagem está faltando no C #? Se houver, por que não foi adicionado?

blackr1234
fonte
4
De acordo com msdn.microsoft.com/en-us/library/ty67wk28.aspx "O tipo de first_expression e second_expression deve ser o mesmo ou uma conversão implícita deve existir de um tipo para outro."
Egor
2
Não faço C # há algum tempo, mas a mensagem que você cita não diz isso? Os dois ramos do ternário devem retornar o mesmo tipo de dados. Você está dando a ele um int e uma String. C # não sabe como converter automaticamente um int em String. Você precisa colocar uma conversão de tipo explícita nele.
Jay
2
@ blackr1234 Porque an intnão é um objeto. É um primitivo.
Code-Apprentice
3
@ Code-Apprentice sim, eles são muito diferentes, de fato - C # tem reificação de tempo de execução, então as listas são de tipos não relacionados. Ah, e você também pode adicionar covariância / contravariância de interface genérica à mistura se quiser, para introduzir algum nível de relacionamento;)
Lucas Trzesniewski
3
Para benefício do OP: o tipo erasure em Java significa isso ArrayList<String>e ArrayList<Integer>torna - se apenas ArrayListno bytecode. Isso significa que eles parecem ser exatamente do mesmo tipo em tempo de execução. Aparentemente, em C #, eles são de tipos diferentes.
Code-Apprentice

Respostas:

106

Olhando através da seção 7.14 de especificação da linguagem C # 5: Operador condicional , podemos ver o seguinte:

  • Se x tiver o tipo X e y tiver o tipo Y, então

    • Se uma conversão implícita (§6.1) existe de X para Y, mas não de Y para X, então Y é o tipo da expressão condicional.

    • Se uma conversão implícita (§6.1) existe de Y para X, mas não de X para Y, então X é o tipo da expressão condicional.

    • Caso contrário, nenhum tipo de expressão pode ser determinado e ocorre um erro em tempo de compilação

Em outras palavras: ele tenta descobrir se xey podem ou não ser convertidos um para o outro e se não, ocorre um erro de compilação. No nosso caso intestring não tem nenhuma conversão explícita ou implícita por isso não irá compilar.

Compare isso com a seção 15.25 da Especificação da linguagem Java 7: Operador condicional :

  • Se o segundo e o terceiro operandos tiverem o mesmo tipo (que pode ser do tipo nulo), esse é o tipo da expressão condicional. ( NÃO )
  • Se um dos segundo e terceiro operandos for do tipo primitivo T, e o tipo do outro for o resultado da aplicação da conversão de boxing (§5.1.7) para T, então o tipo da expressão condicional é T. ( NÃO )
  • Se um dos segundo e terceiro operandos for do tipo nulo e o tipo do outro for um tipo de referência, então o tipo da expressão condicional é esse tipo de referência. ( NÃO )
  • Caso contrário, se o segundo e o terceiro operandos têm tipos que são conversíveis (§5.1.8) para tipos numéricos, então há vários casos: ( NÃO )
  • Caso contrário, o segundo e o terceiro operandos são dos tipos S1 e S2, respectivamente. Seja T1 o tipo que resulta da aplicação da conversão de boxe em S1 e seja T2 o tipo que resulta da aplicação de conversão de boxe em S2.
    O tipo da expressão condicional é o resultado da aplicação da conversão de captura (§5.1.10) para lub (T1, T2) (§15.12.2.7). ( SIM )

E, olhando a seção 15.12.2.7. Inferindo argumentos de tipo com base em argumentos reais , podemos ver que ele tenta encontrar um ancestral comum que servirá como o tipo usado para a chamada que o faz chegar Object. Object é um argumento aceitável para que a chamada funcione.

Jeroen Vannevel
fonte
1
Eu li a especificação C # dizendo que deve haver uma conversão implícita em pelo menos uma direção. O erro do compilador ocorre apenas quando não há conversão implícita em nenhuma direção.
Code-Apprentice
1
Claro, esta ainda é a resposta mais completa, já que você cita diretamente das especificações.
Code-Apprentice
Na verdade, isso só foi alterado com o Java 5 (acho que pode ter sido 6). Antes disso, o Java tinha exatamente o mesmo comportamento do C #. Devido à maneira como os enums são implementados, isso provavelmente causou ainda mais surpresas em Java do que em C #, embora tenha sido uma boa mudança o tempo todo.
Voo de
@Voo: FYI, Java 5 e Java 6 têm a mesma versão de especificação ( The Java Language Specification , Terceira edição), então definitivamente não mudou em Java 6.
ruakh
86

As respostas dadas são boas; Eu acrescentaria a eles que essa regra do C # é uma consequência de uma diretriz de design mais geral. Quando solicitado a inferir o tipo de uma expressão a partir de uma das várias opções, o C # escolhe a melhor delas . Ou seja, se você der ao C # algumas opções como "Girafa, Mamífero, Animal", ele pode escolher a mais geral - Animal - ou pode escolher a mais específica - Girafa - dependendo das circunstâncias. Mas ele deve escolher uma das escolhas que realmente foi dado . C # nunca diz "minhas escolhas são entre gato e cachorro, portanto, vou deduzir que Animal é a melhor escolha". Essa não foi uma escolha dada, então C # não pode escolhê-la.

No caso do operador ternário, C # tenta escolher o tipo mais geral de int e string, mas nenhum é o tipo mais geral. Em vez de escolher um tipo que não era uma escolha em primeiro lugar, como objeto, C # decide que nenhum tipo pode ser inferido.

Também observo que isso está de acordo com outro princípio de design do C #: se algo parecer errado, diga ao desenvolvedor. A linguagem não diz "Vou adivinhar o que você quis dizer e continuar confuso, se puder". A linguagem diz: "Acho que você escreveu algo confuso aqui e vou falar sobre isso".

Além disso, observo que C # não raciocina da variável para o valor atribuído , mas sim a outra direção. C # não diz "você está atribuindo a uma variável de objeto, portanto, a expressão deve ser conversível em objeto, portanto, certificarei que seja". Em vez disso, C # diz "esta expressão deve ter um tipo e devo ser capaz de deduzir que o tipo é compatível com o objeto". Como a expressão não possui um tipo, é produzido um erro.

Eric Lippert
fonte
6
Passei do primeiro parágrafo me perguntando por que o súbito interesse de covariância / contravariância, aí comecei a concordar mais no segundo parágrafo, aí vi quem escreveu a resposta ... Deveria ter adivinhado antes. Você já notou exatamente o quanto você usa 'Giraffe' como exemplo ? Você deve realmente gostar de girafas.
Pharap
21
As girafas são fantásticas!
Eric Lippert
9
@Pharap: Com toda a seriedade, há razões pedagógicas pelas quais eu uso tanto a girafa. Em primeiro lugar, as girafas são conhecidas em todo o mundo. Em segundo lugar, é uma palavra divertida de dizer, e eles têm uma aparência tão estranha que é uma imagem memorável. Terceiro, usar, digamos, "girafa" e "tartaruga" na mesma analogia enfatiza fortemente "essas duas classes, embora possam compartilhar uma classe base, são animais muito diferentes com características muito diferentes". Perguntas sobre sistemas de tipos geralmente têm exemplos em que os tipos são logicamente muito semelhantes, o que direciona sua intuição para o lado errado.
Eric Lippert
7
@Pharap: Ou seja, a questão geralmente é algo como "por que uma lista de fábricas fornecedoras de faturas não pode ser usada como uma lista de fábricas fornecedoras de faturas?" e sua intuição diz "sim, são basicamente a mesma coisa", mas quando você diz "por que uma sala cheia de girafas não pode ser usada como uma sala cheia de mamíferos?" então torna-se uma analogia muito mais intuitiva dizer "porque uma sala cheia de mamíferos pode conter um tigre, uma sala cheia de girafas não pode".
Eric Lippert
6
@Eric, originalmente, tive esse problema int? b = (a != 0 ? a : (int?) null). Há também essa pergunta , junto com todas as perguntas vinculadas ao lado. Se você continuar seguindo os links, há alguns. Em comparação, nunca ouvi falar de alguém enfrentando um problema do mundo real com a maneira como o Java o faz.
BlueRaja - Danny Pflughoeft
24

Em relação à parte dos genéricos:

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

No C #, o compilador tenta converter as partes da expressão à direita em algum tipo comum; desde List<int>eList<string> são dois tipos construídos distintos, um não pode ser convertido no outro.

Em Java, o compilador tenta encontrar um supertipo comum em vez de converter, portanto, a compilação do código envolve o uso implícito de curingas e eliminação de tipo ;

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

tem o tipo de compilação de ArrayList<?>(na verdade, pode ser também ArrayList<? extends Serializable>ou ArrayList<? extends Comparable<?>>, dependendo do contexto de uso, uma vez que ambos são supertipos genéricos comuns) e o tipo de tempo de execução bruto ArrayList(já que é o supertipo bruto comum).

Por exemplo (teste você mesmo) ,

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
fonte
Na verdade, Write(two > six ? new List<object>() : new List<string>());também não funciona.
blackr1234
2
@ blackr1234 exatamente: eu não queria repetir o que outras respostas já disseram, mas a expressão ternária precisa ser avaliada como um tipo, e o compilador não tentará encontrar o "mínimo denominador comum" dos dois.
Mathieu Guindon
2
Na verdade, não se trata de eliminação de tipo, mas de tipos curinga. Os apagamentos de ArrayList<String>e ArrayList<Integer>seriam ArrayList(o tipo bruto), mas o tipo inferido para este operador ternário é ArrayList<?>(o tipo curinga). De modo mais geral, o fato de o Java runtime implementar genéricos por meio de eliminação de tipo não tem efeito no tipo de tempo de compilação de uma expressão.
Meriton
1
@vaxquis obrigado! Sou um cara do C #, os genéricos Java me confundem ;-)
Mathieu Guindon
2
Na verdade, o tipo de two > six ? new ArrayList<Integer>() : new ArrayList<String>()é o ArrayList<? extends Serializable&Comparable<?>>que significa que você pode atribuí-lo a uma variável de tipo ArrayList<? extends Serializable>e também a uma variável de tipo ArrayList<? extends Comparable<?>>, mas é claro ArrayList<?>, que é equivalente a ArrayList<? extends Object>, também funciona.
Holger
6

Em Java e C # (e na maioria das outras linguagens), o resultado de uma expressão tem um tipo. No caso do operador ternário, existem duas subexpressões possíveis avaliadas para o resultado e ambas devem ser do mesmo tipo. No caso do Java, uma intvariável pode ser convertida em Integerautoboxing. Agora, uma vez que ambos Integere Stringherdam de Object, eles podem ser convertidos para o mesmo tipo por uma conversão de estreitamento simples.

Por outro lado, em C #, an inté um primitivo e não há conversão implícita para stringou qualquer outro object.

Code-Apprentice
fonte
Obrigado pela resposta. Autoboxing é o que estou pensando atualmente. C # não permite autoboxing?
blackr1234
2
Não acho que isso seja correto, pois encaixar um int em um objeto é perfeitamente possível em C #. A diferença aqui é, acredito, que o C # apenas adota uma abordagem bastante descontraída para determinar se é permitido ou não.
Jeroen Vannevel
9
Isso não tem nada a ver com o boxe automático, que aconteceria da mesma forma em C #. A diferença é que C # não tenta encontrar um supertipo comum enquanto Java (desde Java 5 apenas!) O faz. Você pode facilmente testar isso tentando ver o que acontece se você tentar com 2 classes personalizadas (que ambas herdam do objeto, obviamente). Também há uma conversão implícita de intpara object.
Voo de
3
E para esclarecer um pouco mais: a conversão de Integer/Stringpara Objectnão é uma conversão de estreitamento, é exatamente o oposto :-)
Voo
1
Integere Stringtambém implementar Serializablee, Comparableportanto, atribuições a qualquer um deles funcionarão bem, por exemplo, Comparable<?> c=condition? 6: "6";ou List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();são códigos Java legais.
Holger
5

Isso é muito simples. Não há conversão implícita entre string e int. o operador ternário precisa que os dois últimos operandos tenham o mesmo tipo.

Experimentar:

Write(two > six ? two.ToString() : "6");
ChiralMichael
fonte
11
A questão é o que causa a diferença entre Java e C #. Isso não responde à pergunta.
Jeroen Vannevel