Почему разрешено выполнение кода Java в комментариях с определенными символами Unicode?

1356

Следующий код производит вывод "Hello World!" (нет, на самом деле, попробуйте).

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

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

Причина этого заключается в том, что компилятор Java анализирует символ Unicode \u000dкак новую строку и преобразуется в:

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

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

Это приводит к тому, что комментарий «исполняется».

Поскольку это можно использовать для «сокрытия» вредоносного кода или всего, что может представить злой программист, почему это разрешено в комментариях ?

Почему это разрешено спецификацией Java?

Редж
источник
44
«Почему это разрешено» мне кажется слишком основанным на мнении. Языковые дизайнеры приняли решение, что еще нужно знать? Если вы не найдете утверждение человека, принимающего такое решение, мы можем только строить догадки.
Инго Бюрк
194
Одна интересная вещь, по крайней мере, в том, что IDE OP явно ошибается и отображает некорректную подсветку,
dhke
14
Возможно связано: stackoverflow.com/questions/4448180/…
dhke
47
@Tobb Но Java-дизайнеры посещают SO, поэтому можно получить ответы по одному из них. Также могут существовать ресурсы, которые уже отвечают на этот вопрос.
Пшемо
41
Простой ответ заключается в том, что по правилам языка код вообще не содержится в комментариях, поэтому вопрос некорректен.
Маркиз Лорн

Ответы:

741

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

Как указано в разделе 3.3 JLS, это позволяет любому инструменту на основе ASCII обрабатывать исходные файлы:

[...] Язык программирования Java определяет стандартный способ преобразования программы, написанной на Unicode, в ASCII, которая превращает программу в форму, которая может обрабатываться инструментами на основе ASCII. [...]

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

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

Есть много ошибок на эту тему, и Java Puzzlers Джошуа Блоха и Нила Гафтера включили следующий вариант:

Это легальная программа на Java? Если так, что это печатает?

\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

(Эта программа оказывается простой программой "Hello World".)

В решении головоломки, они указывают на следующее:

Если серьезно, то эта головоломка подкрепляет уроки предыдущих трех: экранирование Unicode необходимо, когда вам нужно вставить символы, которые не могут быть представлены каким-либо другим способом в вашей программе. Избегайте их во всех остальных случаях.


Источник: Java: Выполнение кода в комментариях ?!

aioobe
источник
84
Короче говоря, Java намеренно позволяет это: «ошибка» в IDE ОП?
Вирсавия
60
@Bathsheba: это больше в головах людей. Люди не пытаются понять, как работает синтаксический анализ Java, поэтому IDE иногда отображают код неправильно. В приведенном выше примере комментарий должен заканчиваться, \u000dа часть после него должна иметь подсветку кода.
Аарон Дигулла
62
Другая распространенная ошибка - вставлять пути Windows в код, например, // C:\user\...что приводит к ошибке компиляции, поскольку \userне является допустимой escape-последовательностью Unicode.
Аарон Дигулла
50
В затмении Код после \u000dподсвечивается частично. После нажатия Ctrl + Shift + F символ заменяется новой строкой, а остальная строка
переносится
20
@TheLostMind Если я правильно понимаю ответ, вы также сможете воспроизвести его с блочными комментариями. \u002A/должен закончить комментарий.
Темыр
141

Поскольку это еще не решено, здесь поясняется, почему перевод экранирования Unicode происходит перед любой другой обработкой исходного кода:

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

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

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

Это причина еще одной странной особенности, о которой даже не упоминалось: \uuuuuuxxxx синтаксис:

Когда инструмент перевода экранирует символы и встречает последовательность, которая уже является экранированной последовательностью, он должен вставить uв последовательность дополнительный символ , преобразовав \ucafeв \uucafe. Значение не меняется, но при преобразовании в другом направлении инструмент должен просто удалить один uи заменить только последовательности, содержащие один u, их символами Юникода. Таким образом, даже экранированные символы Юникода сохраняются в своем первоначальном виде при конвертации назад и вперед. Я думаю, никто никогда не использовал эту функцию ...

Holger
источник
1
Интересно, native2asciiчто, кажется, не использует \uu...xxxxсинтаксис,
ninjalj
5
Да, native2asciiбыло предназначено, чтобы помочь подготовить пакеты ресурсов, преобразовав их в iso-latin-1, так как Properties.loadбыло исправлено только чтение latin-1. И там, правила разные, нет \uuu…синтаксиса и нет ранней стадии обработки. В файлах свойств, property=multi\u000alineдействительно так же, как property=multi\nline. (Вопреки фразе «использование выходов Unicode, как определено в разделе 3.3 спецификации языка Java ™» документации)
Хольгер
10
Обратите внимание, что эта цель дизайна могла быть достигнута без каких-либо бородавок; самый простой способ - запретить \uэкранирование для генерации символов в диапазоне U + 0000–007F. (Все такие символы могут быть изначально представлены всеми национальными кодировками, которые были актуальны в 1990-х годах - ну, может быть, за исключением некоторых управляющих символов, но вам все равно они не нужны для написания Java.)
zwol
3
@zwol: хорошо, если вы исключите управляющие символы, которые в любом случае не разрешены в исходном коде Java, вы правы. Тем не менее, это будет означать усложнение правил. И сегодня уже слишком поздно обсуждать это решение ...
Хольгер
ах проблема сохранения документа в utf8 а не в латинице или что-то еще. Все мои базы данных также были
взломаны
106

Я собираюсь совершенно безрезультатно добавить это, просто потому, что я не могу с собой поделать, и я еще не видел, чтобы это было сделано, что вопрос недействителен, поскольку содержит скрытую предпосылку, которая неверна, а именно, что комментарий!

В Java исходный код \ u000d во всех отношениях эквивалентен символу ASCII CR. Это конец строки, простой и понятный, где бы он ни происходил. Форматирование в вопросе вводит в заблуждение, то, что фактически соответствует этой последовательности символов:

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

ИМХО, поэтому самый правильный ответ: код выполняется, потому что его нет в комментарии; это на следующей строке. «Выполнение кода в комментариях» не разрешено в Java, как и следовало ожидать.

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

Пепейн Шмитц
источник
6
Я согласен, это не Java-ошибка проектирования, но это ошибка IDE.
bvdb
3
Вопрос скорее в том, почему код, который выглядит как комментарий для человека, не знакомого с этим конкретным аспектом языка и, возможно, без ссылки на подсветку синтаксиса, на самом деле не является комментарием. Возражение на основании предпосылки вопроса является недействительным.
Фил
@Phil: он выглядит только как комментарий при просмотре с помощью определенных инструментов, другие показывают это иначе.
Jmoreno
1
@ jmoreno для чтения кода не нужно иметь ничего, кроме текстового редактора. По крайней мере, это нарушает принцип наименьшего удивления, а именно, что комментарии в стиле // продолжаются до следующего символа \ n, а не до какой-либо другой последовательности, которая в конечном итоге заменяется на \ n. Никогда не ожидается, что комментарии будут чем-то иным, кроме раздетых. Плохой препроцессор.
Фил
69

\u000dПобег заканчивается комментарий , потому что \uпобеги равномерно преобразуются в соответствующие символы Unicode , прежде чем программа лексемы. Вы могли бы также использовать \u0057\u0057вместо того, //чтобы начать комментарий.

Это ошибка в вашей IDE, которая должна синтаксически выделять строку, чтобы было ясно, что\u000d комментарий заканчивается.

Это также ошибка дизайна в языке. Это не может быть исправлено сейчас, потому что это сломало бы программы, которые зависят от него. \uescape-коды должны быть либо преобразованы компилятором в соответствующий символ Unicode только в тех случаях, когда это «имеет смысл» (строковые литералы и идентификаторы, и, вероятно, нигде больше), либо им должно быть запрещено генерировать символы в диапазоне U + 0000–007F. , или оба. Любая из этих семантик помешала бы завершению комментария \u000dэкранированием, не вмешиваясь в случаи, когда \uэкранирования полезны - обратите внимание, что это включает использование \uэкранирования внутри комментариев в качестве способа кодирования комментариев в нелатинском скрипте, потому что текстовый редактор может иметь более широкое представление о том, где\uпобеги важнее, чем компилятор. (Я не знаю ни о каком редакторе или IDE, которые будут отображать \uэкранирование как соответствующие символы в любом контексте.)

Существует аналогичная ошибка проектирования в семействе C, 1 когда обратная косая черта обрабатывается до определения границ комментариев, например,

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

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

1 Для педантов: я знаю, что этот аспект C был на 100% преднамеренным, с обоснованием - я не придумываю это - что это позволило бы вам механически втиснуть код произвольно длинными линиями в перфокарты. Это было все еще неправильное дизайнерское решение.

zwol
источник
17
Я бы не сказал, что это ошибка проектирования . Я мог бы согласиться с вами, что это был неудачный выбор дизайна или выбор с неудачными последствиями, но я все еще думаю, что он работает так, как задумывали дизайнеры языка: он позволяет вам использовать любой символ юникода в любом месте файла, сохраняя при этом кодировку ASCII файла.
aioobe
12
\uС учетом сказанного, я думаю, что выбор этапа обработки для был менее абсурдным, чем решение последовать примеру C в использовании начальных нулей для восьмеричной записи. Хотя восьмеричные нотации иногда полезны, я еще не слышал, чтобы кто-либо формулировал аргумент, почему ведущий ноль является хорошим способом его обозначения.
Суперкат
3
@supercat Люди, которые добавили эту функцию в C89, обобщали поведение исходного препроцессора K & R, а не разрабатывали функцию с нуля. Я сомневаюсь, что они были знакомы с лучшими практиками перфокарт, и я также сомневаюсь, что эта функция когда-либо использовалась для своей заявленной цели, за исключением, может быть, одного или двух заданий на ретро-вычисления.
zwol
8
@supercat У меня не было бы проблемы с Java \uкак преобразованием перед токенизацией, если бы было запрещено создавать символы в диапазоне U + 0000..U + 007F. Именно сочетание «это работает везде» и «это псевдоним ASCII-символов с синтаксической значимостью» превращает его из неловкого в прямое и неправильное.
zwol
4
На ваш «для педантов»: Конечно , в то время однострочный комментарий не существует . А поскольку в C есть терминатор оператора, который не является новой строкой, он в основном будет использоваться для длинных строк, за исключением того, что, насколько я могу судить, «конкатенация строковых литералов» была в K & R. //
Марк Херд
22

Это был намеренный выбор дизайна, который восходит к первоначальному дизайну Java.

Тем людям, которые спрашивают «кто хочет, чтобы Unicode избегал комментариев в комментариях?», Я предполагаю, что это люди, чей родной язык использует латинский набор символов. Другими словами, в первоначальном дизайне Java заложено, что люди могут использовать произвольные символы Unicode везде, где это разрешено в программе Java, чаще всего в комментариях и строках.

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

Джонатан Гиббонс
источник
8
В настоящее время мы используем UTF-8 для нашего исходного кода и можем использовать символы Unicode напрямую, без необходимости экранирования.
Паŭло Эберманн
21

Я согласен с @zwol, что это ошибка дизайна; но я еще более критично отношусь к этому.

\uescape полезен в строковых и символьных литералах; и это единственное место, где оно должно существовать. С ним нужно обращаться так же, как и с другими побегами \n; и "\u000A" должно означать точно "\n".

В \uxxxxкомментариях нет абсолютно никакого смысла - никто не может это прочитать.

Точно так же нет смысла использовать \uxxxx в другой части программы. Единственное исключение, вероятно, в общедоступных API, которые принудительно содержат некоторые не-ascii-символы - что мы видели в последний раз?

У дизайнеров были свои причины в 1995 году, но спустя 20 лет это, кажется, неправильный выбор.

(вопрос к читателям - почему этот вопрос продолжает получать новые голоса? Этот вопрос связан где-то популярно?)

Zhongyu
источник
5
Я полагаю, вы не зависаете, где в API используются не-ASCII символы. Есть люди, использующие его (не я), например, в азиатских странах. И когда вы используете не идентификаторы ASCII в идентификаторах, запрещать их в комментариях к документации не имеет большого смысла. Тем не менее, позволить им внутри токена и позволить им изменить значение или границу токена - это разные вещи.
Хольгер
15
они могут использовать правильную кодировку файлов. зачем писать, int \u5431когда можно сделатьint 整
ZhongYu
3
Что вы будете делать, когда вам нужно будет скомпилировать код на основе их API и не сможете использовать правильную кодировку (предположим, что UTF-8в 1995 году не было широкой поддержки). Вам просто нужно вызвать один метод, и вы не хотите устанавливать пакет поддержки азиатского языка вашей операционной системы (вспомните, девяностые годы) для этого единственного метода…
Хольгер,
5
Что гораздо яснее, чем в 1995 году, так это то, что вы лучше знаете английский, если хотите программировать. Программирование - это международное взаимодействие, и почти все ресурсы на английском языке.
ZhongYu
8
Я не думаю, что это изменилось. Документация на Java в большинстве случаев была полностью английской. Некоторое время существовал японский перевод, но поддержка двух языков на самом деле не поддерживает идею сохранения его для всех языков мира (скорее, это опровергло). А до этого не было основного языка с поддержкой Unicode в идентификаторах. Таким образом, я думаю, кто-то думал, что локализованный исходный код был следующей большой вещью. Я бы сказал, к счастью , это не взлетело.
Хольгер
11

Единственные люди, которые могут ответить, почему экранирование Unicode было реализовано так, как они были, - это люди, которые написали спецификацию.

Возможная причина этого заключается в том, что было желание разрешить весь BMP как возможные символы исходного кода Java. Это представляет проблему, хотя:

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

Это невероятно сложно, когда Юникоду удается избежать столкновения: он создает целый набор новых правил лексера.

Самый простой выход состоит в том, чтобы выполнить лексирование в два этапа: сначала найдите и замените все экранированные символы Юникода символом, который он представляет, а затем проанализируйте полученный документ, как если бы экранированные символы Юникода не существовали.

Плюсом этого является то, что его легко указать, поэтому он упрощает спецификацию и легко реализуется.

Недостатком является, ну, ваш пример.

Мартейн
источник
2
Или ограничьте использование \ uxxxx идентификаторами, строковыми литералами и символьными константами. Именно это и делает С11.
ниндзя
это действительно усложняет правила синтаксического анализа, потому что именно они определяют эти вещи, и я полагаю, что это одна из причин того, что это так.
Мартейн