Por que esse código, escrito ao contrário, imprime "Hello World!"

261

Aqui está um código que eu encontrei na Internet:

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

Esse código é impresso Hello World!na tela; você pode vê-lo aqui . Eu posso ver claramente public static void mainescrito, mas é ao contrário. Como esse código funciona? Como isso compila?

Edit: Eu tentei esse código no IntellIJ, e funciona bem. No entanto, por algum motivo, ele não funciona no bloco de notas ++, junto com o cmd. Ainda não encontrei uma solução para isso, então, se alguém encontrar, comente abaixo.

Abóbora imaginária
fonte
38
Este é engraçado ... Algo a ver com suporte a RTL?
Eugene Sh.
12
Há o caractere Unicode # 8237; logo após a Me também depois []a: fileformat.info/info/unicode/char/202d/index.htm É chamado OVERRIDE da esquerda para a direita
Riiverside
45
xkcd obrigatório: xkcd.com/1137
Pac0
4
Você pode ver facilmente o que está acontecendo aqui simplesmente fazendo seleções no snippet de código usando o mouse.
Andreas Rejbrand
14
niam diov citats cilbupSoa como um provérbio latino ..
Mick mnemônicos

Respostas:

250

Existem caracteres invisíveis aqui que alteram a forma como o código é exibido. No Intellij, eles podem ser encontrados copiando e colando o código em uma string vazia ( ""), que os substitui por escapes Unicode, removendo seus efeitos e revelando a ordem que o compilador vê.

Aqui está a saída dessa copiar e colar:

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

Os caracteres do código-fonte são armazenados nesta ordem e o compilador os trata como estando nessa ordem, mas são exibidos de maneira diferente.

Observe o \u202Ecaractere, que é uma substituição da direita para a esquerda, iniciando um bloco em que todos os caracteres são forçados a serem exibidos da direita para a esquerda e o \u202D, que é uma substituição da esquerda para a direita, iniciando um bloco aninhado onde todos os caracteres são forçados na ordem da esquerda para a direita, substituindo a primeira substituição.

Logo, quando ele exibe o código original, class Mé exibido normalmente, mas \u202Einverte a ordem de exibição de tudo, de lá para o \u202D, o que reverte tudo novamente. (Formalmente, tudo, desde o terminal \u202Daté a linha, é revertido duas vezes, uma vez devido ao \u202De uma vez com o restante do texto revertido devido ao \u202E, motivo pelo qual esse texto aparece no meio da linha e não no final.) A direcionalidade da próxima linha é tratada independentemente da primeira devido ao terminador da linha, {'H','e','l','l','o',' ','W','o','r','l','d','!'});}}sendo exibida normalmente.

Para o algoritmo bidirecional Unicode completo (extremamente complexo, com dezenas de páginas), consulte o Anexo # 9 do Padrão Unicode .

Davis Broda
fonte
Você não explica o que o compilador (em oposição à rotina de exibição) faz com esses caracteres Unicode. Eu posso ignorá-los completamente (ou tratá-los como espaço em branco) ou interpretá-los como realmente contribuindo para o código-fonte. Não conheço as regras Java aqui, mas o fato de elas serem colocadas no final de identificadores não utilizados sugere para mim que ela pode ser a última, e os caracteres Unicode são, de fato, parte desses nomes de identificadores.
Marc van Leeuwen
Isso funcionaria da mesma maneira em c #, sem interesse?
IanF1
14
@ IanF1 Funcionaria em qualquer idioma em que o compilador / intérprete contasse caracteres RTL e LTR como espaço em branco. Mas nunca faça isso no código de produção se você valorizar a sanidade da próxima pessoa em tocar no seu código, que poderia ser você.
Wizzwizz4
2
Ou, em outras palavras: "Sempre codifique como se a pessoa que acabasse mantendo o seu código fosse um psicopata violento que sabe onde você mora". @ IanF1. Ou talvez: "Sempre codifique como se a pessoa que acaba mantendo o seu código o nomear e envergonhar como o autor original do Stack Overflow".
Cody Gray
43

Parece diferente devido ao algoritmo bidirecional Unicode . Existem dois caracteres invisíveis de RLO e LRO que o Algoritmo Bidirecional Unicode usa para alterar a aparência visual dos caracteres aninhados entre esses dois metacaracteres.

O resultado é que visualmente eles olham em ordem inversa, mas os caracteres reais na memória não são revertidos. Você pode analisar os resultados aqui . O compilador Java ignorará o RLO e o LRO e os tratará como espaço em branco, e é por isso que o código é compilado.

Nota 1: esse algoritmo é usado pelos editores de texto e navegadores para exibir visualmente os caracteres LTR (inglês) e RTL (por exemplo, árabe, hebraico) juntos ao mesmo tempo - portanto, bidirecional. Você pode ler mais sobre o algoritmo bidirecional no site da Unicode .
Nota 2: O comportamento exato de LRO e RLO é definido na Seção 2.2 do algoritmo.

James Lawson
fonte
Qual é o propósito de tal capacidade?
Eugene Sh.
6
Às vezes, esses caracteres são necessários para renderizar visualmente o árabe e o hebraico corretamente. Esses idiomas são lidos e escritos da direita para a esquerda (RTL), o primeiro caractere que é lido / gravado aparece no lado direito . Você pode ler mais aqui .
James Lawson
Os caracteres árabe e hebraico são intrinsecamente RTL, no entanto - eles aparecerão RTL mesmo sem uma substituição explícita e até reverterão automaticamente a ordem de alguns outros caracteres nas proximidades, acho que geralmente pontuação - substituições explícitas raramente são necessárias.
User2357112 suporta Monica
Esta página aqui descreve quando as substituições são necessárias. @ user2357112 está certo, eles raramente são necessários. De fato, quando você tem pontuação, cotações e números - esses caracteres especiais são considerados "neutros". Para um computador que não consegue ler as palavras e entender o contexto, não está claro se deve tratá-las como LTR ou RTL, mas o algoritmo bidi precisa escolher alguns pedidos. Às vezes, "erra" e você precisa usar esses caracteres de substituição para "corrigi-lo".
James Lawson
3
Além disso, U + 202E e U + 202D não são considerados espaços em branco. O Java considera apenas o espaço ASCII, a guia horizontal, o avanço de formulário e o CR / LF / CRLF como espaço em branco . Eles são realmente lexically parte dos identificadores M\u202Ee a\u202D, mas os identificadores parecem estar equiparados a Me a. (O JLS não explica muito bem isso.) #
User2357112 suporta Monica 12/17/17
28

O personagem U+202Ereflete o código da direita para a esquerda, mas é muito inteligente. Está oculto começando no M,

"class M\u202E{..."

Como eu encontrei a mágica por trás disso?

Bem, no começo, quando vi a pergunta difícil, "é uma piada, perder o tempo de outra pessoa", mas depois abri meu IDE ("IntelliJ"), criei uma classe e passei o código ... e compilou !!! Então, olhei melhor e vi que o "vazio público estático" estava para trás, então fui lá com o cursor e apaguei alguns caracteres ... E o que acontece? Os caracteres começaram a apagar para trás , então, pensei mmm .... raro ... tenho que executá-lo ... Então, continuo executando o programa, mas primeiro eu preciso salvá-lo ... e foi que eu encontrei! . Não pude salvar o arquivo porque meu IDE disse que havia uma codificação diferente para algum caractere e me indicou onde estava., Então inicio uma pesquisa no Google para caracteres especiais que podem fazer o trabalho, e é isso :)

Um pouco sobre

o algoritmo bidirecional Unicode e U+202Eenvolvido, explique brevemente :

O Padrão Unicode prescreve uma ordem de representação de memória conhecida como ordem lógica. Quando o texto é apresentado em linhas horizontais, a maioria dos scripts exibe caracteres da esquerda para a direita. No entanto, existem vários scripts (como árabe ou hebraico) em que a ordem natural do texto horizontal exibido é da direita para a esquerda. Se todo o texto tiver uma direção horizontal uniforme, a ordem do texto exibido não será ambígua.

No entanto, como esses scripts da direita para a esquerda usam dígitos escritos da esquerda para a direita, o texto é bidirecional: uma mistura de texto da direita para a esquerda e da esquerda para a direita. Além dos dígitos, as palavras incorporadas do inglês e outros scripts também são escritas da esquerda para a direita, produzindo também texto bidirecional. Sem uma especificação clara, podem surgir ambiguidades na determinação da ordem dos caracteres exibidos quando a direção horizontal do texto não é uniforme.

Este anexo descreve o algoritmo usado para determinar a direcionalidade para o texto Unicode bidirecional. O algoritmo estende o modelo implícito atualmente empregado por várias implementações existentes e adiciona caracteres de formatação explícitos para circunstâncias especiais. Na maioria dos casos, não é necessário incluir informações adicionais no texto para obter a ordem correta da exibição.

No entanto, no caso de texto bidirecional, há circunstâncias em que uma ordem bidirecional implícita não é suficiente para produzir texto compreensível. Para lidar com esses casos, um conjunto mínimo de caracteres de formatação direcional é definido para controlar a ordem dos caracteres quando renderizados. Isso permite o controle exato da ordem de exibição da troca legível e garante que o texto sem formatação usado para itens simples, como nomes de arquivos ou rótulos, possa sempre ser pedido corretamente para exibição.

Por que criar um algoritmo como este ?

o algoritmo bidi pode renderizar uma sequência de caracteres árabes ou hebraicos um após o outro da direita para a esquerda.

Damián Rafael Lattenero
fonte
4

O Capítulo 3 da especificação de linguagem fornece uma explicação, descrevendo em detalhes como a tradução lexical é feita para um programa Java. O que é mais importante para a pergunta:

Os programas são escritos em Unicode (§3.1) , mas são fornecidas traduções lexicais (§3.2) para que escapes Unicode (§3.3) possam ser usados ​​para incluir qualquer caractere Unicode usando apenas caracteres ASCII.

Portanto, um programa é escrito em caracteres Unicode, e o autor pode escapá-los usando \uxxxxno caso de a codificação do arquivo não suportar o caractere Unicode, caso em que é traduzido para o caractere apropriado. Um dos caracteres Unicode presentes neste caso é \u202E. Não é mostrado visualmente no trecho, mas se você tentar alternar a codificação do navegador, os caracteres ocultos poderão aparecer.

Portanto, a tradução lexical resulta na declaração de classe:

class M\u202E{

o que significa que o identificador de classe é M\u202E. A especificação considera isso como um identificador válido:

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

Uma "letra ou dígito Java" é um caractere para o qual o método Character.isJavaIdentifierPart(int)retorna verdadeiro.

M Anouti
fonte
Desculpe, mas isso é invertido (trocadilhos). Não há escapadas no código fonte; você está descrevendo como poderia ter sido escrito. E ele é compilado para uma classe chamada "M" (apenas um caractere).
quer
@ TomBlodget De fato, mas o ponto (que de fato destaque na citação das especificações) é que o compilador também pode processar caracteres Unicode brutos. Essa é realmente toda a explicação. A tradução de escape é apenas uma informação adicional e não está diretamente relacionada a este caso. Quanto à classe compilada, acho que é porque o caractere do switch RTL está sendo descartado pelo compilador. Vou tentar ver se isso é esperado, mas acho que acontece após a fase de tradução lexical.
M Anouti 13/10/19