Robisz coś, co nie ma sensu, a potem nie podoba ci się komunikat, który generuje kompilator? To jest nieprawidłowy kod C # ... co dokładnie miałeś zrobić?
Hogan
19
@Hogan: Kod wydaje się na tyle przypadkowy i akademicki, że można go zadać z czystej ciekawości. Zgaduję ...
BoltClock
8
null + true zwraca 1 w JavaScript.
Fatih Acet
Odpowiedzi:
147
Choć może się to wydawać dziwne, po prostu przestrzega reguł ze specyfikacji języka C #.
Z sekcji 7.3.4:
Operacja w postaci x op y, gdzie op jest operatorem binarnym z możliwością przeciążenia, x jest wyrażeniem typu X, a y jest wyrażeniem typu Y, jest przetwarzana w następujący sposób:
Określany jest zbiór kandydujących operatorów zdefiniowanych przez użytkownika dostarczonych przez X i Y dla operatora operacji op (x, y). Zestaw składa się z sumy operatorów kandydatów dostarczonych przez X i operatorów kandydatów dostarczonych przez Y, z których każdy jest określony zgodnie z zasadami określonymi w § 7.3.5. Jeśli X i Y są tego samego typu lub jeśli X i Y pochodzą ze wspólnego typu podstawowego, to współdzielone operatory kandydujące występują tylko raz w połączonym zestawie.
Jeśli zbiór kandydujących operatorów zdefiniowanych przez użytkownika nie jest pusty, staje się to zbiorem kandydujących operatorów dla operacji. W przeciwnym razie predefiniowane implementacje operacji operatorów binarnych, w tym ich podniesione formularze, staną się zbiorem operatorów kandydujących do operacji. Predefiniowane implementacje danego operatora są określone w opisie operatora (§7.8 do §7.12).
Zasady rozwiązywania przeciążeń z § 7.5.3 są stosowane do zbioru kandydatów na operatorów w celu wybrania najlepszego operatora w odniesieniu do listy argumentów (x, y), a operator ten staje się wynikiem procesu rozwiązywania przeciążenia. Jeśli rozpoznanie przeciążenia nie wybierze jednego najlepszego operatora, wystąpi błąd w czasie powiązania.
A więc przejdźmy przez to po kolei.
X jest tutaj typem zerowym - lub w ogóle nie jest typem, jeśli chcesz o tym myśleć w ten sposób. Nie zapewnia żadnych kandydatów. Y to bool, co nie zapewnia żadnych +operatorów zdefiniowanych przez użytkownika . Więc pierwszy krok nie znajduje operatorów zdefiniowanych przez użytkownika.
Następnie kompilator przechodzi do drugiego punktu, przeglądając predefiniowany operator binarny + implementacje i ich podniesione formularze. Są one wymienione w sekcji 7.8.4 specyfikacji.
Jeśli przejrzysz te predefiniowane operatory, jedynym, który ma zastosowanie, jest string operator +(string x, object y). Zatem zestaw kandydatów ma jeden wpis. To sprawia, że ostatni punktor jest bardzo prosty ... rozwiązanie przeciążenia wybiera ten operator, podając ogólny typ wyrażenia string.
Ciekawostką jest to, że wystąpi to nawet wtedy, gdy dla niewymienionych typów dostępne są inne operatory zdefiniowane przez użytkownika. Na przykład:
// Foo defined Foo operator+(Foo foo, bool b)Foo f =null;Foo g = f +true;
W porządku, ale nie jest używany w przypadku literału o wartości null, ponieważ kompilator nie wie, aby do niego zajrzeć Foo. Wie tylko, aby wziąć pod uwagę, stringponieważ jest to predefiniowany operator wyraźnie wymieniony w specyfikacji. (W rzeczywistości, to nie operator określa typu string ... 1 ) Oznacza to, że będzie to nie kompilacji:
// Error: Cannot implicitly convert type 'string' to 'Foo'Foo f =null+true;
Inne typy drugiego argumentu będą oczywiście używać innych operatorów:
var x =null+0;// x is Nullable<int>var y =null+0L;// y is Nullable<long>var z =null+DayOfWeek.Sunday;// z is Nullable<DayOfWeek>
1 Możesz się zastanawiać, dlaczego nie ma operatora string +. To rozsądne pytanie i tylko zgaduję odpowiedź, ale rozważ to wyrażenie:
string x = a + b + c + d;
Gdyby stringnie było specjalnej wielkości liter w kompilatorze C #, to skończyłoby się równie efektywnie:
To utworzyło dwa niepotrzebne ciągi pośrednie. Jednakże, ponieważ kompilator ma specjalne wsparcie, w rzeczywistości jest w stanie skompilować powyższe jako:
string x =string.Concat(a, b, c, d);
który może utworzyć tylko pojedynczy ciąg o dokładnie odpowiedniej długości, kopiując wszystkie dane dokładnie raz. Miły.
„podając ogólny typ wyrażenia jako łańcuch” Po prostu się nie zgadzam. Błąd wynika z truebraku możliwości zamiany na string. Jeśli wyrażenie było prawidłowe, typ byłby string, ale w tym przypadku niepowodzenie konwersji na łańcuch powoduje, że całe wyrażenie jest błędem, a zatem nie ma typu.
leppie
6
@leppie: Wyrażenie jest prawidłowe, a jego typ to ciąg. Spróbuj "var x = null + true;" - kompiluje się i xjest typu string. Zauważ, że użyty tutaj podpis to string operator+(string, object)- jest konwertowany boolna object(co jest w porządku), a nie na string.
Jon Skeet
Dzięki Jon, zrozum teraz. BTW sekcja 14.7.4 w wersji 2 specyfikacji.
leppie
@leppie: Czy to jest w numeracji ECMA? To zawsze było zupełnie inne :(
Jon Skeet,
47
w 20 minut. Napisał to wszystko w 20 minut. Powinien napisać książkę czy coś… och, poczekaj.
Epaga
44
Powodem jest to, że po wprowadzeniu +w grę wchodzą reguły wiążące operatora C #. Rozważy zestaw +dostępnych operatorów i wybierze najlepsze przeciążenie. Jeden z tych operatorów jest następujący
stringoperator+(string x,object y)
To przeciążenie jest zgodne z typami argumentów w wyrażeniu null + true. W związku z tym jest wybierany jako operator i jest oceniany jako zasadniczo ((string)null) + trueoceniający do wartości "True".
Sekcja 7.7.4 specyfikacji języka C # zawiera szczegóły dotyczące tego rozwiązania.
Zauważyłem, że wygenerowany IL przechodzi bezpośrednio do wywołania Concat. Myślałem, że zobaczę tam wywołanie funkcji operatora +. Czy byłaby to po prostu optymalizacja wbudowana?
quentin-starin
1
@qstarin wierzę, że powodem jest to, że tak naprawdę nie ma operator+for string. Zamiast tego istnieje tylko w umyśle kompilatora i po prostu tłumaczy to na wezwaniastring.Concat
JaredPar
1
@qstarin @JaredPar: W mojej odpowiedzi zająłem się tym bardziej.
Jon Skeet
11
Kompilator wychodzi na poszukiwanie operatora + (), który może najpierw przyjąć pusty argument. Żaden ze standardowych typów wartości nie kwalifikuje się, null nie jest dla nich prawidłową wartością. Jedynym dopasowaniem jest System.String.operator + (), nie ma dwuznaczności.
Drugi argument tego operatora również jest łańcuchem. To idzie kapooey, nie można implicite przekonwertować bool na string.
nullzostanie rzutowany na łańcuch pusty i istnieje niejawny konwerter z wartości bool na łańcuch, więc truezostanie on rzutowany na łańcuch, a następnie +zostanie zastosowany operator: wygląda to tak: string str = "" + true.ToString ();
var b =(null+DateTime.Now);// Stringvar b =(null+1);// System.Nullable<Int32> | same with System.Single, System.Double, System.Decimal, System.TimeSpan etcvar b =(null+newObject());// String | same with any ref type
Powodem tego jest wygoda (łączenie ciągów jest częstym zadaniem).
Jak powiedział BoltClock, operator „+” jest zdefiniowany dla typów liczbowych, ciągów znaków i może być również zdefiniowany dla naszych własnych typów (przeciążanie operatorów).
Jeśli nie ma przeciążonego operatora „+” w typach argumentów i nie są to typy liczbowe, kompilator domyślnie wykonuje konkatenację ciągów.
Kompilator wstawia wywołanie do, String.Concat(...)gdy łączysz się za pomocą „+”, a implementacja Concat wywołuje ToString na każdym przekazanym obiekcie.
Odpowiedzi:
Choć może się to wydawać dziwne, po prostu przestrzega reguł ze specyfikacji języka C #.
Z sekcji 7.3.4:
A więc przejdźmy przez to po kolei.
X jest tutaj typem zerowym - lub w ogóle nie jest typem, jeśli chcesz o tym myśleć w ten sposób. Nie zapewnia żadnych kandydatów. Y to
bool
, co nie zapewnia żadnych+
operatorów zdefiniowanych przez użytkownika . Więc pierwszy krok nie znajduje operatorów zdefiniowanych przez użytkownika.Następnie kompilator przechodzi do drugiego punktu, przeglądając predefiniowany operator binarny + implementacje i ich podniesione formularze. Są one wymienione w sekcji 7.8.4 specyfikacji.
Jeśli przejrzysz te predefiniowane operatory, jedynym, który ma zastosowanie, jest
string operator +(string x, object y)
. Zatem zestaw kandydatów ma jeden wpis. To sprawia, że ostatni punktor jest bardzo prosty ... rozwiązanie przeciążenia wybiera ten operator, podając ogólny typ wyrażeniastring
.Ciekawostką jest to, że wystąpi to nawet wtedy, gdy dla niewymienionych typów dostępne są inne operatory zdefiniowane przez użytkownika. Na przykład:
W porządku, ale nie jest używany w przypadku literału o wartości null, ponieważ kompilator nie wie, aby do niego zajrzeć
Foo
. Wie tylko, aby wziąć pod uwagę,string
ponieważ jest to predefiniowany operator wyraźnie wymieniony w specyfikacji. (W rzeczywistości, to nie operator określa typu string ... 1 ) Oznacza to, że będzie to nie kompilacji:Inne typy drugiego argumentu będą oczywiście używać innych operatorów:
1 Możesz się zastanawiać, dlaczego nie ma operatora string +. To rozsądne pytanie i tylko zgaduję odpowiedź, ale rozważ to wyrażenie:
Gdyby
string
nie było specjalnej wielkości liter w kompilatorze C #, to skończyłoby się równie efektywnie:To utworzyło dwa niepotrzebne ciągi pośrednie. Jednakże, ponieważ kompilator ma specjalne wsparcie, w rzeczywistości jest w stanie skompilować powyższe jako:
który może utworzyć tylko pojedynczy ciąg o dokładnie odpowiedniej długości, kopiując wszystkie dane dokładnie raz. Miły.
źródło
true
braku możliwości zamiany nastring
. Jeśli wyrażenie było prawidłowe, typ byłbystring
, ale w tym przypadku niepowodzenie konwersji na łańcuch powoduje, że całe wyrażenie jest błędem, a zatem nie ma typu.x
jest typustring
. Zauważ, że użyty tutaj podpis tostring operator+(string, object)
- jest konwertowanybool
naobject
(co jest w porządku), a nie nastring
.Powodem jest to, że po wprowadzeniu
+
w grę wchodzą reguły wiążące operatora C #. Rozważy zestaw+
dostępnych operatorów i wybierze najlepsze przeciążenie. Jeden z tych operatorów jest następującyTo przeciążenie jest zgodne z typami argumentów w wyrażeniu
null + true
. W związku z tym jest wybierany jako operator i jest oceniany jako zasadniczo((string)null) + true
oceniający do wartości"True"
.Sekcja 7.7.4 specyfikacji języka C # zawiera szczegóły dotyczące tego rozwiązania.
źródło
operator+
forstring
. Zamiast tego istnieje tylko w umyśle kompilatora i po prostu tłumaczy to na wezwaniastring.Concat
Kompilator wychodzi na poszukiwanie operatora + (), który może najpierw przyjąć pusty argument. Żaden ze standardowych typów wartości nie kwalifikuje się, null nie jest dla nich prawidłową wartością. Jedynym dopasowaniem jest System.String.operator + (), nie ma dwuznaczności.
Drugi argument tego operatora również jest łańcuchem. To idzie kapooey, nie można implicite przekonwertować bool na string.
źródło
Co ciekawe, używając Reflectora do sprawdzenia, co jest generowane, poniższy kod:
jest przekształcany przez kompilator do tego:
Muszę powiedzieć, że powód tej „optymalizacji” jest nieco dziwny i nie rymuje się z wyborem operatora, którego bym oczekiwał.
Ponadto poniższy kod:
jest przekształcany w
gdzie
string b = true;
faktycznie nie jest akceptowany przez kompilator.źródło
null
zostanie rzutowany na łańcuch pusty i istnieje niejawny konwerter z wartości bool na łańcuch, więctrue
zostanie on rzutowany na łańcuch, a następnie+
zostanie zastosowany operator: wygląda to tak: string str = "" + true.ToString ();jeśli sprawdzisz to za pomocą Ildasm:
string str = null + true;
jest jak poniżej:
źródło
Zwariowany?? Nie, musi być ku temu powód.
Niech ktoś zadzwoni
Eric Lippert
...źródło
Powodem tego jest wygoda (łączenie ciągów jest częstym zadaniem).
Jak powiedział BoltClock, operator „+” jest zdefiniowany dla typów liczbowych, ciągów znaków i może być również zdefiniowany dla naszych własnych typów (przeciążanie operatorów).
Jeśli nie ma przeciążonego operatora „+” w typach argumentów i nie są to typy liczbowe, kompilator domyślnie wykonuje konkatenację ciągów.
Kompilator wstawia wywołanie do,
String.Concat(...)
gdy łączysz się za pomocą „+”, a implementacja Concat wywołuje ToString na każdym przekazanym obiekcie.źródło