¿Por qué este código, escrito al revés, imprime "Hello World!"

261

Aquí hay un código que encontré en Internet:

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

Este código se imprime Hello World!en la pantalla; Puedes verlo correr aquí . Puedo ver claramente public static void mainescrito, pero está al revés. ¿Cómo funciona este código? ¿Cómo se compila esto?

Editar: probé este código en IntellIJ, y funciona bien. Sin embargo, por alguna razón no funciona en notepad ++, junto con cmd. Todavía no he encontrado una solución para eso, así que si alguien lo hace, comente a continuación.

Calabaza imaginaria
fuente
38
Este es divertido ... ¿Tiene algo que ver con el soporte RTL?
Eugene Sh.
12
Ahí está el personaje de Unicode # 8237; justo después de My también después []a: fileformat.info/info/unicode/char/202d/index.htm Se llama OVERRIDE DE IZQUIERDA A DERECHA
Riiverside
45
obligatorio xkcd: xkcd.com/1137
Pac0
44
Puede ver fácilmente lo que está sucediendo aquí simplemente haciendo selecciones en el fragmento de código con el mouse.
Andreas Rejbrand
14
niam diov citats cilbupsuena como un proverbio latino ..
Mick Mnemonic

Respuestas:

250

Aquí hay caracteres invisibles que alteran la forma en que se muestra el código. En Intellij, estos se pueden encontrar copiando y pegando el código en una cadena vacía ( ""), que los reemplaza con escapes Unicode, eliminando sus efectos y revelando el orden que ve el compilador.

Aquí está la salida de ese copiar y pegar:

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

Los caracteres del código fuente se almacenan en este orden, y el compilador los trata como si estuvieran en este orden, pero se muestran de manera diferente.

Tenga en cuenta que el \u202Ecarácter, que es una anulación de derecha a izquierda, comienza un bloque donde todos los caracteres están obligados a mostrarse de derecha a izquierda, y el \u202D, que es una anulación de izquierda a derecha, inicia un bloque anidado donde todos los caracteres se fuerzan en orden de izquierda a derecha, anulando la primera anulación.

Ergo, cuando muestra el código original, class Mse muestra normalmente, pero \u202Einvierte el orden de visualización de todo, desde allí hasta el \u202D, lo que invierte todo nuevamente. (Formalmente, todo, desde el \u202Dterminador de línea hasta el reverso, se invierte dos veces, una vez debido al \u202Dy el resto del texto invertido debido a la \u202E, por lo que este texto aparece en el medio de la línea en lugar del final). La direccionalidad de la línea siguiente se maneja independientemente de la primera debido al terminador de línea, por lo que {'H','e','l','l','o',' ','W','o','r','l','d','!'});}}se muestra normalmente.

Para el algoritmo bidireccional Unicode completo (extremadamente complejo, decenas de páginas de largo), consulte el Anexo 9 estándar de Unicode .

Davis Broda
fuente
No explica qué hace el compilador (a diferencia de la rutina de visualización) con esos caracteres Unicode. Podría ignorarlos directamente (o tratarlos como espacios en blanco), o podría interpretarlos como una contribución real al código fuente. No conozco las reglas de Java aquí, pero el hecho de que se coloquen al final de identificadores que de otro modo no se usarían me sugiere que podría ser la última, y ​​los caracteres Unicode son de hecho parte de esos nombres de identificadores.
Marc van Leeuwen
¿Funcionaría de la misma manera en C #, por interés?
IanF1
14
@ IanF1 Funcionaría en cualquier idioma donde el compilador / intérprete cuente los caracteres RTL y LTR como espacios en blanco. Pero nunca haga esto en el código de producción si valora la cordura de la próxima persona que toque su código, que bien podría ser usted.
wizzwizz4
2
O, en otras palabras: "Codifique siempre como si la persona que termina manteniendo su código sea un psicópata violento que sabe dónde vive". @ IanF1. O tal vez: "Codifique siempre como si la persona que termina manteniendo su código lo nombrará y avergonzará como el autor original en Stack Overflow".
Cody Gray
43

Se ve diferente debido al Algoritmo bidireccional Unicode . Hay dos caracteres invisibles de RLO y LRO que el Algoritmo bidireccional Unicode usa para cambiar la apariencia visual de los caracteres anidados entre estos dos metacaracteres.

El resultado es que visualmente se ven en orden inverso, pero los caracteres reales en la memoria no se invierten. Puedes analizar los resultados aquí . El compilador de Java ignorará RLO y LRO, y los tratará como espacios en blanco, razón por la cual el código se compila.

Nota 1: los editores de texto y los navegadores utilizan este algoritmo para mostrar visualmente los caracteres, tanto los caracteres LTR (inglés) como los caracteres RTL (p. Ej., Árabe, hebreo) juntos al mismo tiempo, por lo tanto, "bi" -direccional. Puede leer más sobre el algoritmo bidireccional en el sitio web de Unicode .
Nota 2: El comportamiento exacto de LRO y RLO se define en la Sección 2.2 del Algoritmo.

James Lawson
fuente
¿Cuál es el propósito de tal capacidad?
Eugene Sh.
66
Estos caracteres son necesarios a veces para representar visualmente árabe y hebreo correctamente. Estos idiomas se leen y escriben de derecha a izquierda (RTL), el primer carácter que se lee / escribe aparece en el lado derecho . Puedes leer más aquí .
James Lawson
Sin embargo, los caracteres árabes y hebreos son intrínsecamente RTL: aparecerán RTL incluso sin una anulación explícita, e incluso revertirán automáticamente el orden de ciertos otros caracteres cercanos, creo que son principalmente signos de puntuación, por lo que rara vez son necesarias anulaciones explícitas.
user2357112 es compatible con Monica
Esta página aquí describe cuándo son necesarias las anulaciones. @ user2357112 tiene razón, rara vez se necesitan. De hecho, cuando tiene signos de puntuación, citas y números, estos caracteres especiales se consideran "neutrales". Para una computadora que no puede leer las palabras y entender el contexto, no está claro si tratarlas como LTR o RTL, pero el algoritmo bidi tiene que elegir algún orden. A veces "se equivoca" y necesita usar estos caracteres de anulación para "corregirlo".
James Lawson
3
Además, U + 202E y U + 202D no se consideran espacios en blanco. Java solo considera el espacio ASCII, la pestaña horizontal, el feed de formulario y CR / LF / CRLF como espacios en blanco . En realidad son léxicamente parte de los identificadores M\u202Ey a\u202D, pero esos identificadores parecen tratarse como equivalentes a My a. (El JLS no hace un buen trabajo al explicar esto.)
user2357112 apoya a Monica
28

El personaje U+202Erefleja el código de derecha a izquierda, aunque es muy inteligente. Está oculto a partir de la M,

"class M\u202E{..."

¿Cómo encontré la magia detrás de esto?

Bueno, al principio cuando vi la pregunta, dije: "es una especie de broma, perder el tiempo de alguien más", pero luego abrí mi IDE ("IntelliJ"), creé una clase y pasé el código ... y se compiló !!! Entonces, miré mejor y vi que el "vacío estático público" estaba al revés, así que fui allí con el cursor y borré algunos caracteres ... ¿Y qué sucede? Los caracteres comenzaron a borrarse hacia atrás , así que pensé mmm ... raro ... tengo que ejecutarlo ... Así que procedo a ejecutar el programa, pero primero tenía que guardarlo ... y fue entonces cuando ¡Lo encontré! . No pude guardar el archivo porque mi IDE decía que había una codificación diferente para algunos caracteres, y me indicó dónde estaba, Así que empiezo una investigación en Google de caracteres especiales que podrían hacer el trabajo, y eso es todo :)

Un poco sobre

el algoritmo bidireccional de Unicode, e U+202Einvolucrado, explica brevemente :

El estándar Unicode prescribe un orden de representación de memoria conocido como orden lógico. Cuando el texto se presenta en líneas horizontales, la mayoría de los scripts muestran caracteres de izquierda a derecha. Sin embargo, hay varios guiones (como el árabe o el hebreo) donde el orden natural del texto horizontal en la pantalla es de derecha a izquierda. Si todo el texto tiene una dirección horizontal uniforme, entonces el orden del texto de la pantalla no es ambiguo.

Sin embargo, debido a que estos scripts de derecha a izquierda usan dígitos que se escriben de izquierda a derecha, el texto es en realidad bidireccional: una mezcla de texto de derecha a izquierda y de izquierda a derecha. Además de los dígitos, las palabras incrustadas del inglés y otros scripts también se escriben de izquierda a derecha, produciendo también texto bidireccional. Sin una especificación clara, pueden surgir ambigüedades para determinar el orden de los caracteres mostrados cuando la dirección horizontal del texto no es uniforme.

Este anexo describe el algoritmo utilizado para determinar la direccionalidad del texto bidireccional Unicode. El algoritmo extiende el modelo implícito actualmente empleado por varias implementaciones existentes y agrega caracteres de formato explícito para circunstancias especiales. En la mayoría de los casos, no es necesario incluir información adicional con el texto para obtener un orden de visualización correcto.

Sin embargo, en el caso del texto bidireccional, hay circunstancias en las que un orden bidireccional implícito no es suficiente para producir un texto comprensible. Para tratar estos casos, se define un conjunto mínimo de caracteres de formato direccional para controlar el orden de los caracteres cuando se representan. Esto permite un control exacto del orden de visualización para el intercambio legible y garantiza que el texto sin formato utilizado para elementos simples como nombres de archivo o etiquetas siempre se pueda ordenar correctamente para su visualización.

¿Por qué crear un algoritmo como este ?

El algoritmo bidi puede representar una secuencia de caracteres árabes o hebreos uno tras otro de derecha a izquierda.

Damián Rafael Lattenero
fuente
4

El Capítulo 3 de la especificación del lenguaje proporciona una explicación al describir en detalle cómo se realiza la traducción léxica para un programa Java. Lo más importante para la pregunta:

Los programas están escritos en Unicode (§3.1) , pero se proporcionan traducciones léxicas (§3.2) para que los escapes de Unicode (§3.3) se puedan usar para incluir cualquier carácter Unicode utilizando solo caracteres ASCII.

Entonces, un programa está escrito en caracteres Unicode, y el autor puede escapar de ellos usando \uxxxxen caso de que la codificación del archivo no sea compatible con el carácter Unicode, en cuyo caso se traduce al carácter apropiado. Uno de los caracteres Unicode presentes en este caso es \u202E. No se muestra visualmente en el fragmento, pero si intenta cambiar la codificación del navegador, pueden aparecer los caracteres ocultos.

Por lo tanto, la traducción léxica da como resultado la declaración de clase:

class M\u202E{

lo que significa que el identificador de clase es M\u202E. La especificación considera esto como un identificador válido:

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

Una "letra o dígito Java" es un carácter para el cual el método Character.isJavaIdentifierPart(int)devuelve verdadero.

M Anouti
fuente
Lo siento, pero esto es al revés (juego de palabras). No hay escapes en el código fuente; Estás describiendo cómo podría haber sido escrito. Y se compila en una clase llamada "M" (solo un carácter).
Tom Blodget
@TomBlodget De hecho, pero el punto (que de hecho destaqué en la cita de especificaciones) es que el compilador también puede procesar caracteres Unicode sin procesar. Esa es realmente la explicación completa. La traducción de escape es solo una información adicional y no está directamente relacionada con este caso. En cuanto a la clase compilada, creo que es porque el compilador descarta de alguna manera el carácter de cambio RTL. Intentaré ver si esto se espera, pero creo que sucede después de la fase de traducción léxica.
M Anouti