Dlaczego ten kod, napisany wstecz, wypisuje „Hello World!”

261

Oto kod, który znalazłem w Internecie:

class M‮{public static void main(String[]a‭){System.out.print(new char[]
{'H','e','l','l','o',' ','W','o','r','l','d','!'});}}    

Ten kod drukuje Hello World!się na ekranie; możesz zobaczyć, jak to działa tutaj . Wyraźnie widzępublic static void main napisane, ale jest odwrotnie. Jak działa ten kod? Jak to się nawet kompiluje?

Edycja: Wypróbowałem ten kod w IntellIJ i działa dobrze. Jednak z jakiegoś powodu nie działa w Notatniku ++, wraz z cmd. Nadal nie znalazłem rozwiązania tego problemu, więc jeśli ktoś to zrobi, skomentuj poniżej.

Wyimaginowana Dynia
źródło
38
Ten jest zabawny ... Masz coś wspólnego ze wsparciem RTL?
Eugene Sh.
12
Istnieje znak Unicode # 8237; zaraz po Mi również po []a: fileformat.info/info/unicode/char/202d/index.htm To się nazywa LEWE DO PRAWEGO OVERRIDE
Riiverside
45
obowiązkowe xkcd: xkcd.com/1137
Pac0
4
Możesz bardzo łatwo zobaczyć, co się tutaj dzieje, po prostu dokonując wyborów we fragmencie kodu za pomocą myszy.
Andreas Rejbrand
14
niam diov citats cilbupbrzmi jak przysłowie łacińskie ..
Mick Mnemonic

Odpowiedzi:

250

Są tu niewidoczne znaki, które zmieniają sposób wyświetlania kodu. W Intellij można je znaleźć, wklejając kod do pustego łańcucha ("" ), który zastępuje je znakami ucieczki Unicode, usuwając ich efekty i ujawniając kolejność, jaką widzi kompilator.

Oto wynik tej kopiuj-wklej:

"class M\u202E{public static void main(String[]a\u202D){System.out.print(new char[]\n"+
        "{'H','e','l','l','o',' ','W','o','r','l','d','!'});}}   "

Znaki kodu źródłowego są przechowywane w tej kolejności, a kompilator traktuje je jako znajdujące się w tej kolejności, ale są wyświetlane inaczej.

Zwróć uwagę na \u202Eznak, który jest przesłonięciem od prawej do lewej, rozpoczynając blok, w którym wszystkie znaki muszą być wyświetlane od prawej do lewej, oraz\u202D lewej do prawej, rozpoczynając zagnieżdżony blok, w którym wszystkie znaki są wymuszane w kolejności od lewej do prawej, zastępując pierwszą zmianę.

Ergo, gdy wyświetla oryginalny kod, class Mjest wyświetlane normalnie, ale \u202Eodwraca kolejność wyświetlania wszystkiego od tego do \u202D, co odwraca wszystko ponownie. (Formalnie wszystko od \u202Dterminatora do wiersza jest dwukrotnie odwracane, raz z powodu \u202Di raz z resztą tekstu odwróconego z powodu \u202E, i dlatego ten tekst pojawia się na środku linii zamiast na końcu.) Kierunkowość następnej linii jest obsługiwana niezależnie od pierwszej z powodu zakończenia linii, więc {'H','e','l','l','o',' ','W','o','r','l','d','!'});}}jest wyświetlana normalnie.

Aby zapoznać się z pełnym (niezwykle złożonym, dziesiątkami stron) algorytmem dwukierunkowym Unicode, zobacz Standardowy kod Unicode, załącznik nr 9 .

Davis Broda
źródło
Nie wyjaśniasz, co robi kompilator (w przeciwieństwie do procedury wyświetlania) z tymi samymi znakami Unicode. Mogę je całkowicie zignorować (lub potraktować jako białe znaki), lub może to zinterpretować jako faktycznie przyczyniające się do kodu źródłowego. Nie znam tutaj reguł Java, ale fakt, że są one umieszczone na końcu nieużywanych w inny sposób identyfikatorów sugeruje mi, że mogą to być te ostatnie, a znaki Unicode są w rzeczywistości częścią tych nazw identyfikatorów.
Marc van Leeuwen,
Czy zadziałałoby to w ten sam sposób w c #, poza zainteresowaniem?
IanF1,
14
@ IanF1 Działa w każdym języku, w którym kompilator / interpreter liczy znaki RTL i LTR jako białe znaki. Ale nigdy nie rób tego w kodzie produkcyjnym, jeśli w ogóle cenisz zdrowie psychiczne następnej osoby, która dotknie twojego kodu, którą możesz być ty.
wizzwizz4
2
Lub innymi słowy: „Zawsze koduj tak, jakby osoba, która kończy utrzymywanie twojego kodu, była agresywnym psychopatą, który wie, gdzie mieszkasz”. , @ IanF1. A może: „Zawsze koduj tak, jakby osoba, która skończy utrzymywać Twój kod, nazwałaby cię i zawstydzi jako oryginalny autor w Stack Overflow”.
Cody Gray
43

Wygląda inaczej ze względu na dwukierunkowy algorytm Unicode . Istnieją dwa niewidoczne znaki RLO i LRO, których używa dwukierunkowy algorytm Unicode do zmiany wyglądu znaków zagnieżdżonych między tymi dwoma metaznakami.

Powoduje to, że wizualnie wyglądają w odwrotnej kolejności, ale rzeczywiste znaki w pamięci nie są odwracane. Możesz przeanalizować wyniki tutaj . Kompilator Java zignoruje RLO i LRO i potraktuje je jako białe znaki, dlatego kompiluje kod.

Uwaga 1: Ten algorytm jest używany przez edytory tekstu i przeglądarki do wizualnego wyświetlania znaków jednocześnie zarówno znaków LTR (angielski), jak i RTL (np. Arabski, hebrajski) jednocześnie - stąd „dwukierunkowy”. Możesz przeczytać więcej o algorytmie dwukierunkowym na stronie Unicode .
Uwaga 2: Dokładne zachowanie LRO i RLO zdefiniowano w sekcji 2.2 algorytmu.

James Lawson
źródło
Jaki jest cel takiej zdolności?
Eugene Sh.
6
Te znaki są czasem potrzebne do poprawnego renderowania arabskiego i hebrajskiego. Języki te są odczytywane i zapisywane od prawej do lewej (RTL), pierwszy znak, który jest odczytywany / zapisywany, pojawia się po prawej stronie . Możesz przeczytać więcej tutaj .
James Lawson
Arabskie i hebrajskie znaki są z natury RTL - pojawią się w RTL nawet bez wyraźnego zastąpienia, a nawet automatycznie odwrócą kolejność niektórych innych znaków w pobliżu, myślę, że głównie interpunkcja - więc wyraźne zastąpienia są rzadko konieczne.
user2357112 obsługuje Monikę
Ta strona tutaj opisano, gdy zastępuje są konieczne. @ user2357112 ma rację, rzadko są potrzebne. Rzeczywiście, gdy masz interpunkcję, cytaty i cyfry - te znaki specjalne są uważane za „neutralne”. W przypadku komputera, który nie potrafi odczytać słów i nie rozumie kontekstu, nie jest jasne, czy należy je traktować jako LTR czy RTL, ale algorytm bidi musi wybrać pewne uporządkowanie. Czasami „robi to źle” i musisz użyć tych znaków zastępujących, aby „poprawić”.
James Lawson
3
Również U + 202E i U + 202D nie są uważane za białe znaki. Java bierze pod uwagę tylko spacje ASCII, tabulację poziomą, wysuw formularza i CR / LF / CRLF . W rzeczywistości są one leksykalnie częścią identyfikatorów M\u202Ei a\u202D, ale te identyfikatory wydają się traktowane jako równoważne z Mi a. (JLS nie radzi sobie dobrze z wyjaśnianiem tego.)
user2357112 obsługuje Monikę
28

Znak U+202Eodzwierciedla kod od prawej do lewej, jest jednak bardzo sprytny. Jest ukryty, zaczynając od litery M,

"class M\u202E{..."

Jak znalazłem za tym magię ?

Cóż, na początku, kiedy zobaczyłem trudne pytanie: „to rodzaj żartu, stracić kogoś innego czasu”, ale potem otworzyłem swoje IDE („IntelliJ”), stworzyłem klasę i przekroczyłem kod ... i to się skompilowało !!! Spojrzałem więc lepiej i zobaczyłem, że „publiczna pustka statyczna” była zacofana, więc poszedłem tam z kursorem i usunąłem kilka znaków ... A co się stanie? Znaki zaczęły się wymazywać wstecz , więc pomyślałem mmm .... rzadko ... muszę go wykonać ... Więc kontynuuję wykonywanie programu, ale najpierw musiałem go zapisać ... i to wtedy znalazłem to! . Nie mogłem zapisać pliku, ponieważ moje IDE powiedziało, że dla niektórych znaków istnieje inne kodowanie, i wskazuj mi, gdzie to było, Więc zaczynam badania w Google w celu znalezienia specjalnych znaków, które mogłyby wykonać zadanie i to wszystko :)

Trochę o

dwukierunkowy algorytm Unicode i U+202Epowiązane krótkie wyjaśnienie :

Standard Unicode określa porządek reprezentacji pamięci zwany porządkiem logicznym. Kiedy tekst jest prezentowany w poziomych liniach, większość skryptów wyświetla znaki od lewej do prawej. Istnieje jednak kilka skryptów (takich jak arabski lub hebrajski), w których naturalne uporządkowanie wyświetlanego tekstu poziomego odbywa się od prawej do lewej. Jeśli cały tekst ma jednolity kierunek poziomy, kolejność wyświetlanego tekstu jest jednoznaczna.

Ponieważ jednak te skrypty od prawej do lewej używają cyfr zapisanych od lewej do prawej, tekst jest w rzeczywistości dwukierunkowy: mieszanka tekstu od prawej do lewej i od lewej do prawej. Oprócz cyfr, od lewej do prawej strony pisane są również słowa z języka angielskiego i innych skryptów, które również generują tekst dwukierunkowy. Bez jasnej specyfikacji mogą pojawić się niejasności przy ustalaniu kolejności wyświetlanych znaków, gdy poziomy kierunek tekstu nie jest jednolity.

W tym załączniku opisano algorytm stosowany do określania kierunkowości dwukierunkowego tekstu Unicode. Algorytm rozszerza domyślny model wykorzystywany obecnie przez szereg istniejących implementacji i dodaje jawne znaki formatujące w szczególnych okolicznościach. W większości przypadków nie ma potrzeby dołączania dodatkowych informacji do tekstu, aby uzyskać prawidłowe uporządkowanie wyświetlania.

Jednak w przypadku tekstu dwukierunkowego istnieją okoliczności, w których niejawne uporządkowanie dwukierunkowe nie jest wystarczające do uzyskania zrozumiałego tekstu. Aby poradzić sobie z tymi przypadkami, zdefiniowano minimalny zestaw znaków formatowania kierunkowego, aby kontrolować kolejność znaków podczas renderowania. Umożliwia to dokładną kontrolę kolejności wyświetlania w celu czytelnej wymiany i zapewnia, że ​​zwykły tekst używany do prostych elementów, takich jak nazwy plików lub etykiety, zawsze może być poprawnie uporządkowany do wyświetlenia.

Dlaczego stworzyć jakiś algorytm, jak to ?

algorytm bidi może renderować sekwencję znaków arabskich lub hebrajskich jeden po drugim od prawej do lewej.

Damián Rafael Lattenero
źródło
4

Rozdział 3 specyfikacji języka zawiera wyjaśnienie, szczegółowo opisując, w jaki sposób wykonuje się tłumaczenie leksykalne dla programu Java. Co jest najważniejsze dla pytania:

Programy są napisane w Unicode (§3.1) , ale zapewnione są tłumaczenia leksykalne (§3.2), dzięki czemu znaki ucieczki Unicode (§3.3) mogą być użyte do włączenia dowolnego znaku Unicode zawierającego tylko znaki ASCII.

Tak więc program jest zapisany w postaci znaków Unicode, a autor może je uciec, \uxxxxna wypadek, gdyby kodowanie pliku nie obsługiwało znaku Unicode, w którym to przypadku jest tłumaczone na odpowiedni znak. Jednym ze znaków Unicode obecnych w tym przypadku jest \u202E. Nie jest pokazywany wizualnie we fragmencie, ale jeśli spróbujesz przełączyć kodowanie przeglądarki, mogą pojawić się ukryte znaki.

Dlatego tłumaczenie leksykalne powoduje deklarację klasy:

class M\u202E{

co oznacza, że ​​identyfikator klasy to M\u202E. Specyfikacja uważa to za ważny identifer:

Identifier:
    IdentifierChars but not a Keyword or BooleanLiteral or NullLiteral
IdentifierChars:
    JavaLetter {JavaLetterOrDigit}

„Litera lub cyfra Java” to znak, dla którego metoda Character.isJavaIdentifierPart(int)zwraca true.

M. Anouti
źródło
Przepraszam, ale to jest wstecz (zamierzone słowo). W kodzie źródłowym nie ma znaków ucieczki; opisujesz, jak to mogło być napisane. I kompiluje się do klasy o nazwie „M” (tylko jeden znak).
Tom Blodget
@TomBlodget Rzeczywiście, ale chodzi o to, że (co w rzeczywistości podkreśliłem w cytacie specyfikacji) jest to, że kompilator może również przetwarzać nieprzetworzone znaki Unicode. To naprawdę całe wyjaśnienie. Tłumaczenie specjalne jest tylko dodatkową informacją i nie jest bezpośrednio związane z tą sprawą. Jeśli chodzi o klasę skompilowaną, to chyba dlatego, że kompilator w jakiś sposób odrzuca znak przełącznika RTL. Spróbuję sprawdzić, czy jest to oczekiwane, ale myślę, że dzieje się to po fazie tłumaczenia leksykalnego.
M Anouti