Swift e Objective-C

[Swift básico #6] Por que strings não aceitam text[0]?

Explicamos por que Swift String não aceita índices inteiros, partindo dos clusters de grafemas estendidos. Verificamos na prática as diferenças entre Character, Unicode scalar e UTF-8 em strings coreanas compostas.

5 min de leitura
Imagem de capa de [Swift básico #6] Por que strings não aceitam text[0]?

Um dos pontos que mais confundem quem migra de outra linguagem para Swift são as strings. text[0] não funciona, text[2..<5] também não, e é preciso criar um tipo de índice separado para escrever text.index(text.startIndex, offsetBy: 2). Em Python, isso seria resolvido com um único caractere.

Isso não aconteceu porque a equipe do Swift não conseguiu criar a API. É justamente o contrário: ela expõe com honestidade que o conceito de “enésimo caractere de uma string” não é simples. Outras linguagens escondem essa complexidade e às vezes retornam a resposta errada. O Swift escolheu sempre retornar a resposta certa, mesmo sendo menos conveniente. Nesta sexta e última parte da série Swift básico, vamos investigar por que strings são realmente difíceis.

O fluxo que pode falhar ao converter uma string em número é tratado com seu modelo de erro em Como escolher entre throws, try e Result no Swift.

Por que “quantos caracteres?” é difícil — o mundo do Unicode

Começamos com esta pergunta: quantos caracteres há em “café”? Parece óbvio que são 4, mas para o computador existem duas respostas. é pode ser armazenado como um único ponto de código precomposto (U+00E9), ou como e (U+0065) mais um acento combinante (U+0301). A aparência é igual, mas a quantidade interna de pontos de código é 1 ou 2.

O coreano torna esse problema ainda mais concreto. Uma sílaba coreana pode ser um único ponto de código de sílaba precomposta (U+AC01), ou uma combinação de três letras jamo. Se você já trabalhou com nomes de arquivos do macOS, talvez tenha visto nomes coreanos salvos em NFD (Normalization Form D — forma de normalização Unicode que decompõe letras em unidades jamo) aparecerem separados em outro sistema. O mesmo “caractere” pode ter um ou três pontos de código.

Os emojis acrescentam outra camada. O emoji de família 👨‍👩‍👧‍👦 une quatro emojis de pessoas com joiners de largura zero (ZWJ, Zero Width Joiner — caracteres invisíveis que agrupam emojis como um só), totalizando sete pontos de código. Em unidades de código UTF-16, são 11. Para os olhos, porém, é um caractere.

Por isso o padrão Unicode define uma unidade própria para “um caractere percebido por uma pessoa”: o cluster de grafemas estendido (extended grapheme cluster). é, uma sílaba coreana composta e o emoji de família são, cada um, um cluster de grafemas.

Escolhas das linguagens — uma resposta errada é mais rápida ou uma resposta certa é mais lenta?

Agora podemos comparar como cada linguagem lida com o “enésimo caractere”.

O "👨‍👩‍👧‍👦".length do JavaScript é 11 porque conta unidades de código UTF-16. Java funciona da mesma forma. O len do Python 3 retorna 7 porque conta pontos de código. Todos entram em conflito com a intuição humana de “um caractere”. Cortar ingenuamente uma string com emojis pode dividir um emoji ao meio e gerar texto corrompido.

O "👨‍👩‍👧‍👦".count do Swift é 1 porque Character representa um cluster de grafemas, não um ponto de código. count é igual independentemente de como é armazenado, e == retorna true considerando a normalização. A resposta é sempre correta segundo o que as pessoas veem.

Essa precisão tem um custo. Clusters de grafemas têm tamanho variável, então encontrar o enésimo caractere exige percorrer desde o início e verificar cada limite. Por isso strings do Swift não têm índices inteiros. Uma API que fizesse text[7] parecer O(1) esconderia um custo real de O(n). Usar text[i] dentro de um loop o torna O(n²), e o Swift decidiu não incentivar isso. O tipo opaco String.Index deixa claro pelo tipo que a posição foi encontrada por contagem. O princípio da primeira parte da série de filosofia — não esconder custos — também se repete nas strings.

A mesma string é medida de forma diferente em cada linguagem
A mesma string é medida de forma diferente em cada linguagem

Caixa de ferramentas prática — trabalhando com strings sem índices

Com o princípio entendido, vamos à prática. Mesmo sem índices inteiros, ferramentas mais seguras resolvem a maioria das tarefas.

Para cortar as extremidades, use prefix e suffix. text.prefix(10)retornam com segurança os 10 primeiros caracteres (grafemas), ou apenas os disponíveis, sem crash. Não é preciso calcular índices de substring para criar textos de pré-visualização.

Para pesquisar, use range(of:) e firstIndex(of:). Quando você precisa de uma posição, geralmente é a posição de um conteúdo específico. Basta colocar o Range ou Index retornado no subscript; não é necessária conversão para inteiro.

Para separar, use split. text.split(separator: ",")substitui a iteração manual por índices.

Se você realmente precisa do enésimo caractere, use Array. Quando um algoritmo precisa de acesso aleatório por caractere, o padrão é converter para Array(text). O custo O(n) da conversão é pago uma vez; depois, todo acesso custa O(1). É muito melhor do que chamar index(offsetBy:) a cada loop.

Quando precisar de bytes, especifique a view. Às vezes você precisa de quantidade de bytes, não de caracteres, como em tráfego de rede ou limites de colunas de DB. Swift deixa a unidade explícita com text.utf8.count, text.utf16.count e text.unicodeScalars.count. Lembre que NSString.length, encontrado em UserDefaults ou APIs antigas, usa UTF-16 e pode ser diferente de count do Swift.

Há uma armadilha: índices de strings diferentes não são compatíveis. Usar em b um String.Index obtido de a pode causar crash ou resultado incorreto. Um índice pertence àquela string naquele momento. Depois de modificar a string, considere os índices anteriores inválidos.

A maioria das operações com strings funciona sem índices inteiros
A maioria das operações com strings funciona sem índices inteiros

Por que isso é especialmente importante para desenvolvedores coreanos

Ao criar serviços coreanos, esse conhecimento deixa de ser teoria e passa a ser prático.

Limites de tamanho de apelidos são um exemplo típico. Ao validar “até 10 caracteres”, se servidor (outra linguagem) e cliente (Swift) contarem em unidades diferentes, um apelido aceito pelo cliente pode ser rejeitado pelo servidor. Se o servidor limitar bytes UTF-8, o coreano usa 3 bytes por caractere; portanto, 10 caracteres são 30 bytes. Primeiro, a equipe deve concordar sobre o que contar; no Swift, escolha conscientemente count (grafemas) ou utf8.count (bytes).

A decomposição em jamo é outro problema. Por causa do NFD mencionado acima, strings coreanas vindas de sistemas externos podem parecer iguais e ter pontos de código diferentes. O == do Swift considera a normalização, então geralmente funciona, mas ao usar strings externas como chaves de dicionários baseados em hash ou passá-las para outra linguagem, é mais seguro normalizar explicitamente para NFC (Normalization Form C — forma que armazena jamo combinados em caracteres precompostos) com precomposedStringWithCanonicalMapping.

Para recursos como busca por consoante inicial em pesquisa e autocompletar, é preciso decompor grafemas em jamo; a view unicodeScalars é o ponto de partida. Entendendo views, o requisito vira “percorrer outra view”, não “precisar de uma biblioteca especial”.

Resultado confirmado na execução

No Apple Swift 6.3.3, comparamos a forma precomposta com a forma composta criada com três jamo 한. A string composta tem três Unicode scalar, mas String.count a conta como um caractere, e as duas representações foram iguais na comparação de equivalência canônica.

string=characters:1,scalars:3,canonically-equal:true

Depois de confirmar isso, passei a mostrar a unidade nos nomes das variáveis ao implementar limites de tamanho. Para characterLimit, uso count; para tamanho de transferência, utf8.count, e registro a mesma unidade no contrato do servidor. Se o acordo usar apenas um length sem unidade, coreano e emojis podem facilmente causar divergências entre cliente e servidor.

Resumo

  • O “enésimo caractere” é difícil porque um caractere Unicode (um cluster de grafemas) é formado por uma quantidade variável de pontos de código. é, coreano composto e emojis ZWJ são exemplos típicos.
  • O length de outras linguagens pode contar unidades ou pontos de código e divergir da intuição. O count do Swift conta grafemas e sempre responde segundo a perspectiva humana.
  • O preço é acesso aleatório O(n); por isso o Swift removeu índices inteiros para não esconder esse custo.
  • Na prática, prefix/suffix, range(of:) e split resolvem quase tudo; use conversão para Array no acesso aleatório e especifique a view utf8 para contar bytes.
  • Em serviços coreanos, os pontos práticos são concordar sobre a unidade de validação de caracteres e tratar a normalização NFC/NFD.

Com isso termina a série básica de seis partes. A partir de agora começa a série intermediária. O primeiro tema é o coração do gerenciamento de memória do Swift: ARC (Automatic Reference Counting) e a escolha entre weak e unowned. É a parte principal da história das referências cíclicas anunciada no artigo sobre closures.

Leitura recomendada

Fontes e verificação

  • Swift StringApple Developer Documentation · Documentação oficial · Consultado 26 de agosto de 2026Evidência: Modelo de coleção Character de String, equivalência canônica Unicode, views de strings e APIs de índice
  • The Swift Programming Language: Strings and CharactersSwift.org · Documentação oficial · Consultado 26 de agosto de 2026Evidência: Clusters de grafemas estendidos, representação coreana composta e custo de count e indexação inteira