Dlaczego wykonywanie kodu Java w komentarzach z pewnymi znakami Unicode jest dozwolone?

1356

Poniższy kod tworzy wynik „Hello World!” (nie, naprawdę, spróbuj).

public static void main(String... args) {

   // The comment below is not a typo.
   // \u000d System.out.println("Hello World!");
}

Powodem tego jest to, że kompilator Java analizuje znak Unicode \u000djako nową linię i zostaje przekształcony w:

public static void main(String... args) {

   // The comment below is not a typo.
   //
   System.out.println("Hello World!");
}

W ten sposób powstaje komentarz „wykonywany”.

Skoro można tego użyć do „ukrywania” złośliwego kodu lub czegokolwiek innego złego programistę, dlaczego jest to dozwolone w komentarzach ?

Dlaczego zezwala na to specyfikacja Java?

Reg
źródło
44
„Dlaczego to dozwolone” wydaje mi się zbyt oparte na opiniach. Projektanci języków podjęli decyzję, co jeszcze trzeba wiedzieć? O ile nie znajdziesz oświadczenia osoby podejmującej tę decyzję, możemy jedynie spekulować.
Ingo Bürk
194
Jedną interesującą rzeczą jest to, że IDE OP najwyraźniej źle to robi i wyświetla nieprawidłowe podświetlanie,
dhke
14
Możliwe powiązanie: stackoverflow.com/questions/4448180/…
dhke
47
@Tobb Ale projektanci Java odwiedzają SO, więc można uzyskać odpowiedzi od jednego z nich. Mogą też istnieć zasoby, które już odpowiadają na to pytanie.
Pshemo,
41
Prosta odpowiedź jest taka, że ​​kod nie jest w komentarzu, zgodnie z regułami języka, więc pytanie jest źle sformułowane.
user207421,

Odpowiedzi:

741

Dekodowanie Unicode odbywa się przed jakimkolwiek innym tłumaczeniem leksykalnym. Kluczową zaletą tego jest to, że sprawia, że ​​przechodzenie między ASCII a dowolnym innym kodowaniem jest banalne. Nie musisz nawet wymyślać, gdzie zaczynają się i kończą komentarze!

Jak stwierdzono w sekcji 3.3 JLS, pozwala to dowolnemu narzędziu opartemu na ASCII przetwarzać pliki źródłowe:

[...] Język programowania Java określa standardowy sposób przekształcania programu napisanego w Unicode w ASCII, który zmienia program w formę, którą można przetwarzać za pomocą narzędzi opartych na ASCII. [...]

Daje to fundamentalną gwarancję niezależności platformy (niezależności obsługiwanych zestawów znaków), która zawsze była kluczowym celem platformy Java.

Możliwość zapisania dowolnego znaku Unicode w dowolnym miejscu pliku jest ciekawą funkcją, szczególnie ważną w komentarzach, gdy dokumentuje się kod w językach innych niż łacińskie. Fakt, że może on wpływać na semantykę w tak subtelny sposób, jest po prostu (niefortunnym) efektem ubocznym.

Istnieje wiele gotch na ten temat, a Java Puzzlers Joshua Blocha i Neala Gaftera zawierały następujący wariant:

Czy to legalny program Java? Jeśli tak, co drukuje?

\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020
\u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079
\u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020
\u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063
\u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028
\u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020
\u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b
\u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074
\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020
\u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b
\u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d

(Ten program okazuje się być zwykłym programem „Hello World”).

W rozwiązaniu zagadki zwracają uwagę na następujące kwestie:

Mówiąc poważniej, ta łamigłówka służy wzmocnieniu lekcji poprzednich trzech: Ucieczki Unicode są niezbędne, gdy trzeba wstawić znaki, których nie można przedstawić w żaden inny sposób do programu. Unikaj ich we wszystkich innych przypadkach.


Źródło: Java: Wykonywanie kodu w komentarzach ?!

aioobe
źródło
84
Krótko mówiąc, Java celowo na to pozwala: „błąd” znajduje się w IDE PO?
Batszeba
60
@Bathsheba: To bardziej w głowach ludzi. Ludzie nie próbują zrozumieć, jak działa parsowanie Java, więc IDE czasami wyświetlają kod w niewłaściwy sposób. W powyższym przykładzie komentarz powinien kończyć się, \u000da część po nim powinna zawierać podświetlenia kodu.
Aaron Digulla,
62
Innym częstym błędem jest wklejanie ścieżek systemu Windows do kodu, // C:\user\...co prowadzi do błędu kompilacji, ponieważ \usernie jest prawidłową sekwencją specjalną Unicode.
Aaron Digulla,
50
W zaćmieniu Kod po \u000djest częściowo podświetlony. Po naciśnięciu Ctrl + Shift + F znak zostaje zastąpiony nową linią, a reszta linii jest zawijana
bluelDe
20
@TheLostMind Jeśli dobrze rozumiem odpowiedź, powinieneś być w stanie ją odtworzyć również z komentarzami blokowymi. \u002A/powinien zakończyć komentarz.
Taemyr
141

Ponieważ nie rozwiązano jeszcze tego problemu, oto wyjaśnienie, dlaczego tłumaczenie znaków unikodowych odbywa się przed jakimkolwiek innym przetwarzaniem kodu źródłowego:

Ideą było to, że umożliwia bezstratne tłumaczenia kodu źródłowego Java między różnymi kodowaniami znaków. Obecnie istnieje szerokie wsparcie dla Unicode i nie wygląda to na problem, ale wtedy deweloperowi z zachodniego kraju nie było łatwo otrzymać kod źródłowy od jego azjatyckiego kolegi zawierającego azjatyckie znaki, wprowadzić pewne zmiany ( w tym jego kompilacja i testowanie) i odesłanie wyniku, wszystko bez szkody.

Tak więc kod źródłowy Java może być napisany w dowolnym kodowaniu i pozwala na szeroki zakres znaków w obrębie identyfikatorów, znaków i String literałów oraz komentarzy. Następnie, aby przenieść go bezstratnie, wszystkie znaki nieobsługiwane przez kodowanie docelowe są zastępowane znakami ucieczki Unicode.

Jest to proces odwracalny, a interesującym punktem jest to, że tłumaczenie może być wykonane przez narzędzie, które nie musi nic wiedzieć o składni kodu źródłowego Java, ponieważ reguła tłumaczenia nie jest od niego zależna. Działa to, ponieważ tłumaczenie ich rzeczywistych znaków Unicode wewnątrz kompilatora odbywa się również niezależnie od składni kodu źródłowego Java. Oznacza to, że można wykonać dowolną liczbę kroków tłumaczenia w obu kierunkach bez zmiany znaczenia kodu źródłowego.

To jest powód kolejnej dziwnej funkcji, o której nawet nie wspomniano: \uuuuuuxxxxskładnia:

Gdy narzędzie do tłumaczenia ucieka przed znakami i napotyka sekwencję, która jest już sekwencją ucieczkową, powinna wstawić dodatkowy udo sekwencji, konwertując \ucafena \uucafe. Znaczenie nie zmienia się, ale podczas konwersji w innym kierunku narzędzie powinno po prostu usunąć jeden ui zastąpić tylko sekwencje zawierające pojedynczy uznakami Unicode. W ten sposób, nawet znaki ucieczki Unicode są zachowywane w oryginalnej formie podczas konwersji tam i z powrotem. Chyba nikt nigdy nie używał tej funkcji…

Holger
źródło
1
Co ciekawe, native2asciiwydaje się , że nie używa \uu...xxxxskładni,
ninjalj
5
Tak, native2asciimiał pomóc w przygotowaniu pakietów zasobów, przekształcając je w iso-latin-1, tak jak Properties.loadnaprawiono tylko do odczytu latin-1. I tam reguły są różne, bez \uuu…składni i bez wczesnego etapu przetwarzania. W plikach właściwości property=multi\u000alinejest rzeczywiście taki sam jak property=multi\nline. (Sprzeczne z wyrażeniem „używanie znaków ucieczki Unicode zgodnie z definicją w sekcji 3.3 specyfikacji języka Java ™” dokumentacji)
Holger
10
Zauważ, że ten cel projektowy mógłby zostać osiągnięty bez brodawek; najprościej byłoby zabraniać \uucieczkom generowania znaków w zakresie U + 0000–007F. (Wszystkie takie znaki mogą być reprezentowane natywnie przez wszystkie narodowe kodowania, które były istotne w latach 90. - no może oprócz niektórych znaków kontrolnych, ale i tak nie trzeba ich pisać w Javie.)
zwolnij
3
@zwol: cóż, jeśli wykluczysz znaki kontrolne, które i tak nie są dozwolone w kodzie źródłowym Java, masz rację. Niemniej jednak oznaczałoby to skomplikowanie zasad. A dzisiaj jest już za późno, aby omówić decyzję ...
Holger,
ah problem z zapisaniem dokumentu w utf8, a nie łacińskim czy czymś innym. Wszystkie moje bazy danych zostały również złamane z powodu tego zachodniego nonsensu
David 天宇 Wong,
106

Zamierzam całkowicie nieskutecznie dodać punkt, tylko dlatego, że nie mogę się powstrzymać i jeszcze go nie widziałem, że pytanie jest nieprawidłowe, ponieważ zawiera ukrytą przesłankę, która jest błędna, a mianowicie, że kod jest w komentarz!

W Javie kod źródłowy \ u000d jest pod każdym względem równoważny znakowi ASCII CR. Jest to linia kończąca się, prosta i prosta, gdziekolwiek się pojawi. Formatowanie w pytaniu jest mylące, czemu właściwie odpowiada ta sekwencja znaków:

public static void main(String... args) {
   // The comment below is no typo. 
   // 
 System.out.println("Hello World!");
}

IMHO najbardziej poprawną odpowiedzią jest zatem: kod jest wykonywany, ponieważ nie ma go w komentarzu; jest w następnej linii. „Wykonywanie kodu w komentarzach” jest niedozwolone w Javie, tak jak można się spodziewać.

Wiele zamieszania wynika z faktu, że wyróżniki składni i środowiska IDE nie są wystarczająco zaawansowane, aby uwzględnić tę sytuację. Albo wcale nie przetwarzają znaków ucieczki unicode, albo robią to po parsowaniu kodu, a nie przedtem, jak to javacrobi.

Pepijn Schmitz
źródło
6
Zgadzam się, to nie jest „błąd projektowy” javy, ale to błąd IDE.
bvdb
3
Pytanie dotyczy raczej tego, dlaczego kod, który wygląda jak komentarz dla kogoś, kto nie zna tego konkretnego aspektu języka i być może bez odniesienia do podświetlania składni, w rzeczywistości nie jest komentarzem. Sprzeciw oparty na przesłance nieważności pytania jest nieuczciwy.
Phil
@Phil: wygląda jak komentarz, gdy przegląda się go za pomocą określonych narzędzi, inni pokazują go inaczej.
jmoreno
1
@ jmoreno nie trzeba mieć nic więcej niż edytor tekstu do czytania kodu. Przynajmniej narusza zasadę najmniejszego zaskoczenia, a mianowicie, że komentarze w stylu // są kontynuowane aż do następnego \ n znaku - nie do żadnej innej sekwencji, która ostatecznie zostanie zastąpiona przez \ n. Komentarze nigdy nie powinny być niczym innym niż pozbawionymi treści. Zły preprocesor.
Phil
69

\u000dUcieczka kończy komentarz bo \uucieczek są równomiernie konwertowane do odpowiadających im znaków Unicode zanim program jest tokenized. Można równie używać \u0057\u0057zamiast //się rozpocząć komentarz.

Jest to błąd w twoim IDE, który powinien podświetlić składnię linii, aby było jasne, że \u000dkoniec komentarza.

Jest to również błąd projektowy w języku. Nie można go teraz poprawić, ponieważ spowodowałoby to uszkodzenie programów od niego zależnych. \uznaki ucieczki powinny być albo konwertowane na odpowiedni znak Unicode przez kompilator tylko w kontekstach, w których to „ma sens” (literały łańcuchowe i identyfikatory i prawdopodobnie nigdzie indziej) lub powinny być zabronione generowanie znaków w zakresie U + 0000–007F , lub obie. Każda z tych semantyków uniemożliwiłaby zakończenie komentarza przez \u000ducieczkę, bez ingerencji w przypadki, w których \uużyteczne są ucieczki - zauważ, że to obejmuje użycie \uucieczek wewnątrz komentarzy jako sposobu kodowania komentarzy w skrypcie innym niż łaciński, ponieważ edytor tekstów mógłby wziąć szerszy pogląd na to, gdzie\usekwencje specjalne są znaczące niż kompilator. (Nie znam jednak żadnego edytora ani IDE, które wyświetlają \uznaki specjalne jako odpowiednie znaki w dowolnym kontekście).

W rodzinie C występuje podobny błąd projektowy 1, w którym nowa kreska ułamkowa odwrócona jest przetwarzana przed określeniem granic komentarzy, np.

// this is a comment \
   this is still in the comment!

Przedstawiam to, aby zilustrować, że łatwo jest popełnić ten konkretny błąd projektowy, i nie zdaję sobie sprawy, że jest to błąd, dopóki nie jest za późno, aby go naprawić, jeśli jesteś przyzwyczajony do myślenia o tokenizacji i analizowaniu sposobu myślenia programistów kompilatora o tokenizacji i analizie. Zasadniczo, jeśli już zdefiniowałeś swoją formalną gramatykę, a następnie ktoś wymyślił specjalny składniowy przypadek - trigrafy, ukośnik-nowa linia, kodowanie dowolnych znaków Unicode w plikach źródłowych ograniczonych do ASCII, cokolwiek - które muszą być zaklinowane, łatwiej jest dodaj przepustkę transformacji wcześniej tokenizerem, aby zdefiniować tokenizator, aby zwrócić uwagę na to, gdzie warto użyć tego specjalnego przypadku.

1 Dla pedantów: Zdaję sobie sprawę, że ten aspekt C był w 100% zamierzony, a uzasadnienie - nie zmyślam tego - że pozwoli ci na mechaniczne dopasowanie kodu z dowolnie długimi liniami do perforowanych kart. To wciąż była niepoprawna decyzja projektowa.

zwol
źródło
17
Nie posunąłbym się nawet do stwierdzenia, że ​​to błąd projektowy . Mogę się z tobą zgodzić, że był to zły wybór projektu lub wybór o niefortunnych konsekwencjach, ale nadal uważam, że działa tak, jak zamierzali projektanci języka: umożliwia użycie dowolnego znaku Unicode w dowolnym miejscu pliku, przy zachowaniu kodowania ASCII pliku.
aioobe
12
To powiedziawszy, uważam, że wybór etapu przetwarzania dla \ubył mniej absurdalny niż decyzja o podążaniu za przykładem C w użyciu zer wiodących do notacji ósemkowej. Chociaż notacja ósemkowa jest czasem przydatna, jeszcze nie słyszałem, aby ktokolwiek wypowiedział argument, dlaczego wiodące zero jest dobrym sposobem na wskazanie tego.
supercat
3
@supercat Ludzie, którzy wrzucili tę funkcję do C89, uogólnili zachowanie oryginalnego preprocesora K&R, a nie zaprojektowali funkcję od zera. Wątpię, czy znali najlepsze praktyki z kartami dziurkowanymi, a także wątpię, czy ta funkcja była kiedykolwiek używana do jej określonego celu, z wyjątkiem może jednego lub dwóch ćwiczeń retrocomputing.
zwolnienie
8
@supercat Nie miałbym problemu z Javą \ujako transformacją przed tokenizacją, gdyby zabroniono tworzenia znaków w zakresie U + 0000..U + 007F. Jest to kombinacja „to działa wszędzie” i „aliasuje znaki ASCII o znaczeniu składniowym”, które obniżają go z niezręcznego do całkowicie błędnego.
zwolnienie
4
Na własną „dla pedantów”: Oczywiście w tym czasie// komentarz jednoliniowy nie istnieje . A ponieważ C ma terminator instrukcji, który nie jest nową linią, byłby głównie używany do długich łańcuchów, z wyjątkiem tego, o ile mogę ustalić, że „dosłowne łączenie łańcuchów znaków” było tam z K&R.
Mark Hurd
22

To był celowy wybór projektu, który sięga wstecz do pierwotnego projektu Java.

Dla tych, którzy pytają „kto chce, aby Unicode ucieka w komentarzach?”, Zakładam, że są to ludzie, których język ojczysty używa zestawu znaków łacińskich. Innymi słowy, nieodłącznym elementem oryginalnego projektu Javy jest to, że ludzie mogą używać dowolnych znaków Unicode wszędzie tam, gdzie jest to dozwolone w programie Java, najczęściej w komentarzach i ciągach znaków.

Jest to prawdopodobnie niedociągnięcie w programach (takich jak IDE) używanych do przeglądania tekstu źródłowego, że takie programy nie mogą interpretować znaków ucieczki Unicode i wyświetlać odpowiedniego glifu.

Jonathan Gibbons
źródło
8
Obecnie używamy UTF-8 dla naszego kodu źródłowego i możemy używać znaków Unicode bezpośrednio, bez potrzeby ucieczki.
Paŭlo Ebermann
21

Zgadzam się z @zwol, że jest to błąd projektowy; ale jestem jeszcze bardziej krytyczny.

\uEscape jest użyteczny w literałach łańcuchowych i znakach; i to jest jedyne miejsce, w którym powinno istnieć. Powinno być traktowane tak samo jak inne ucieczki \n; i "\u000A" powinno oznaczać dokładnie "\n".

W \uxxxxkomentarzach absolutnie nie ma sensu - nikt tego nie może przeczytać.

Podobnie nie ma sensu używać \uxxxxw innej części programu. Jedynym wyjątkiem są prawdopodobnie publiczne interfejsy API, które są zmuszane do umieszczania znaków innych niż ascii - kiedy ostatnio to widzieliśmy?

Projektanci mieli swoje powody w 1995 roku, ale 20 lat później wydaje się, że to zły wybór.

(pytanie do czytelników - dlaczego to pytanie wciąż zyskuje nowe głosy? czy to pytanie jest powiązane z jakiegoś miejsca popularnego?)

ZhongYu
źródło
5
Myślę, że nie kręcisz się, gdzie w interfejsach API używane są znaki inne niż ASCII. Są ludzie, którzy go używają (nie ja), np. W krajach azjatyckich. A kiedy używasz znaków innych niż ASCII w identyfikatorach, zabranianie ich w komentarzach do dokumentacji nie ma większego sensu. Niemniej jednak pozwolenie im na umieszczenie tokena i zmiana znaczenia lub granicy tokena to różne rzeczy.
Holger,
15
mogą używać właściwego kodowania plików. int \u5431po co pisz, kiedy możeszint 整
ZhongYu,
3
Co zrobisz, gdy będziesz musiał skompilować kod w oparciu o jego API i nie będziesz mógł użyć właściwego kodowania (załóż, że UTF-8w 1995 roku nie było powszechnego wsparcia). Musisz tylko wywołać jedną metodę i nie chcesz instalować pakietu wsparcia języka azjatyckiego dla swojego systemu operacyjnego (pamiętaj, lata dziewięćdziesiąte) dla tej jednej metody…
Holger
5
O wiele bardziej wyraźne niż w 1995 roku jest to, że lepiej znasz angielski, jeśli chcesz programować. Programowanie to interakcja międzynarodowa, a prawie wszystkie zasoby są w języku angielskim.
ZhongYu,
8
Nie sądzę, że to się zmieniło. Dokumentacja Javy była również w większości angielska. Przez pewien czas utrzymywano japońskie tłumaczenie, ale utrzymywanie dwóch języków tak naprawdę nie popiera pomysłu utrzymania go dla wszystkich lokalizacji na świecie (raczej go obalił). A przedtem i tak nie było języka głównego nurtu z obsługą Unicode w identyfikatorach. Sądzę, że ktoś pomyślał, że zlokalizowany kod źródłowy to kolejna wielka rzecz. Powiedziałbym na szczęście , że nie wystartował.
Holger,
11

Jedynymi osobami, które potrafią odpowiedzieć na pytanie, dlaczego ucieczki Unicode zostały zaimplementowane, są ludzie, którzy napisali specyfikację.

Prawdopodobnym powodem jest chęć dopuszczenia całego BMP jako możliwego znaku kodu źródłowego Java. Stanowi to jednak problem:

  • Chcesz móc używać dowolnego znaku BMP.
  • Chcesz mieć możliwość wprowadzania dowolnych znaków BMP w dość prosty sposób. Sposobem na to jest ucieczka z Unicode.
  • Chcesz, aby specyfikacja leksykalna była łatwa dla ludzi do czytania i pisania, a także stosunkowo łatwa do wdrożenia.

Jest to niezwykle trudne, gdy ucieczki Unicode wchodzą do walki: tworzy cały ładunek nowych reguł leksykalnych.

Najłatwiejszym rozwiązaniem jest wykonanie leksykalizacji w dwóch krokach: najpierw wyszukaj i zamień wszystkie znaki zmiany znaczenia Unicode na znak, który reprezentuje, a następnie przeanalizuj wynikowy dokument tak, jakby znaki zmiany znaczenia nie istniały.

Zaletą tego jest to, że jest łatwy do określenia, więc upraszcza specyfikację i jest łatwy do wdrożenia.

Minusem jest twój przykład.

Martijn
źródło
2
Lub ogranicz użycie \ uxxxx do identyfikatorów, literałów łańcuchowych i stałych znaków. To właśnie robi C11.
ninjalj
to naprawdę komplikuje reguły parsera, ponieważ to one definiują te rzeczy, a to, co spekuluję, jest częścią tego, dlaczego tak jest.
Martijn,