Почему этот код, написанный в обратном порядке, печатает «Hello World!»

261

Вот код, который я нашел в интернете:

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

Этот код печатается Hello World!на экране; Вы можете видеть, что это бежит здесь . Я ясно вижу public static void mainнаписанное, но это задом наперед. Как работает этот код? Как это вообще компилируется?

Редактировать: я попробовал этот код в IntellIJ, и он отлично работает. Однако по какой-то причине он не работает в notepad ++ вместе с cmd. Я до сих пор не нашел решения этой проблемы, поэтому, если кто-нибудь найдет, прокомментируйте ниже.

Мнимая Тыква
источник
38
Это забавно ... Какое-то отношение к поддержке RTL?
Евгений Ш.
12
Есть Unicode персонаж # 8237; сразу после, Mа также после []a: fileformat.info/info/unicode/char/202d/index.htm. Он называется LEFT-TO-RIGHT OVERRIDE
Риверсайд
45
обязательно xkcd: xkcd.com/1137
Pac0
4
Вы можете очень легко увидеть, что здесь происходит, просто сделав выборки во фрагменте кода с помощью мыши.
Андреас Рейбранд
14
niam diov citats cilbupзвучит как латинская пословица ..
Мик Мнемоник

Ответы:

250

Здесь есть невидимые символы, которые изменяют способ отображения кода. В Intellij их можно найти, скопировав код в пустую строку ("" ), которая заменяет их на экранированные Unicode, удаляя их эффекты и раскрывая порядок, который видит компилятор.

Вот результат этой копии-вставки:

"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','!'});}}   "

Символы исходного кода хранятся в этом порядке, и компилятор обрабатывает их как в этом порядке, но они отображаются по-разному.

Обратите внимание на \u202Eсимвол, который является переопределением справа налево, начиная блок, где все символы должны отображаться справа налево, и\u202D слева направо, начиная вложенный блок, где все символы приводятся в порядке слева направо, переопределяя первое переопределение.

Ergo, когда он отображает исходный код, class Mотображается нормально, но \u202Eменяет порядок отображения всего оттуда до \u202D, что снова меняет все. (Формально все, начиная с конца и до конца \u202Dстроки, меняется на противоположное дважды, один раз из-за \u202Dи один раз, а остальная часть текста перевернута из-за \u202E, поэтому этот текст отображается в середине строки вместо конца.) Направленность следующей строки обрабатывается независимо от первой из-за ограничителя строки, поэтому {'H','e','l','l','o',' ','W','o','r','l','d','!'});}}отображается нормально.

Для полного (чрезвычайно сложного, двунаправленного алгоритма Unicode) см. Приложение № 9 к Стандартному Unicode .

Дэвис Брода
источник
Вы не объясняете, что делает компилятор (в отличие от процедуры отображения) с этими символами Unicode. Я мог бы полностью игнорировать их (или рассматривать их как пробелы), или это могло бы интерпретировать их как фактический вклад в исходный код. Я не знаю здесь правил Java, но тот факт, что они размещаются в конце неиспользуемых идентификаторов, подсказывает мне, что это могут быть последние, и символы Unicode фактически являются частью этих имен идентификаторов.
Марк ван Леувен
Будет ли это работать так же, как в C #, из интереса?
IanF1
14
@ IanF1 Это будет работать на любом языке, где компилятор / интерпретатор считает символы RTL и LTR пробелами. Но никогда не делайте этого в производственном коде, если вы вообще цените здравомыслие следующего человека, который прикоснется к вашему коду, который вполне может быть вами.
wizzwizz4
2
Или, другими словами: «Всегда кодируйте так, как будто человек, который в конечном итоге поддерживает ваш код, является жестоким психопатом, который знает, где вы живете». @ IanF1. Или, возможно: «Всегда пишите код, как если бы человек, который в итоге поддерживал ваш код, назвал вас и опозорил как первоначального автора в переполнении стека».
Коди Грей
43

Это выглядит иначе из-за двунаправленного алгоритма Unicode . Существует два невидимых символа RLO и LRO, которые двунаправленный алгоритм Unicode использует для изменения визуального представления символов, вложенных между этими двумя метасимволами.

В результате они визуально выглядят в обратном порядке, но действительные символы в памяти не меняются местами. Вы можете проанализировать результаты здесь . Компилятор Java будет игнорировать RLO и LRO и обрабатывать их как пробелы, поэтому код компилируется.

Примечание 1: Этот алгоритм используется текстовыми редакторами и браузерами для визуального отображения символов как символов LTR (английский), так и символов RTL (например, арабский, иврит) одновременно - следовательно, «bi» -направленный. Вы можете прочитать больше о Двунаправленном алгоритме на сайте Unicode .
Примечание 2: Точное поведение LRO и RLO определено в разделе 2.2 Алгоритма.

Джеймс Лоусон
источник
Какова цель такой возможности?
Евгений Ш.
6
Эти символы необходимы иногда для визуальной визуализации арабского и иврита. Эти языки читаются и пишутся справа налево (RTL), первый символ, который читается / пишется, появляется справа . Вы можете прочитать больше здесь .
Джеймс Лоусон
Однако арабские и ивритские символы по сути являются RTL - они будут отображаться RTL даже без явного переопределения, и они даже автоматически изменят порядок расположения некоторых других символов поблизости, я думаю, что в основном это знаки препинания - поэтому явные переопределения редко нужны.
user2357112 поддерживает Monica
Эта страница здесь описывает, когда необходимы переопределения. @ user2357112 прав, они редко нужны. Действительно, когда у вас есть знаки препинания, кавычки и цифры - эти специальные символы считаются «нейтральными». Для компьютера, который не может прочитать слова и понять контекст, неясно, следует ли рассматривать их как LTR или RTL, но алгоритм двунаправленного текста должен выбрать некоторый порядок. Иногда это «неправильно», и вам нужно использовать эти переопределенные символы, чтобы «исправить».
Джеймс Лоусон
3
Кроме того, U + 202E и U + 202D не считаются пробелами. Java рассматривает только пробел ASCII, горизонтальную табуляцию, подачу формы и CR / LF / CRLF . Они на самом деле являются лексической частью идентификаторов M\u202Eи a\u202D, но эти идентификаторы, по-видимому, рассматриваются как эквивалентные Mи a. (JLS не очень хорошо объясняет это.)
user2357112 поддерживает Монику
28

Персонаж U+202Eотражает код справа налево, хотя он очень умный. Скрыт, начиная с М,

"class M\u202E{..."

Как я нашел магию за этим?

Ну, сначала, когда я увидел жесткий вопрос, «это своего рода шутка, потерять кого-то еще раз», но затем я открыл свою IDE («IntelliJ»), создал класс и пропустил код ... и это скомпилировано !!! Итак, я посмотрел получше и увидел, что «общедоступная статическая пустота» была задом наперед, поэтому я пошел туда с курсором и удалил несколько символов ... И что происходит? Символы начали стираться задом наперед , поэтому я подумал, ммм ... редко ... я должен выполнить ее ... Итак, я продолжаю выполнять программу, но сначала мне нужно было сохранить ее ... и это было, когда я нашел это! , Я не смог сохранить файл, потому что моя IDE сказала, что для некоторого символа была другая кодировка, и укажи мне, где это былоИтак, я начинаю исследование в Google для специальных символов, которые могли бы сделать работу, и все :)

Немного о

двунаправленный алгоритм Unicode, и U+202Eвкратце, объясню :

Стандарт Unicode предписывает порядок представления памяти, известный как логический порядок. Когда текст представлен горизонтальными линиями, большинство сценариев отображают символы слева направо. Однако есть несколько сценариев (таких как арабский или иврит), в которых естественный порядок горизонтального текста на дисплее отображается справа налево. Если весь текст имеет одинаковое горизонтальное направление, то порядок отображения текста на экране однозначен.

Однако, поскольку эти сценарии справа налево используют цифры, которые пишутся слева направо, текст на самом деле является двунаправленным: смесь текста справа налево и слева направо. В дополнение к цифрам, встроенные слова из английского и других алфавитов также пишутся слева направо, также создавая двунаправленный текст. Без четкой спецификации могут возникнуть неоднозначности при определении порядка отображаемых символов, когда горизонтальное направление текста не является равномерным.

В этом приложении описан алгоритм, используемый для определения направленности для двунаправленного текста Unicode. Алгоритм расширяет неявную модель, используемую в настоящее время рядом существующих реализаций, и добавляет явные символы форматирования для особых обстоятельств. В большинстве случаев нет необходимости включать дополнительную информацию в текст для получения правильного порядка отображения.

Однако в случае двунаправленного текста существуют обстоятельства, когда неявного двунаправленного упорядочения недостаточно для создания понятного текста. Чтобы справиться с этими случаями, минимальный набор символов направленного форматирования определяется для управления порядком символов при визуализации. Это позволяет точно контролировать порядок отображения для разборчивого обмена и гарантирует, что простой текст, используемый для простых элементов, таких как имена файлов или метки, всегда может быть правильно упорядочен для отображения.

Зачем создавать такой алгоритм, как этот ?

алгоритм двунаправленного текста может отображать последовательность символов арабского или иврита один за другим справа налево.

Дамиан Рафаэль Латтенеро
источник
4

Глава 3 спецификации языка дает объяснение, подробно описывая, как осуществляется лексический перевод для Java-программы. Что важнее всего для вопроса:

Программы написаны на Unicode (§3.1) , но предоставляются лексические переводы (§3.2), так что экранирование Unicode (§3.3) может использоваться для включения любого символа Unicode, использующего только символы ASCII.

Таким образом, программа написана в символах Unicode, и автор может избежать их использования, \uxxxxесли кодировка файла не поддерживает символ Unicode, и в этом случае она переводится в соответствующий символ. Одним из символов Unicode, присутствующих в этом случае, является \u202E. Он не отображается визуально во фрагменте, но если вы попытаетесь переключить кодировку браузера, могут появиться скрытые символы.

Следовательно, лексический перевод приводит к объявлению класса:

class M\u202E{

что означает , что идентификатор класса M\u202E. Спецификация рассматривает это как действительные идентификтор:

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

«Буква или цифра Java» - это символ, для которого метод Character.isJavaIdentifierPart(int)возвращает true.

М Аноути
источник
Извините, но это назад (каламбур). В исходном коде нет переходов; Вы описываете, как это могло быть написано. И он компилируется в класс с именем «M» (всего один символ).
Том Блоджет
@TomBlodget Действительно, но суть (которую я особо подчеркнул в цитате спецификации) заключается в том, что компилятор также может обрабатывать необработанные символы Юникода. Это действительно все объяснение. Эскейп-перевод - это просто дополнительная информация, которая не имеет прямого отношения к этому делу. Что касается скомпилированного класса, я думаю, это потому, что символ переключателя RTL каким-то образом отбрасывается компилятором. Я постараюсь увидеть, ожидается ли это, но я думаю, что это произойдет после фазы лексического перевода.
М Anouti