Różnica między C # a operatorem trójargumentowym Javy (? :)

94

Jestem nowicjuszem w C # i właśnie napotykam problem. Istnieje różnica między językiem C # a Javą, gdy mamy do czynienia z operatorem trójargumentowym ( ? :).

W następującym segmencie kodu, dlaczego czwarta linia nie działa? Kompilator wyświetla komunikat o błędzie there is no implicit conversion between 'int' and 'string'. Piąta linia również nie działa. Oba Listsą obiektami, prawda?

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

Jednak ten sam kod działa w Javie:

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

Jakiej funkcji języka w C # brakuje? Jeśli tak, dlaczego nie jest dodany?

blackr1234
źródło
4
Zgodnie z msdn.microsoft.com/en-us/library/ty67wk28.aspx „Typy first_expression i second_expression muszą być takie same lub musi istnieć niejawna konwersja z jednego typu na drugi.”
Egor
2
Nie robiłem C # od jakiegoś czasu, ale czy cytowana wiadomość nie mówi tego? Dwie gałęzie trójskładnika muszą zwracać ten sam typ danych. Dajesz mu int i String. C # nie wie, jak automagicznie przekonwertować int na String. Musisz umieścić na nim jawną konwersję typu.
Jay
2
@ blackr1234 Ponieważ intobiekt nie jest obiektem. To prymitywne.
Code-Apprentice
3
@ Code-Apprentice tak, rzeczywiście są bardzo różne - C # ma reifikację środowiska uruchomieniowego, więc listy są niepowiązanych typów. Aha, i możesz też wrzucić do miksu ogólną kowariancję / kontrawariancję interfejsu, jeśli chcesz, aby wprowadzić pewien poziom relacji;)
Lucas Trzesniewski
3
Z korzyścią dla OP: wymazanie typu w Javie oznacza to ArrayList<String>i ArrayList<Integer>staje się po prostu ArrayListw kodzie bajtowym. Oznacza to, że w czasie wykonywania wydają się być dokładnie tego samego typu . Najwyraźniej w C # są to różne typy.
Code-Apprentice

Odpowiedzi:

106

Przeglądając specyfikację języka C # 5 w sekcji 7.14: Operator warunkowy , widzimy:

  • Jeśli x ma typ X, a y ma typ Y, to

    • Jeśli niejawna konwersja (§6.1) istnieje od X do Y, ale nie od Y do X, wtedy Y jest typem wyrażenia warunkowego.

    • Jeśli niejawna konwersja (§6.1) istnieje od Y do X, ale nie od X do Y, wtedy X jest typem wyrażenia warunkowego.

    • W przeciwnym razie nie można określić żadnego typu wyrażenia i wystąpi błąd w czasie kompilacji

Innymi słowy: próbuje znaleźć, czy x i y mogą zostać zamienione na siebie nawzajem, a jeśli nie, wystąpi błąd kompilacji. W naszym przypadku inti stringnie ma wyraźnej lub dorozumianej konwersji, więc nie będzie skompilować.

Porównaj to ze specyfikacją języka Java 7 w sekcji 15.25: Operator warunkowy :

  • Jeśli drugi i trzeci operand mają ten sam typ (który może być typem zerowym), to jest to typ wyrażenia warunkowego. ( NIE )
  • Jeśli jeden z drugiego i trzeciego operandu jest typu pierwotnego T, a typ drugiego jest wynikiem zastosowania konwersji pudełkowej (§5.1.7) do T, to typ wyrażenia warunkowego to T. ( NIE )
  • Jeśli jeden z drugiego i trzeciego operandu jest typu null, a drugi jest typem referencyjnym, to typ wyrażenia warunkowego jest typem referencyjnym. ( NIE )
  • W przeciwnym razie, jeśli drugi i trzeci operand mają typy, które można zamienić (§5.1.8) na typy liczbowe, istnieje kilka przypadków: ( NIE )
  • W przeciwnym razie drugi i trzeci operand są odpowiednio typu S1 i S2. Niech T1 będzie typem wynikającym z zastosowania konwersji pudełkowej do S1 i niech T2 będzie typem, który wynika z zastosowania konwersji pudełkowej do S2.
    Typ wyrażenia warunkowego jest wynikiem zastosowania konwersji przechwytywania (§5.1.10) do lub (T1, T2) (§15.12.2.7). ( TAK )

Patrząc na sekcję 15.12.2.7. Wnioskowanie o argumentach typu Na podstawie rzeczywistych argumentów widzimy, że próbuje znaleźć wspólnego przodka, który posłuży jako typ użyty do wywołania, z którym się kończy Object. Object jest dopuszczalnym argumentem, więc wywołanie będzie działać.

Jeroen Vannevel
źródło
1
Czytam specyfikację C #, mówiącą, że musi istnieć niejawna konwersja w co najmniej jednym kierunku. Błąd kompilatora występuje tylko wtedy, gdy nie ma niejawnej konwersji w żadnym kierunku.
Code-Apprentice
1
Oczywiście jest to nadal najbardziej kompletna odpowiedź, ponieważ cytujesz bezpośrednio ze specyfikacji.
Code-Apprentice
W rzeczywistości zostało to zmienione tylko w Javie 5 (myślę, że mogło to być 6). Wcześniej Java zachowywała się dokładnie tak samo jak C #. Ze względu na sposób implementacji wyliczeń prawdopodobnie spowodowało to jeszcze więcej niespodzianek w Javie niż w C #, chociaż przez cały czas jest to dobra zmiana.
Voo
@Voo: FYI, Java 5 i Java 6 mają tę samą wersję specyfikacji ( The Java Language Specification , Third Edition), więc zdecydowanie nie zmieniła się w Javie 6.
ruakh
86

Podane odpowiedzi są dobre; Dodam do nich, że ta reguła języka C # jest konsekwencją bardziej ogólnych wytycznych projektowych. Gdy C # poprosi o wywnioskowanie typu wyrażenia na podstawie jednej z kilku opcji, wybiera najlepszą z nich . Oznacza to, że jeśli dasz C # kilka opcji, takich jak „Żyrafa, ssak, zwierzę”, może wybrać najbardziej ogólny - Zwierzę - lub może wybrać najbardziej szczegółowy - Żyrafa - w zależności od okoliczności. Ale musi wybrać jeden z wyborów, które mu faktycznie dano . C # nigdy nie mówi „moje wybory są między kotem a psem, dlatego wnioskuję, że najlepszym wyborem jest Animal”. To nie był dany wybór, więc C # nie może go wybrać.

W przypadku operatora trójskładnikowego C # próbuje wybrać bardziej ogólny typ int i string, ale żaden z nich nie jest typem bardziej ogólnym. Zamiast wybierać typ, który nie był wyborem w pierwszej kolejności, na przykład obiekt, C # decyduje, że nie można wywnioskować typu.

Zwracam również uwagę, że jest to zgodne z inną zasadą projektowania języka C #: jeśli coś wygląda nie tak, poinformuj o tym dewelopera. Język nie mówi: „Zgadnę, co miałeś na myśli i będę się przez to gmatwał, jeśli będę mógł”. Język mówi: „Myślę, że napisałeś tu coś zagmatwanego i powiem ci o tym”.

Zwracam również uwagę, że C # nie prowadzi rozumowania ze zmiennej do przypisanej wartości , ale raczej w przeciwnym kierunku. C # nie mówi „przypisujesz do zmiennej obiektu, dlatego wyrażenie musi być konwertowalne na obiekt, dlatego upewnię się, że tak jest”. Zamiast tego C # mówi „to wyrażenie musi mieć typ i muszę być w stanie wywnioskować, że typ jest zgodny z obiektem”. Ponieważ wyrażenie nie ma typu, generowany jest błąd.

Eric Lippert
źródło
6
Przeszedłem przez pierwszy akapit, zastanawiając się, dlaczego nagłe zainteresowanie kowariancją / kontrawariancją, potem zacząłem bardziej zgadzać się z drugim akapitem, a potem zobaczyłem, kto napisał odpowiedź ... Powinienem był zgadnąć wcześniej. Czy zauważyłeś kiedyś , jak bardzo używasz „Żyrafy” jako przykładu ? Musisz naprawdę lubić żyrafy.
Pharap
21
Żyrafy są niesamowite!
Eric Lippert
9
@Pharap: Mówiąc poważnie, istnieją pedagogiczne powody, dla których tak często używam żyrafy. Przede wszystkim żyrafy są dobrze znane na całym świecie. Po drugie, jest to rodzaj zabawnego słowa i wyglądają tak dziwnie, że jest to niezapomniany obraz. Po trzecie, użycie, powiedzmy, „żyrafy” i „żółwia” w tej samej analogii silnie podkreśla „te dwie klasy, chociaż mogą mieć wspólną klasę podstawową, są bardzo różnymi zwierzętami o bardzo różnych cechach”. Pytania dotyczące systemów typów często zawierają przykłady, w których typy są logicznie bardzo podobne, co kieruje twoją intuicją w niewłaściwy sposób.
Eric Lippert,
7
@Pharap: To znaczy, zwykle pojawia się pytanie "dlaczego nie można użyć listy fabryk dostawców faktur dla klientów jako listy fabryk dostawców faktur?" a twoja intuicja mówi: „tak, to w zasadzie to samo”, ale kiedy mówisz: „dlaczego nie można używać pokoju pełnego żyraf jako pokoju pełnego ssaków?” wtedy bardziej intuicyjną analogią staje się stwierdzenie: „ponieważ pokój pełen ssaków może pomieścić tygrysa, a pokój pełen żyraf nie może”.
Eric Lippert
6
@Eric Pierwotnie napotkałem ten problem int? b = (a != 0 ? a : (int?) null). Jest też to pytanie wraz ze wszystkimi powiązanymi pytaniami z boku. Jeśli nadal będziesz podążać za linkami, jest ich całkiem sporo. Dla porównania, nigdy nie słyszałem, żeby ktoś napotkał prawdziwy problem ze sposobem, w jaki robi to Java.
BlueRaja - Danny Pflughoeft
24

Odnośnie części ogólnej:

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

W języku C # kompilator próbuje przekonwertować prawostronne części wyrażenia na typowy typ; od List<int>iList<string> są dwoma różnymi skonstruowanymi typami, jednego nie można przekonwertować na drugi.

W Javie kompilator próbuje znaleźć wspólny typ nadrzędny zamiast dokonywać konwersji, więc kompilacja kodu obejmuje niejawne użycie symboli wieloznacznych i wymazywanie typów ;

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

ma typ kompilacji ArrayList<?>(w rzeczywistości może to być również ArrayList<? extends Serializable>lub ArrayList<? extends Comparable<?>>, w zależności od kontekstu użycia, ponieważ oba są typowymi rodzajami nadrzędnymi) i typ czasu wykonywania surowegoArrayList uruchomieniowego (ponieważ jest to wspólny nadtyp surowy).

Na przykład (sprawdź to sam) ,

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
źródło
Właściwie Write(two > six ? new List<object>() : new List<string>());też nie działa.
blackr1234
2
@ blackr1234 dokładnie: Nie chciałem powtarzać tego, co zostało powiedziane w innych odpowiedziach, ale wyrażenie trójskładnikowe musi zostać oszacowane na typ, a kompilator nie będzie próbował znaleźć „najniższego wspólnego mianownika” z tych dwóch.
Mathieu Guindon
2
Właściwie nie chodzi o usuwanie typów, ale o typy symboli wieloznacznych. Wymazania ArrayList<String>i ArrayList<Integer>byłyby ArrayList(typ surowy), ale typ wywnioskowany dla tego operatora trójskładnikowego to ArrayList<?>(typ wieloznaczny). Mówiąc bardziej ogólnie, środowisko wykonawcze Java implementuje typy ogólne poprzez wymazywanie typów, nie ma wpływu na typ wyrażenia w czasie kompilacji .
meriton
1
@vaxquis thanks! Jestem facetem C #,
generiki
2
W rzeczywistości typ two > six ? new ArrayList<Integer>() : new ArrayList<String>()jest, ArrayList<? extends Serializable&Comparable<?>>co oznacza, że ​​możesz przypisać go do zmiennej typu, ArrayList<? extends Serializable>jak również zmiennej typu ArrayList<? extends Comparable<?>>, ale oczywiście ArrayList<?>, co jest równoważne ArrayList<? extends Object>, również działa.
Holger
6

Zarówno w Javie, jak i C # (i większości innych języków) wynik wyrażenia ma typ. W przypadku operatora trójskładnikowego istnieją dwa możliwe wyrażenia podrzędne obliczane dla wyniku i oba muszą mieć ten sam typ. W przypadku Javy intzmienna może zostać przekonwertowana na Integerzmienną poprzez autoboxing. Ponieważ zarówno Integeri Stringdziedziczą z Object, można je przekonwertować na ten sam typ przez prostą konwersję zawężającą.

Z drugiej strony, w C #, an intjest prymitywem i nie ma niejawnej konwersji do stringani żadnej innej object.

Code-Apprentice
źródło
Dziękuję za odpowiedź. Obecnie myślę o autoboxingu. Czy C # nie zezwala na autoboxing?
blackr1234
2
Nie sądzę, aby to było poprawne, ponieważ umieszczenie int na obiekcie jest całkowicie możliwe w C #. Uważam, że różnica polega na tym, że C # po prostu przyjmuje bardzo swobodne podejście do określania, czy jest to dozwolone, czy nie.
Jeroen Vannevel
9
Nie ma to nic wspólnego z automatycznym boksem, który zdarza się tak samo często w C #. Różnica polega na tym, że C # nie próbuje znaleźć wspólnego nadtypu, podczas gdy Java (tylko od Java 5!) To robi. Możesz to łatwo sprawdzić, próbując zobaczyć, co się stanie, jeśli wypróbujesz to z 2 niestandardowymi klasami (które oczywiście dziedziczą po obiekcie). Istnieje również niejawna konwersja z intdo object.
Voo
3
I jeszcze trochę więcej: Konwersja z Integer/Stringna Objectnie jest konwersją zawężającą, jest dokładnie odwrotnie :-)
Voo
1
Integera Stringtakże zaimplementować, Serializablea Comparablewięc przypisania do któregokolwiek z nich również będą działać, np. Comparable<?> c=condition? 6: "6";lub List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();są legalnym kodem Java.
Holger
5

To jest całkiem proste. Nie ma niejawnej konwersji między ciągiem a int. operator trójskładnikowy wymaga, aby ostatnie dwa operandy miały ten sam typ.

Próbować:

Write(two > six ? two.ToString() : "6");
ChiralMichael
źródło
11
Pytanie brzmi, co powoduje różnicę między Javą a C #. To nie odpowiada na pytanie.
Jeroen Vannevel