Differenza tra C # e l'operatore ternario di Java (? :)


94

Sono un principiante di C # e ho appena riscontrato un problema. C'è una differenza tra C # e Java quando si ha a che fare con l'operatore ternario ( ? :).

Nel seguente segmento di codice, perché la quarta riga non funziona? Il compilatore mostra un messaggio di errore di there is no implicit conversion between 'int' and 'string'. Anche la quinta riga non funziona. Entrambi Listsono oggetti, non è vero?

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

Tuttavia, lo stesso codice funziona in 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

Quale caratteristica del linguaggio in C # manca? Se esiste, perché non viene aggiunto?


4
Secondo msdn.microsoft.com/en-us/library/ty67wk28.aspx "Il tipo di first_expression e second_expression deve essere lo stesso, oppure deve esistere una conversione implicita da un tipo all'altro."
Egor

2
Non ho fatto C # per un po ', ma il messaggio che citi non lo dice? I due rami del ternario devono restituire lo stesso tipo di dati. Stai dando un int e una stringa. C # non sa come convertire automagicamente un int in una stringa. È necessario inserire una conversione di tipo esplicita su di esso.
Jay,

2
@ blackr1234 Perché un intnon è un oggetto. È un primitivo.
Code-Apprentice

3
@ Code-Apprentice sì, sono davvero molto diversi - C # ha la reificazione del runtime, quindi gli elenchi sono di tipi non correlati. Oh, e potresti anche inserire la covarianza / controvarianza dell'interfaccia generica nel mix se lo desideri, per introdurre un certo livello di relazione;)
Lucas Trzesniewski

3
A vantaggio dell'OP: il tipo di cancellazione in Java lo significa ArrayList<String>e ArrayList<Integer>diventa solo ArrayListnel bytecode. Ciò significa che sembrano essere esattamente lo stesso tipo in fase di esecuzione. Apparentemente in C # sono tipi diversi.
Code-Apprentice

Risposte:


106

Esaminando la sezione 7.14 delle specifiche del linguaggio C # 5: Operatore condizionale possiamo vedere quanto segue:

  • Se x ha il tipo X e y ha il tipo Y, allora

    • Se esiste una conversione implicita (§6.1) da X a Y, ma non da Y a X, allora Y è il tipo di espressione condizionale.

    • Se esiste una conversione implicita (§6.1) da Y a X, ma non da X a Y, allora X è il tipo di espressione condizionale.

    • In caso contrario, non è possibile determinare il tipo di espressione e si verifica un errore in fase di compilazione

In altre parole: si cerca di trovare se xey possono essere convertiti in vicenda e, se non, si verifica un errore di compilazione. Nel nostro caso inte stringnon hanno conversioni esplicite o implicite, quindi non verranno compilate.

Confrontalo con la sezione 15.25 delle specifiche del linguaggio Java 7: Operatore condizionale :

  • Se il secondo e il terzo operando hanno lo stesso tipo (che può essere il tipo null), allora quello è il tipo di espressione condizionale. ( NO )
  • Se uno del secondo e terzo operando è di tipo primitivo T e il tipo dell'altro è il risultato dell'applicazione della conversione di boxe (§5.1.7) a T, il tipo di espressione condizionale è T. ( NO )
  • Se uno del secondo e del terzo operando è di tipo null e il tipo dell'altro è un tipo di riferimento, il tipo di espressione condizionale è quel tipo di riferimento. ( NO )
  • Altrimenti, se il secondo e il terzo operando hanno tipi convertibili (§5.1.8) in tipi numerici, allora ci sono diversi casi: ( NO )
  • Altrimenti, il secondo e il terzo operando sono rispettivamente di tipo S1 e S2. Sia T1 il tipo che risulta dall'applicazione della conversione di boxe a S1 e che T2 sia il tipo che risulta dall'applicazione della conversione di boxe a S2.
    Il tipo di espressione condizionale è il risultato dell'applicazione della conversione di cattura (§5.1.10) a lub (T1, T2) (§15.12.2.7). ( SI )

E, guardando la sezione 15.12.2.7. Inferendo gli argomenti di tipo in base agli argomenti effettivi , possiamo vedere che cerca di trovare un antenato comune che fungerà da tipo utilizzato per la chiamata con cui lo atterra Object. Object è un argomento accettabile, quindi la chiamata funzionerà.


1
Ho letto la specifica C # dicendo che deve esserci una conversione implicita in almeno una direzione. L'errore del compilatore si verifica solo quando non è presente alcuna conversione implicita in nessuna direzione.
Code-Apprentice

1
Naturalmente, questa è ancora la risposta più completa poiché citi direttamente dalle specifiche.
Code-Apprentice

Questo in realtà è stato modificato solo con Java 5 (penso, potrebbe essere stato 6). Prima di allora Java aveva esattamente lo stesso comportamento di C #. A causa del modo in cui sono implementate le enumerazioni, questo probabilmente ha causato ancora più sorprese in Java che in C #, anche se quindi un buon cambiamento da sempre.
Voo

@Voo: Cordiali saluti, Java 5 e Java 6 hanno la stessa versione delle specifiche ( The Java Language Specification , Third Edition), quindi sicuramente non è cambiato in Java 6.
ruakh

86

Le risposte fornite sono buone; Aggiungerei a loro che questa regola di C # è una conseguenza di una linea guida di progettazione più generale. Quando viene chiesto di dedurre il tipo di un'espressione da una delle diverse scelte, C # sceglie il migliore unico di esse . Cioè, se dai a C # alcune scelte come "Giraffe, Mammal, Animal", potrebbe scegliere la più generale - Animal - o potrebbe scegliere la più specifica - Giraffe - a seconda delle circostanze. Ma deve scegliere una delle scelte che le sono state effettivamente date . C # non dice mai "le mie scelte sono tra cane e gatto, quindi deduco che Animal è la scelta migliore". Non era una scelta data, quindi C # non può sceglierla.

Nel caso dell'operatore ternario C # cerca di scegliere il tipo più generale di int e stringa, ma nessuno dei due è il tipo più generale. Invece di scegliere un tipo che non era una scelta in primo luogo, come object, C # decide che nessun tipo può essere dedotto.

Noto anche che questo è in linea con un altro principio di progettazione di C #: se qualcosa sembra storto, dillo allo sviluppatore. La lingua non dice "Ho intenzione di indovinare cosa intendevi e se posso cavarmela". La lingua dice "Penso che tu abbia scritto qualcosa di confuso qui, e te ne parlerò".

Inoltre, noto che C # non ragiona dalla variabile al valore assegnato , ma piuttosto dall'altra direzione. C # non dice "stai assegnando a una variabile oggetto quindi l'espressione deve essere convertibile in oggetto, quindi mi assicurerò che lo sia". Piuttosto, C # dice "questa espressione deve avere un tipo e devo essere in grado di dedurre che il tipo è compatibile con l'oggetto". Poiché l'espressione non ha un tipo, viene prodotto un errore.


6
Ho superato il primo paragrafo chiedendomi perché l'improvvisa covarianza / controvarianza interessasse, poi ho iniziato ad essere più d'accordo sul secondo paragrafo, poi ho visto chi ha scritto la risposta ... Avrei dovuto indovinare prima. Hai mai notato quanto usi esattamente "Giraffe" come esempio ? Ti piacciono davvero le giraffe.
Pharap

21
Le giraffe sono fantastiche!
Eric Lippert

9
@Pharap: In tutta serietà, ci sono ragioni pedagogiche per cui uso così tanto la giraffa. Prima di tutto, le giraffe sono famose in tutto il mondo. In secondo luogo, è una sorta di parola divertente da dire, e hanno un aspetto così strano che è un'immagine memorabile. Terzo, l'uso, diciamo, di "giraffa" e "tartaruga" nella stessa analogia sottolinea fortemente "queste due classi, sebbene possano condividere una classe base, sono animali molto diversi con caratteristiche molto diverse". Le domande sui sistemi di tipi spesso hanno esempi in cui i tipi sono logicamente molto simili, il che guida la tua intuizione nel modo sbagliato.
Eric Lippert

7
@Pharap: Cioè, la domanda è di solito qualcosa del tipo "perché non è possibile utilizzare un elenco di fabbriche di fornitori di fatture cliente come elenco di fabbriche di fornitori di fatture?" e la tua intuizione dice "sì, quelli sono praticamente la stessa cosa", ma quando dici "perché una stanza piena di giraffe non può essere usata come una stanza piena di mammiferi?" allora diventa molto più un'analogia intuitiva dire "perché una stanza piena di mammiferi può contenere una tigre, una stanza piena di giraffe no".
Eric Lippert

6
@ Eric con cui mi sono imbattuto originariamente in questo problema int? b = (a != 0 ? a : (int?) null). C'è anche questa domanda , insieme a tutte le domande collegate a lato. Se continui a seguire i link, ce ne sono alcuni. In confronto, non ho mai sentito parlare di qualcuno che si imbatte in un problema reale con il modo in cui Java lo fa.
BlueRaja - Danny Pflughoeft

24

Per quanto riguarda la parte dei generici:

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

In C #, il compilatore tenta di convertire le parti di espressione a destra in un tipo comune; poiché List<int>e List<string>sono due tipi costruiti distinti, uno non può essere convertito nell'altro.

In Java, il compilatore cerca di trovare un supertipo comune invece di convertire, quindi la compilazione del codice implica l'uso implicito di caratteri jolly e la cancellazione del tipo ;

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

ha il tipo di compilazione di ArrayList<?>(in realtà, può essere anche ArrayList<? extends Serializable>o ArrayList<? extends Comparable<?>>, a seconda del contesto d'uso, poiché sono entrambi supertipi generici comuni) e il tipo di runtime di raw ArrayList(poiché è il supertipo raw comune).

Ad esempio (provalo tu stesso) ,

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

In realtà Write(two > six ? new List<object>() : new List<string>());non funziona neanche.
blackr1234

2
@ blackr1234 esattamente: non volevo ripetere ciò che altre risposte hanno già detto, ma l'espressione ternaria deve essere valutata in un tipo e il compilatore non tenterà di trovare il "minimo comune denominatore" dei due.
Mathieu Guindon,

2
In realtà, non si tratta di cancellare il tipo, ma di caratteri jolly. Le cancellazioni di ArrayList<String>e ArrayList<Integer>sarebbero ArrayList(il tipo grezzo), ma il tipo dedotto per questo operatore ternario è ArrayList<?>(il tipo jolly). Più in generale, il fatto che il runtime Java implementa i generici tramite la cancellazione del tipo non ha alcun effetto sul tipo in fase di compilazione di un'espressione.
meriton

1
@vaxquis grazie! Sono un tipo C #, i generici Java mi confondono ;-)
Mathieu Guindon

2
In realtà, il tipo di two > six ? new ArrayList<Integer>() : new ArrayList<String>()è il ArrayList<? extends Serializable&Comparable<?>>che significa che puoi assegnarlo a una variabile di tipo ArrayList<? extends Serializable>oltre che a una variabile di tipo ArrayList<? extends Comparable<?>>, ma ovviamente ArrayList<?>, che è equivalente a ArrayList<? extends Object>, funziona anche.
Holger

6

Sia in Java che in C # (e nella maggior parte degli altri linguaggi), il risultato di un'espressione ha un tipo. Nel caso dell'operatore ternario, ci sono due possibili sottoespressioni valutate per il risultato ed entrambe devono avere lo stesso tipo. Nel caso di Java, una intvariabile può essere convertita in un Integerautoboxing. Ora poiché entrambi Integere Stringereditano da Object, possono essere convertiti nello stesso tipo con una semplice conversione di restringimento.

D'altra parte, in C #, an intè una primitiva e non c'è conversione implicita in stringo qualsiasi altro object.


Grazie per la risposta. L'autoboxing è ciò a cui sto pensando attualmente. C # non consente l'autoboxing?
blackr1234

2
Non penso che questo sia corretto in quanto l'inserimento di un int in un oggetto è perfettamente possibile in C #. La differenza qui è, credo, che C # adotta un approccio molto rilassato nel determinare se è consentito o meno.
Jeroen Vannevel

9
Questo non ha nulla a che fare con l'auto boxing, che accadrebbe altrettanto in C #. La differenza è che C # non cerca di trovare un supertipo comune mentre Java (a partire da Java 5 solo!) Lo fa. Puoi facilmente testarlo provando a vedere cosa succede se lo provi con 2 classi personalizzate (che ereditano entrambe dall'oggetto ovviamente). Inoltre c'è una conversione implicita da inta object.
Voo

3
E per fare il pelo nell'uovo solo un po 'di più: la conversione da Integer/Stringa Objectnon è una conversione restrittiva , è l'esatto opposto :-)
Voo

1
Integere Stringanche implementare Serializablee Comparablequindi anche le assegnazioni a uno di essi funzioneranno, ad esempio Comparable<?> c=condition? 6: "6";o List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();sono codice Java legale.
Holger

5

Questo è piuttosto semplice. Non c'è conversione implicita tra string e int. l'operatore ternario necessita che gli ultimi due operandi abbiano lo stesso tipo.

Provare:

Write(two > six ? two.ToString() : "6");

11
La domanda è cosa causa la differenza tra Java e C #. Questo non risponde alla domanda.
Jeroen Vannevel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.