Skip to content

O Mundo Além do ASCII: Unicode e UTF-8

Como representar todos os idiomas do mundo?

O ASCII foi publicado em 1963 e, por quase 30 anos, foi o suficiente. O mundo da computação era menor, mais fechado, e as pessoas que usavam computadores naquela época, usavam principalmente em inglês. O ASCII funcionava, e funcionava muito bem. Até hoje ele ainda está presente em todo lugar: qualquer código que você escrever em Rust vai usar caracteres ASCII, qualquer endereço de site que você digitar no navegador vai usar caracteres ASCII. Ele não desapareceu e nem vai desaparecer tão cedo.

Veja:

urust.org tem apenas caracteres presentes no ASCII. https:// também.

Mas como você viu na subseção As limitações do ASCII da seção anterior, conforme os computadores foram se espalhando pelo mundo e as redes de comunicação foram conectando países e culturas diferentes, os 128 símbolos do ASCII foram revelando um limite que uma hora ou outra teria que ser resolvido.

Vale lembrar também de outro detalhe que você viu na seção anterior: uma das razões pelas quais o ASCII se limitou a 7 bits foi o custo. Na década de 1960, cada bit a mais no hardware era caro demais para ser desperdiçado, e o comitê da ASA precisou ser inteligente na escolha de quantos símbolos incluir. 128 era suficiente para a necessidade, e 128 era o que cabia no orçamento da época.

3 décadas é muito tempo no mundo da tecnologia. Entre 1961 e o final dos anos 1980, os computadores deixaram de ser máquinas enormes que ocupavam salas inteiras e custavam fortunas - acessíveis apenas a governos e grandes corporações - para se tornarem equipamentos que cabiam numa mesa que pessoas comuns podiam comprar. A capacidade de armazenar informações e representar bits baratearam de tal forma, que ninguém mais precisava perder sono contando quantos bits estava usando por caractere. O obstaculo tecnológico havia desaparecido. O que faltava agora não era mais baratear o computador. Era exatamente o que havia faltado antes do ASCII: um acordo.

Em 1987, três engenheiros (Joe Becker e Lee Collins, que trabalhavam na Xerox, e Mark Davis, que trabalhava na Apple), começaram a trabalhar numa ideia que, na época, soava quase impossível: uma tabela única que incluísse todos os sistemas de escrita de toda a humanidade. Não só os alfabetos ocidentais, mas também o árabe, o japonês, o chinês, o coreano, o devanágari, os hieróglifos egípcios, a escrita cuneiforme dos sumérios, e centenas de outros, incluindo sistemas de escrita de civilizações que nem sequer existem mais, mas onde seria interessante ter seus símbolos disponiveis para uso, para que seja possível representá-los no computador, caso alguém queira. Um projeto que, se funcionasse, resolveria de vez o problema que o ASCII não conseguia resolver.

Três anos depois, em 1991, a primeira versão do projeto foi publicada. O nome do projeto era Unicode.

Unicode

O Unicode não tem nada de mágico ou extraordinariamente diferente do ASCII. Funciona do mesmo jeito, cada caractere recebendo um número único. O que muda, é a escala da pergunta que o projeto tentava responder.

Enquanto em 1961 o comitê da ASA perguntava "quantos símbolos o inglês escrito precisa", e a resposta foi 95. Os três engenheiros em 1987 perguntaram: quantos símbolos a humanidade inteira precisa? E a resposta para essa pergunta é muito maior do que parece. Você tem alguma estimativa?

  • A maior parte do mundo usa o alfbeto latino, com 26 letras.
  • O russo usa o cirílico, com 33.
  • O árabe tem 28 letras base, e se escreve da direita para a esquerda.
  • O japonês mistura três sistemas de escrita num mesmo texto, sendo que só um deles já tem milhares de caracteres.
  • O chinês tem dezenas de milhares.

E isso sem contar hieróglifos, escrita cuneiforme, alfabetos de civilizações extintas cujos textos pesquisadores ainda precisam representar digitalmente...

Para cada símbolo, de cada sistema, em cada idioma, o Unicode precisava atribuir uma entrada númerica única. A tabela que produziram, tinha mais de 7.000 símbolos. 7 mil símbolos atribuídos, espalhados por mais de 160 sistemas de escrita diferentes. Impressionante, não? Uma evolução gigante para o que era o ASCII.

Mas não para por aí. É importante saber que o fato de o Unicode representar 7 mil símbolos, não significa conseguir representar apenas 7 mil símbolos. Pensa comigo: o ASCII representava 128 símbolos porque era sua capacidade máxima. Usavam 7 bits, logo, só tinham 128 espaços disponíveis, e os usaram todos. Se tivessem usado 8 bits, mas não atribuissem os números restantes a nenhum símbolo, teriam capacidade para 256 símbolos, mas estariam representando apenas 128 ainda.

Entendendo isso, se você achou 7 mil um número impressionante, apresento um ainda mais: ao contrário do ASCII, que usou todas as suas combinações possíveis sem sobrar nenhuma, o Unicode foi projetado com espaço para crescer ao longo dos anos. Ele foi projetado com uma capacidade total de aproximadamente 1.100.000 símbolos.

E tenho mais uma surpresa: esse número de 7 mil símbolos na tabela que eu citei acima, foi quando o Unicode foi publicado. Lá em 1991. Ao longo dos anos, até hoje, vem crescendo cada vez mais, e hoje tem mais de 150 mil símbolos.

Isso sim é impressionante.

E os computadores que já existiam?

Um projeto dessa escala realmente é de se admirar, mas se você reparar bem, tinha uma coisa que eles teriam que pensar antes de lançar: em 1991, já havia passado 3 décadas desde o ASCII estava estabelecido. Isso significa que o mundo estava cheio de máquinas usando ASCII. Há 3 décadas, os computadores vinham sendo criados principalmente com base no ASCII. Eles já vinham, há 30 anos, escrevendo e distribuindo seus programas que imprimiam letras ou símbolos na tela, com base inteiramente no ASCII. Décadas de arquivos de texto, programas, bancos de dados - tudo considerando os 128 símbolos do ASCII na posição do ASCII. Se o Unicode simplesmente redistribuisse os números de forma diferente, tudo isso viraria lixo do dia para a noite e dificultaria a adoção do Unicode.

Então os criadores do Unicode tomaram uma decisão que parece óbvia só depois que você ouve: os 128 primeiros code points do Unicode são exatamente os mesmos 128 do ASCII, nos mesmos números. O A é 65 no ASCII e continua sendo 65 no Unicode. O Enter continua sendo um caractere de controle com número 10. Qualquer arquivo de texto escrito desde 1963 continuava válido sem nenhuma alteração. O passado não precisava ser reescrito para que o futuro funcionase.

A partir do 128, o Unicode simplesmente... continuou. O ã ficou com o code point 227. O kanji com 26085. O emoji 🌍 com 127757. A mesma lógica do ASCII, estendida até onde fosse necessário.

Mas aqui surge um problema que para você talvez não seja tão óbvio. Um problema que o ASCII nunca precisou enfrentar, e para entendê-lo, você precisa de uma informação que eu omiti até agora de propósito.

Durante esse tempo todo, eu vim dizendo que o ASCII usa 7 bits. E isso é verdade, tanto que a tabela tem 128 símbolos justamente porque 2⁷ = 128. Mas preciso te contar: não é exatamente assim que o computador lida com isso na prática. No computador, cada caractere ocupa 8 bits. Para entender isso, você precisa entender como o computador guarda os bits. Ele não guarda em bits, guarda em bytes.

Aprofundamento

Você provavelmente está pensando agora: se o computador guarda o ASCII em 8 bits, o que acontece com o oitavo bit? Por que o ASCII não usa os 8 bits então?

A resposta para o não usar um bit a mais é a mesma: pelo custo. Na verdade, não é que seria caro construir computadores com 8 bits. Se o ASCII tivesse 7 bits, os computadores já teriam que pensar em 8 bits mesmo. Então se tivesse que ter um bit a mais, os computadores teriam que pensar em 9 bits.

E para entender isso, e também o que acontece com o oitavo bit, a resposta está no contexto em que o ASCII foi criado. Lembra que ele não foi feito apenas para computadores, mas também para máquinas de telecomunicação e teletipos? Esses equipamentos precisavam do 8º bit para outra finalidade: verificação de erros. Nos computadores normais o oitavo bit apenas ficava zerado.

O computador não pensa em bits, pensa em bytes

Lembra como um computador armazena qualquer coisa? No fundo, tudo são bilhões de bits espalhados pelos seus componentes, e o computador precisa conseguir encontrar qualquer um deles quando precisar. Mas como você faz para localizar um bit específico num mar de bilhões de bits quando precisar saber o seu valor?

Não é uma pergunta trivial. Imagina que você tem um caderno com bilhões de páginas, e em alguma dessas páginas está uma informação que você precisa. Sem nenhuma organização, a única forma de encontrá-la seria folhear página por página, o que levaria uma eternidade. A solução óbvia é numerar as páginas. Com páginas numeradas, se alguém te diz "está na página 3.000.000", você vai diretamente até lá.

CONCEITO IMPORTANTE

O computador resolve exatamente da mesma forma: cada posição de armazenamento recebe um número único, que na computação chamamos endereço.

Mas agora vem a pergunta que realmente importa: quanto de informação fica em cada endereço?

Primeira restrição

Pensa assim: se cada endereço apontasse para apenas 1 bit, você teria o máximo de precisão possível. Seria fantástico e a solução ideal. Correto? Correto! Cada bit seria individualmente localizável. Mas pense no tamanho do problema: um simples arquivo de 1 megabyte contém 8 milhões de bits. Para endereçar esses 8 milhões de bits individualmente, você precisaria de outros 8 milhões de endereços diferentes. E um HD de 1 terabyte? São 8 trilhões de endereços. Cada um desses endereços é um número que o computador precisa conhecer, gerenciar e processar. O circuito responsável por isso ficaria absurdamente grande, complexo e caro.

O outro extremo também não funciona. Se cada endereço apontasse para uma quantidade enorme de bits - digamos, 1000 - você realmente teria pouquíssimos endereços para gerenciar e seria mais fácil projetar um computador onde cada conjunto de 1000 bits tem um endereço específico. Entretanto, cada vez que precisasse de qualquer informação, seria obrigado a carregar 1000 bits ocupando espaço de uma vez, mesmo que precisasse apenas de alguns deles.

O ideal era um tamanho que equilibrasse os dois lados. Pequeno o suficiente para não desperdiçar, grande o suficiente para não multiplicar muitos endereços desnecessariamente. Esse tamanho precisava existir em algum lugar entre "1 bit" e "1000 bits". Mas qual?

Primeira restrição

Ok, a primeira restrição já temos: o equilíbrio.

Segunda restrição

A próxima restrição vem da própria natureza dos circuitos digitais. Você já sabe que cada bit tem dois estados: 0 ou 1. E você já sabe que quando combina bits, as combinações se multiplicam: 2 bits dão 4 combinações, 3 bits dão 8, 4 bits dão 16, 5 dão 32... Cada bit que você adiciona dobra o total de combinações.

Os circuitos físicos são construídos respeitando exatamente essa estrutura. Cada camada do circuito dobra a capacidade da camada anterior, seguindo o mesmo padrão de multiplicação por 2. Isso significa que o circuito funciona eficiente e de forma limpa quando o número de bits que ele gerencia é uma potência de 2. Se você tentasse construir um circuito para uma quantidade de bits que não fosse uma potência de 2 - como 6 ou 10, por exemplo - você quebra esse padrão que já é natural de duplicação, e o hardware precisaria de camadas extras e irregulares para compensar. Funciona, mas fica mais complexo, mais caro, e sem nenhum benefício.

Segunda restrição

A segunda restrição também temos: o número de bits que vamos carregar em cada endereço tem que ser uma potência de 2 (2, 4, 8, 16, 32...).


Nos primeiros computadores comerciais, 8 bits se mostraram suficiente para representar um caractere, um número de 0 a 255, ou uma instrução básica (estou falando do ASCII), o que cobria a maioria das necessidades da época. Computadores de 8 bits eram mais baratos de fabricar do que de 16 bits, e custo dos bits era um fator absolutamente considerável na época,. Consequentemente, os primeiros computadores pessoais de sucesso, como o Apple II e o Commodore 64, foram construídos para trabalhar com 8 bits por vez. Isso criou um efeito cascata: todos os programas, sistemas operacionais, linguagens de programação e periféricos (componentes do computador) foram escritos e fabricados pensando em 8 bits, tornando cada vez mais difícil mudar esse número. Uma vez que o mundo inteiro estava construindo em torno dele, o byte de 8 bits se tornou o padrão.

Então, chegamos a um número de bits ideal para cada endereço: 8 bits.

Apple II, um dos primeiros computadores pessoais de sucesso, lançado em 1977
Apple II (1977) Rama, CC BY-SA 2.0 FR, via Wikimedia Commons
Commodore 64, o computador pessoal mais vendido da história, lançado em 1982
Commodore 64 (1982) Evan-Amos, domínio público, via Wikimedia Commons

Como caracteres Unicode são representados

Mas você se lembra do como chegamos até aqui e do porquê ter aprendido que o computador guardava informação em bytes? Se não, não tem problema. Eu te lembro:

A partir do 128, o Unicode simplesmente... continuou. O ã ficou com o code point 227. O kanji com 26085. O emoji 🌍 com 127757. A mesma lógica do ASCII, estendida até onde fosse necessário.

Mas aqui surge um problema que para você talvez não seja tão óbvio. Um problema que o ASCII nunca precisou enfrentar, e para entendê-lo, você precisa de uma informação que eu omiti até agora de propósito.

A informação que eu tinha omitido era justamente de cada símbolo do ASCII usar 8 bits (1 byte) para ser armazenado e não 7, como você provavelmente pensava. Ambos ASCII e Unicode representam seus símbolos em bytes.

Mas bem, e então, qual é o problema que o Unicode precisaria resolver que o ASCII não precisou?

Foi justamente em COMO guardar os seus símbolos em bytes. 1 byte só conseguiria representar 256 símbolos. O ã é 227, que cabe tranquilamente num byte. Mas o kanji japonês é o codepoint 26085 - esse número não cabe num único byte (que só vai até 255). E o emoji 🌍 que é o code point 127757? Esse nem perto.

O Unicode resolve o problema do o quê: ele define qual número corresponde a qual caractere. Mas surge um problema novo: o como.

Então, como guardar esses números em bytes? Existem algumas formas diferentes de fazer isso, e cada uma tem um nome. Essas formas se chamam codificações, e a mais usada no mundo (responsável por 98% de todo conteúdo da internet) é o UTF-8.

UTF-8: a solução elegante

A solução mais simples seria usar sempre 4 bytes por caractere, já que 4 bytes é a quantidade suficiente para representar qualquer code point do Unicode. Funcionaria. Mas teria um custo enorme - aquele mesmo do motivo por não usarmos logo 1000 bits por endereço: estaria desperdiçando espaço. Um caractere simples que antes ocupava 1 byte por letra, passaria a ocupar 4 bytes por letra. Arquivos ficariam 4 vezes maiores. A internet inteira ficaria 4 vezes mais lenta.

O UTF-8 faz algo mais inteligente: usa um número variável de bytes dependendo do tamanho do code point.

  • Caracteres com code point de 0 a 127 (os mesmos do ASCII) - ocupam 1 byte.
  • Caracteres com code point de 128 a 2047, que inclui a maioria dos acentos dos acentos do português, espanhol, francês, alemão, e outros alfabetos europeus - ocupam 2 bytes.
  • Caracteres com code point de 2048 a 65535, que inclui praticamente todos os caracteres do japonês, coreano e chinês - ocupam 3 bytes.
  • Os demais (emojis, hieróglifos, e caracteres mais raros) - ocupam 4 bytes.

Uma forma mais fácil de visualizar

  • 1 Byte: Caracteres ASCII padrão (valores 0-127).
  • 2 Bytes: Letras com diacríticos, caracteres latinos adicionais, árabe, hebraico, cirílico.
  • 3 Bytes: Caracteres asiáticos (coreano, japonês, chinês...) e outros caracteres do Plano Multilingual Básico.
  • 4 Bytes: Emojis, símbolos matemáticos raros e caracteres históricos.

O resultado é que um texto em inglês puro no UTF-8 ocupa exatamente o mesmo espaço que ocupava no ASCII. Um texto em português ocupa um pouco mais. Um texto em japonês ocupa ainda mais. Cada idioma usa apenas o que precisa, nada mais.

A sacada da compatibilidade

A consequência mais importante do UTF-8 usar 1 byte para os primeiros 128 code points é esta: qualquer arquivo de texto criado em ASCII é automaticamente um arquivo UTF-8 válido. Os bytes são idênticos. Um programa que lê UTF-8 consegue ler um arquivo ASCII sem nenhuma conversão, e vice-versa para textos que usam apenas esses caracteres.

Não é exagero dizer que essa decisão de design foi o principal motivo pelo qual o UTF-8 venceu. Num mundo onde milhões (ou bilhões) de arquivos já existiam em ASCII, uma codificação que quebrasse a compatibilidade teria encontrado resistência enorme. O UTF-8 não pediu para ninguém migrar nada, ele simplesmente funcionou com o que já existia.

Unicode e UTF-8 são a mesma coisa?

Não! E essa confusão é extremamente comum - até entre programadores experientes.

Unicode é o acordo: a tabela que define qual número corresponde a qual caractere. É uma especificação abstrata. O Unicode em si não diz nada sobre bytes.

UTF-8 é uma das formas de transformar esses números em bytes para guardar num arquivo ou transmitir pela internet. Existem outras codificações que também usam a tabela do Unicode, como o UTF-16, que usa 2 ou 4 bytes e UTF-32 que usa sempre 4 para tudo, mas o UTF-8 é de longe a mais comum.

Por que isso importa para você como programador?

Quando você começar a programar em Rust, uma das coisas mais comuns que seu programa vai manipular é texto (palavras, frases ou qualquer sequência de caracteres).

Em programação, esse tipo de dado é chamado de string. Uma string é simplesmente um pedaço de texto armazenado dentro de um programa, como "Olá", "Rust" ou "123".

Para que um computador consiga guardar e entender texto, ele precisa usar um sistema de codificação, e Rust tem uma posição muito clara sobre codificação: todas as strings em Rust são UTF-8, sempre. Não existe outra opção.

Isso significa que você nunca vai precisar se preocupar em "lembrar de usar UTF-8", ele simplesmente é o padrão. Mas significa também que quando você manipular texto em Rust, vai encontrar comportamentos que só fazem sentido se você entender o que acabou de acontecer aqui.

Por exemplo: em Rust, você não consegue acessar o terceiro caractere de uma string da mesma forma que acessa uma lista de números, e o motivo é justamente pelo você aprendeu: caracteres diferentes ocupam números diferentes de bytes. O "terceiro caractere" pode começar no terceiro byte, ou no quinto, ou no sétimo. Depende de quais caracteres vieram antes. Rust te obriga a pensar nisso explicitamente, em vez de esconder o problema debaixo do tapete.

Você entenderá isso na prática quando chegar lá. Por enquanto, o importante é que você já tem a base conceitual para que isso faça sentido quando acontecer.

Antes de avançar

Você consegue explicar a diferença entre ASCII e Unicode? Entre Unicode e UTF-8? E consegue dizer por que o UTF-8 foi a codificação que o mundo adotou, em vez de simplesmente usar 4 bytes para todo e qualquer caractere?

Se sim, você está pronto para a próxima seção.