Strings

Nós já conversamos sobre as strings no capítulo 4, mas vamos dar uma olhada mais em profundidade agora. As strings são uma área que os novos Rustáceos geralmente tem maior dificuldade. Isto é devido a uma combinação de três coisas: a propensão de Rust de certificar-se de expor possíveis erros, as strings são estruturas de dados mais complicadas que muitos programadores lhes dão crédito, e UTF-8. Essas coisas combina de tal forma que parecem difícil quando se vem de outras linguagens.

A razão pela qual as strings estão no capítulo de coleções é que as strings são implementadas como uma coleção de bytes mais alguns métodos para fornecer informações úteis e funcionalidade quando esses bytes são interpretados como texto. Nesta seção, iremos falar sobre as operações em String que todo tipo de coleção tem, como criar, atualizar e ler. Também discutiremos as formas em que String é diferente das outras coleções, a saber, como a indexação em um String é complicada pelas diferenças entre como as pessoas e os computadores interpretam dados String.

O que é String?

Antes de podermos explorar esses aspectos, precisamos falar sobre o que exatamente significa o termo string. Rust realmente só tem um tipo de string no núcleo da própria linguagem: str, a fatia de string, que geralmente é vista na forma emprestada , &str. Nós falamos sobre fatias de strings no Capítulo 4: estas são uma referência a alguns dados de string codificados em UTF-8 armazenados em outro lugar. Literais de strings, por exemplo, são armazenados na saída binária do programa e, portanto, são fatias de string.

O tipo chamado String é fornecido na biblioteca padrão do Rust, em vez de codificado no núleo da linguagem, e é um extensível, mutável, owned, tipo string codificado UTF-8. Quando Rustáceos falam sobre “strings” em Rust, geralmente significa tanto os tipos String quanto os tipos de string&str, normalmente ambos. Esta seção, é em grande parte sobre String, mas ambos esses tipos são usados em grande parte na biblioteca padrão da Rust. Tanto o String como as fatias de string são codificadas em UTF-8.

A biblioteca padrão do Rust também inclui uma série de outros tipos de string, como OsString, OsStr, CString e CStr. Bibliotecas crates podem fornecer mais opções para armazenar dados de string. Semelhante ao nome *String/*Str, elas geralmente fornecem uma variante owned e borrowed, assim como String/&str. Esses tipos de string podem armazenar diferentes codificações ou ser representados na memória de maneira diferente, por exemplo. Nós não estaremos falando sobre esse outro tipo de string neste capítulo; veja a documentação da API para obter mais informações sobre como usá-los e quando cada um é apropriado.

Criando uma Nova String

Muitas das mesmas operações disponíveis com Vec também estão disponíveis em String, começando com a função new para criar uma string, assim:


#![allow(unused)]
fn main() {
let mut s = String::new();
}

Isso cria uma nova string vazia chamada s na qual podemos carregar dados.

Muitas vezes, teremos alguns dados iniciais que gostaríamos de já colocar na string. Para isso, usamos o método to_string, que está disponível em qualquer tipo que implementa a trait Display, como as strings literais:


#![allow(unused)]
fn main() {
let data = "initial contents";

let s = data.to_string();

// o método também funciona em literais diretamente
let s = "initial contents".to_string();
}

Isso cria uma string contendo initial contents.

Também podemos usar a função String :: from para criar uma String de uma string literal. Isso equivale a usar to_string:


#![allow(unused)]
fn main() {
let s = String::from("initial contents");
}

Como as strings são usadas para tantas coisas, existem várias APIs genéricas diferentes que podem ser usadas para strings, então há muitas opções. Algumas delas podem parecer redundantes, mas todas têm seu lugar! Nesse caso, String :: from e .to_string acabam fazendo exatamente o mesmo, então a que você escolher é uma questão de estilo.

Lembre-se de que as string são codificadas em UTF-8, para que possamos incluir qualquer dados apropriadamente codificados neles:


#![allow(unused)]
fn main() {
let hello = "السلام عليكم";
let hello = "Dobrý den";
let hello = "Hello";
let hello = "שָׁלוֹם";
let hello = "नमस्ते";
let hello = "こんにちは";
let hello = "안녕하세요";
let hello = "你好";
let hello = "Olá";
let hello = "Здравствуйте";
let hello = "Hola";
}

Atualizando uma String

Uma String pode crescer em tamanho e seu conteúdo pode mudar assim como o conteúdo de um Vec, empurrando mais dados para ela. Além disso, String tem operações de concatenação implementadas com o operador + por conveniência.

Anexando a uma String com Push

Podemos criar uma String usando o método push_str para adicionar uma seqüência de caracteres:


#![allow(unused)]
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}

s conterá “foobar“ após essas duas linhas. O método push_str leva um fatia de string porque não necessariamente queremos ownership do parâmetro. Por exemplo, seria lamentável se não pudéssemos usar s2 depois de atualizar o seu conteúdo a s1:


#![allow(unused)]
fn main() {
let mut s1 = String::from("foo");
let s2 = String::from("bar");
s1.push_str(&s2);
}

O método push é definido para ter um único caractere como parâmetro e adicionar à String:


#![allow(unused)]
fn main() {
let mut s = String::from("lo");
s.push('l');
}

Após isso, s irá conter “lol”.

Concatenação com o Operador + ou a macro format!

Muitas vezes, queremos combinar duas strings existentes. Uma maneira é usar o operador + dessa forma:


#![allow(unused)]
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // Note que s1 foi movido aqui e não pode ser mais usado
}

Após este código, a String s3 conterá Hello, world!. O motivo que s1 não é mais válido após a adição e o motivo que usamos uma referência a s2 tem a ver com a assinatura do método que é chamado quando usamos o operador +. O operador + usa o método add, cuja assinatura parece algo assim:

fn add(self, s: &str) -> String {

Esta não é a assinatura exata que está na biblioteca padrão; lá o add é definido usando genéricos. Aqui, estamos olhando a assinatura do add com tipos de concreto substituídos pelos genéricos, o que acontece quando nós chamamos esse método com valores String. Vamos discutir genéricos no Capítulo 10. Esta assinatura nos dá as pistas que precisamos para entender o complicado operador +.

Antes de tudo, s2 tem um &, o que significa que estamos adicionando uma referência da segunda string para a primeira string. Isso é devido ao parâmetro s na função add: só podemos adicionar um &str à String, não podemos adicionar dois valores String juntos. Mas espere - o tipo de &s2 é &String, não &str, conforme especificado no segundo parâmetro para add. Por que nosso exemplo compila? Podemos usar &s2 na chamada para add porque um &String o argumento pode ser coerced em um &str - quando a função add é chamada, Rust usa algo chamado de deref coercion, o que você poderia pensar aqui como virando &s2 para&s2[..] para uso na função add. Vamos discutir deref coercion em maior profundidade no Capítulo 15. Como o add não se apropria o parâmetro s2 ainda será uma String válida após essa operação.

Em segundo lugar, podemos ver na assinatura que add toma posse de self, porque self não tem &. Isso significa s1 no exemplo acima será transferido para a chamada add e não será mais válido depois disso. Por enquanto let s3 = s1 + &s2; parece que irá copiar ambas as strings e criar uma nova, esta declaração realmente adere a s1, acrescenta uma cópia do conteúdo de s2, então retorna ownership do resultado. Em outras palavras, parece estar fazendo muitas cópias, mas não é: a implementação é mais eficiente do que copiar.

Se precisarmos concatenar várias strings, o comportamento de + fica complicado:


#![allow(unused)]
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
}

s será “tic-tac-toe” neste momento. Com todos os + e ", fica difícil ver o que está acontecendo. Para strings mais complicadas , podemos usar o macro format!:


#![allow(unused)]
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);
}

Este código também definirá s para “tic-tac-toe”. A macro format! funciona do mesmo modo que println!, mas em vez de imprimir a saída para a tela, ela retorna uma String com o conteúdo. Esta versão é muito mais fácil de ler, e também não incide ownership em nenhum dos seus parâmetros.

Indexação em Strings

Em muitas outras linguagens, acessar caracteres individuais em uma string por referenciando por índice é uma operação válida e comum. Em Rust, no entanto, se nós tentamos acessar partes de uma String usando sintaxe de indexação, vamos ter um erro. Ou seja, este código:

let s1 = String::from("hello");
let h = s1[0];

resultará neste erro:

error: the trait bound `std::string::String: std::ops::Index<_>` is not
satisfied [--explain E0277]
  |>
  |>     let h = s1[0];
  |>             ^^^^^
note: the type `std::string::String` cannot be indexed by `_`

O erro e a nota contam a história: as strings em Rust não suportam a indexação. Assim a próxima pergunta é, por que não? Para responder a isso, temos que conversar um pouco sobre como o Rust armazena strings na memória.

Representação Interna

Uma String é um invólucro sobre um Vec <u8>. Vejamos alguns dos nossos exemplos UTF-8, codificadas corretamente, de strings vistas anteriormente. Primeiro, este:


#![allow(unused)]
fn main() {
let len = String::from("Hola").len();
}

Neste caso, len terá valor de quatro, o que significa que o Vec armazena a string ”Hola” tem quatro bytes de comprimento: cada uma dessas letras leva um byte quando codificado em UTF-8. E o que acontece para esse exemplo?


#![allow(unused)]
fn main() {
let len = String::from("Здравствуйте").len();
}

Uma pessoa que pergunte pelo comprimento da string pode dizer que ela deva ter 12.No entanto, a resposta de Rust é 24. Este é o número de bytes que é necessário para codificar “Здравствуйте“ em UTF-8, uma vez que cada valor escalar Unicode leva dois bytes de armazenamento. Assim sendo, um índice nos bytes da string nem sempre se correlaciona com um valor escalar Unicode válido.

Para demonstrar, considere este código inválido do Rust:

let hello = "Здравствуйте";
let answer = &hello[0];

Qual deve ser o valor da answer? Seria З, a primeira letra? Quando codificado em UTF-8, o primeiro byte de З é 208, e o segundo é 151, então a answer deve, na verdade, ser 208, mas 208 não é um caractere válido em si. Retornar 208 provavelmente não é o que uma pessoa gostaria se eles pedissem a primeira letra desta string, mas esse é o único dado que Rust tem no byte de índice 0. O retorno do valor do byte provavelmente não é o que as pessoas querem, mesmo com caracteres contendo acentuação: &"hello"[0] retornaria 104, não h. Para evitar o retornando um valor inesperado e causando erros que podem não ser descobertos imediatamente, Rust escolhe não compilar este código e previne mal-entendidos anteriormente.

Bytes e Valores Escalares e Clusters de Grafemas! Nossa!

Isso leva a outro ponto sobre UTF-8: existem realmente três maneiras relevantes de olhar para as strings, da perspectiva do Rust: como bytes, valores escalares e clusters de grafemas (a coisa mais próxima do que as pessoas chamariam letras).

Se olharmos para a palavra Hindi “नमस्ते” escrita na escrita Devanagari, é em última instância, armazenada como um Vec de valores u8 que se parece com isto:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

Isso é 18 bytes, e é como os computadores de fato armazenam esses dados. Se olharmos para eles como valores escalares Unicode, que são o tipo char de Rust, aqueles bytes se parecem com isto:

['न', 'म', 'स', '्', 'त', 'े']

Existem seis valores char aqui, mas o quarto e o sexto não são letras, Eles são diacríticos que não fazem sentido por conta própria. Finalmente, se olharmos para eles como clusters de grafemas, teríamos o que uma pessoa chamaria as quatro letras que compõem esta palavra:

["न", "म", "स्", "ते"]

Rust fornece diferentes maneiras de interpretar os dados de uma string bruta que os computadores armazenem para que cada programa possa escolher a interpretação que necessite, não importa em que idioma humano os dados estão.

Uma razão final do Rust não permitir que você indexe uma String para obter um caracter é que as operações de indexação sempre esperam um tempo constante (O(1)). Não é possível garantir que o desempenho com uma String, entretanto, já que o Rust teria que percorrer todo o conteúdo desde o início até o índice para determinar quantos caracteres válidos havia.

Fatiando Strings

Porque não está claro qual seria o tipo de retorno da indexação de string, e muitas vezes é uma má idéia indexar uma string, Rust dissuade-o de fazê-lo pedindo que você seja mais específico se você realmente precisar disso. Do jeito que você pode ser mais específico que a indexação usando [] com um único número é usando [] com um intervalo para criar uma fatia de string contendo bytes específicos:


#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

Aqui, s será um &str que contém os primeiros quatro bytes da string. Mais cedo, mencionamos que cada um desses personagens era de dois bytes, de modo que significa que s será “Зд”.

O que aconteceria se fizéssemos &hello[0..1]? A resposta: entrará em pânico em tempo de execução, da mesma maneira que acessar um índice inválido em um vetor:

thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
character boundary', ../src/libcore/str/mod.rs:1694

Você deve usar isso com cautela, pois isso pode fazer com que seu programa falhe.

Métodos para Interagir Sobre Strings

Felizmente, existem outras maneiras de acessar elementos em um String.

Se precisarmos realizar operações em valores escalares Unicode individuais, a melhor maneira de fazer isso é usar o método chars. Chamando chars em “नमस्ते” é separado e retorna seis valores do tipo char, e você pode interar no resultado para acessar cada elemento:


#![allow(unused)]
fn main() {
for c in "नमस्ते".chars() {
    println!("{}", c);
}
}

Este código irá imprimir:

न
म
स
्
त
े

O método bytes retorna cada byte bruto, que pode ser apropriado para o seu domínio:


#![allow(unused)]
fn main() {
for b in "नमस्ते".bytes() {
    println!("{}", b);
}
}

Este código imprimirá os 18 bytes que compõem esse String, começando por:

224
164
168
224
// ... etc

Mas lembre-se de que os valores escalares Unicode válidos podem ser constituídos por mais de um byte.

Obter clusters de grafemas de strings é complexo, então esta funcionalidade não é fornecida pela biblioteca padrão. Existem crates disponíveis em crates.io se Esta é a funcionalidade que você precisa.

As Strings Não são tão Simples

Para resumir, as strings são complicadas. Diferentes linguagens de programação fazem escolhas diferentes sobre como apresentar essa complexidade ao programador. Rust optou por fazer o tratamento correto dos dados String o comportamento padrão para todos os programas Rust, o que significa que os programadores devem pensar mais no gerenciamento de dados UTF-8 antecipadamente. Este tradeoff expõe mais da complexidade de strings do que outras linguagens de programação, mas isso irá impedi-lo de ter que lidar com erros envolvendo caracteres não-ASCII mais tarde em seu ciclo de desenvolvimento.

Vamos mudar para algo um pouco menos complexo: hash maps!