Unterschied zwischen C # und Javas ternärem Operator (? :)

94

Ich bin ein C # -Neuling und habe gerade ein Problem. Beim Umgang mit dem ternären Operator ( ? :) gibt es einen Unterschied zwischen C # und Java .

Warum funktioniert die 4. Zeile im folgenden Codesegment nicht? Der Compiler zeigt eine Fehlermeldung von an there is no implicit conversion between 'int' and 'string'. Die 5. Zeile funktioniert nicht so gut. Beide Listsind Objekte, nicht wahr?

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

Der gleiche Code funktioniert jedoch 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

Welche Sprachfunktion in C # fehlt? Wenn überhaupt, warum wird es nicht hinzugefügt?

blackr1234
quelle
4
Wie pro msdn.microsoft.com/en-us/library/ty67wk28.aspx „Entweder der Art der first_expression und second_expression müssen gleich sein, oder eine implizite Konvertierung von einem Typ zum anderen existieren.“
Egor
2
Ich habe C # schon eine Weile nicht mehr gemacht, aber sagt es nicht die Nachricht, die Sie zitieren? Die beiden Zweige des Ternärs müssen denselben Datentyp zurückgeben. Du gibst ihm ein int und einen String. C # weiß nicht, wie man ein int automatisch in einen String konvertiert. Sie müssen eine explizite Typkonvertierung darauf setzen.
Jay
2
@ blackr1234 Weil an intkein Objekt ist. Es ist ein Primitiv.
Code-Apprentice
3
@ Code-Apprentice ja, sie sind in der Tat sehr unterschiedlich - C # hat eine Laufzeitreifizierung, daher sind die Listen von nicht verwandten Typen. Oh, und Sie könnten auch generische Schnittstellen-Kovarianz / Kontravarianz in die Mischung einbringen, wenn Sie möchten, um ein gewisses Maß an Beziehung einzuführen;)
Lucas Trzesniewski
3
Zum Vorteil des OP: Typlöschung in Java bedeutet das ArrayList<String>und ArrayList<Integer>wird nur ArrayListim Bytecode. Dies bedeutet, dass sie zur Laufzeit genau vom gleichen Typ zu sein scheinen . Anscheinend sind sie in C # verschiedene Typen.
Code-Apprentice

Antworten:

106

Wenn Sie sich den Abschnitt 7.14 der C # 5-Sprachspezifikation ansehen : Bedingter Operator , sehen Sie Folgendes:

  • Wenn x Typ X und y Typ Y hat, dann

    • Wenn eine implizite Konvertierung (§6.1) von X nach Y, aber nicht von Y nach X existiert, ist Y der Typ des bedingten Ausdrucks.

    • Wenn eine implizite Konvertierung (§6.1) von Y nach X existiert, aber nicht von X nach Y, dann ist X der Typ des bedingten Ausdrucks.

    • Andernfalls kann kein Ausdruckstyp ermittelt werden, und es tritt ein Fehler bei der Kompilierung auf

Mit anderen Worten: Es wird versucht herauszufinden, ob x und y ineinander konvertiert werden können. Andernfalls tritt ein Kompilierungsfehler auf. In unserem Fall intund stringhaben keine explizite oder implizite Konvertierung, so dass es nicht kompiliert wird.

Vergleichen Sie dies mit der Java 7-Sprachspezifikation in Abschnitt 15.25: Bedingter Operator :

  • Wenn der zweite und der dritte Operand denselben Typ haben (der der Nulltyp sein kann), ist dies der Typ des bedingten Ausdrucks. ( NEIN )
  • Wenn einer der zweiten und dritten Operanden vom primitiven Typ T ist und der Typ des anderen das Ergebnis der Anwendung der Boxkonvertierung (§5.1.7) auf T ist, ist der Typ des bedingten Ausdrucks T. ( NO )
  • Wenn einer der zweiten und dritten Operanden vom Typ Null ist und der Typ des anderen ein Referenztyp ist, ist der Typ des bedingten Ausdrucks dieser Referenztyp. ( NEIN )
  • Andernfalls gibt es mehrere Fälle, wenn der zweite und dritte Operand Typen haben, die in numerische Typen konvertierbar sind (§5.1.8): ( NO )
  • Ansonsten sind der zweite und der dritte Operand vom Typ S1 bzw. S2. Sei T1 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S1 ergibt, und sei T2 der Typ, der sich aus der Anwendung der Boxkonvertierung auf S2 ergibt.
    Der Typ des bedingten Ausdrucks ergibt sich aus der Anwendung der Capture-Konvertierung (§5.1.10) auf lub (T1, T2) (§15.12.2.7). ( JA )

Und siehe Abschnitt 15.12.2.7. Ableiten von Typargumenten Basierend auf tatsächlichen Argumenten können wir sehen, dass versucht wird, einen gemeinsamen Vorfahren zu finden, der als Typ für den Aufruf dient, mit dem er landet Object. Object ist ein akzeptables Argument, damit der Aufruf funktioniert.

Jeroen Vannevel
quelle
1
Ich habe die C # -Spezifikation so gelesen, dass es eine implizite Konvertierung in mindestens eine Richtung geben muss. Der Compilerfehler tritt nur auf, wenn keine implizite Konvertierung in eine der beiden Richtungen erfolgt.
Code-Apprentice
1
Dies ist natürlich immer noch die vollständigste Antwort, da Sie direkt aus den Spezifikationen zitieren.
Code-Apprentice
Dies wurde eigentlich nur mit Java 5 geändert (ich denke, könnte 6 gewesen sein). Vorher hatte Java genau das gleiche Verhalten wie C #. Aufgrund der Art und Weise, wie Enums implementiert werden, hat dies in Java wahrscheinlich noch mehr Überraschungen verursacht als in C #, obwohl dies eine gute Änderung war.
Voo
@ Voo: FYI, Java 5 und Java 6 haben die gleiche Spezifikationsversion ( The Java Language Specification , Third Edition), so dass es sich in Java 6 definitiv nicht geändert hat.
Ruakh
86

Die gegebenen Antworten sind gut; Ich möchte ihnen hinzufügen, dass diese Regel von C # eine Folge einer allgemeineren Konstruktionsrichtlinie ist. Wenn C # aufgefordert wird, den Typ eines Ausdrucks aus einer von mehreren Auswahlmöglichkeiten abzuleiten, wählt er das eindeutig beste aus . Das heißt, wenn Sie C # einige Optionen wie "Giraffe, Säugetier, Tier" geben, wählt es je nach den Umständen die allgemeinste - Tier - oder die spezifischste - Giraffe. Aber es muss eine der Entscheidungen treffen, die es tatsächlich gegeben hat . C # sagt nie "Ich habe die Wahl zwischen Katze und Hund, daher werde ich daraus schließen, dass Tier die beste Wahl ist". Das war keine Wahl, also kann C # sie nicht wählen.

Im Fall des ternären Operators versucht C #, den allgemeineren Typ von int und string zu wählen, aber auch nicht den allgemeineren Typ. Anstatt einen Typ auszuwählen, der überhaupt keine Wahl war, wie z. B. ein Objekt, entscheidet C #, dass kein Typ abgeleitet werden kann.

Ich stelle auch fest, dass dies im Einklang mit einem anderen Designprinzip von C # steht: Wenn etwas falsch aussieht, informieren Sie den Entwickler. Die Sprache sagt nicht "Ich werde raten, was du gemeint hast und durchwühlen, wenn ich kann". Die Sprache sagt: "Ich denke, Sie haben hier etwas Verwirrendes geschrieben, und ich werde Ihnen davon erzählen."

Außerdem stelle ich fest, dass C # nicht aus dem Variablen zum zugewiesenen Wert führt , sondern in die andere Richtung. C # sagt nicht "Sie weisen einer Objektvariablen zu, daher muss der Ausdruck in ein Objekt konvertierbar sein, daher werde ich sicherstellen, dass dies der Fall ist". Vielmehr sagt C #: "Dieser Ausdruck muss einen Typ haben, und ich muss ableiten können, dass der Typ mit dem Objekt kompatibel ist." Da der Ausdruck keinen Typ hat, wird ein Fehler erzeugt.

Eric Lippert
quelle
6
Ich kam über den ersten Absatz hinaus und fragte mich, warum das plötzliche Interesse an Kovarianz / Kontravarianz, dann begann ich mich mehr auf den zweiten Absatz zu einigen, dann sah ich, wer die Antwort schrieb ... Hätte früher raten sollen. Haben Sie jemals genau bemerkt , wie oft Sie "Giraffe" als Beispiel verwenden ? Sie müssen Giraffen wirklich mögen.
Pharap
21
Giraffen sind großartig!
Eric Lippert
9
@Pharap: In aller Ernsthaftigkeit gibt es pädagogische Gründe, warum ich so oft Giraffen benutze. Erstens sind Giraffen auf der ganzen Welt bekannt. Zweitens ist es eine Art lustiges Wort, und sie sehen so seltsam aus, dass es ein unvergessliches Bild ist. Drittens betont die Verwendung von beispielsweise "Giraffe" und "Schildkröte" in derselben Analogie stark, "dass diese beiden Klassen, obwohl sie eine Basisklasse teilen können, sehr unterschiedliche Tiere mit sehr unterschiedlichen Eigenschaften sind". Bei Fragen zu Typsystemen gibt es häufig Beispiele, bei denen die Typen logisch sehr ähnlich sind, was Ihre Intuition in die falsche Richtung treibt.
Eric Lippert
7
@Pharap: Das heißt, die Frage lautet normalerweise "Warum kann eine Liste der Fabriken von Kundenrechnungsanbietern nicht als Liste der Fabriken von Rechnungsanbietern verwendet werden?". und Ihre Intuition sagt "Ja, das sind im Grunde die gleichen Dinge", aber wenn Sie sagen "Warum kann ein Raum voller Giraffen nicht als Raum voller Säugetiere genutzt werden?" dann wird es viel mehr zu einer intuitiven Analogie zu sagen, "weil ein Raum voller Säugetiere einen Tiger enthalten kann, ein Raum voller Giraffen nicht".
Eric Lippert
6
@ Eric Ich bin ursprünglich auf dieses Problem gestoßen int? b = (a != 0 ? a : (int?) null). Es gibt auch diese Frage , zusammen mit allen verknüpften Fragen auf der Seite. Wenn Sie den Links weiter folgen, gibt es einige. Im Vergleich dazu habe ich noch nie von jemandem gehört, der auf ein reales Problem mit Java stößt.
BlueRaja - Danny Pflughoeft
24

In Bezug auf den Generika-Teil:

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

In C # versucht der Compiler , die rechten Ausdrucksteile in einen allgemeinen Typ zu konvertieren . Da List<int>und List<string>zwei verschiedene konstruierte Typen sind, kann einer nicht in den anderen konvertiert werden.

In Java versucht der Compiler , einen gemeinsamen Supertyp zu finden, anstatt ihn zu konvertieren. Daher umfasst die Kompilierung des Codes die implizite Verwendung von Platzhaltern und das Löschen von Typen .

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

hat den Kompilierungstyp ArrayList<?>(tatsächlich kann es auch sein ArrayList<? extends Serializable>oder ArrayList<? extends Comparable<?>>, abhängig vom Verwendungskontext, da beide gängige generische Supertypen sind) und den Laufzeittyp von rawArrayList (da es sich um den gemeinsamen rohen Supertyp handelt).

Zum Beispiel (testen Sie es selbst) ,

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
quelle
Funktioniert eigentlich Write(two > six ? new List<object>() : new List<string>());auch nicht.
blackr1234
2
@ blackr1234 genau: Ich wollte nicht wiederholen, was andere Antworten bereits sagten, aber der ternäre Ausdruck muss zu einem Typ ausgewertet werden, und der Compiler wird nicht versuchen, den "kleinsten gemeinsamen Nenner" der beiden zu finden.
Mathieu Guindon
2
Hier geht es eigentlich nicht um das Löschen von Typen, sondern um Platzhaltertypen. Die Löschungen von ArrayList<String>und ArrayList<Integer>wären ArrayList(der Rohtyp), aber der abgeleitete Typ für diesen ternären Operator ist ArrayList<?>(der Platzhaltertyp). Allgemeiner gesagt hat die Implementierung von Generika durch Java- Laufzeit durch Löschen des Typs keine Auswirkung auf den Typ der Kompilierungszeit eines Ausdrucks.
Meriton
1
@axquis danke! Ich bin ein C # -Typ, Java-Generika verwirren mich ;-)
Mathieu Guindon
2
Eigentlich ist die Art der two > six ? new ArrayList<Integer>() : new ArrayList<String>()heißt , ArrayList<? extends Serializable&Comparable<?>>das bedeutet , dass Sie es auf eine Variable vom Typ zuweisen können , ArrayList<? extends Serializable>sowie eine Variable vom Typ ArrayList<? extends Comparable<?>>, aber natürlich ArrayList<?>, was äquivalent ist ArrayList<? extends Object>, funktioniert auch.
Holger
6

Sowohl in Java als auch in C # (und den meisten anderen Sprachen) hat das Ergebnis eines Ausdrucks einen Typ. Im Fall des ternären Operators werden zwei mögliche Unterausdrücke für das Ergebnis ausgewertet, und beide müssen denselben Typ haben. Im Fall von Java kann eine intVariable Integerdurch Autoboxing in eine Variable konvertiert werden. Da nun beide Integerund von Stringerben Object, können sie durch eine einfache Verengungskonvertierung in denselben Typ konvertiert werden.

Andererseits ist an in C # intein Primitiv und es gibt keine implizite Konvertierung zu stringoder einem anderen object.

Code-Lehrling
quelle
Danke für die Antwort. Autoboxing ist das, woran ich gerade denke. Erlaubt C # kein Autoboxing?
blackr1234
2
Ich denke nicht, dass dies richtig ist, da das Boxen eines Int in ein Objekt in C # durchaus möglich ist. Ich glaube, der Unterschied besteht darin, dass C # nur einen sehr entspannten Ansatz verfolgt, um festzustellen, ob dies zulässig ist oder nicht.
Jeroen Vannevel
9
Dies hat nichts mit Auto-Boxen zu tun, was in C # genauso häufig vorkommen würde. Der Unterschied besteht darin, dass C # nicht versucht, einen gemeinsamen Supertyp zu finden, während Java (da nur Java 5!) Dies tut. Sie können dies leicht testen, indem Sie versuchen zu sehen, was passiert, wenn Sie es mit 2 benutzerdefinierten Klassen versuchen (die beide offensichtlich vom Objekt erben). Es gibt auch eine implizite Konvertierung von intnach object.
Voo
3
Und um ein bisschen mehr zu picken: Die Konvertierung von Integer/Stringnach Objectist keine verengende Konvertierung, es ist genau das Gegenteil :-)
Voo
1
Integerund Stringauch implementieren Serializableund Comparableso funktionieren Zuweisungen zu einem von ihnen auch, z. B. Comparable<?> c=condition? 6: "6";oder List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();sind legaler Java-Code.
Holger
5

Das ist ziemlich einfach. Es gibt keine implizite Konvertierung zwischen string und int. Der ternäre Operator benötigt die letzten beiden Operanden, um denselben Typ zu haben.

Versuchen:

Write(two > six ? two.ToString() : "6");
ChiralMichael
quelle
11
Die Frage ist, was den Unterschied zwischen Java und C # verursacht. Dies beantwortet die Frage nicht.
Jeroen Vannevel