Quando comecei a estudar Rust, tive certa dificuldade em escolher qual tipo representaria melhor meus dados em diferentes situações. Acredito que isso se deve, em parte, ao tempo que passei utilizando linguagens de alto nível de abstração, como TypeScript, onde raramente preciso pensar sobre isso. Por exemplo, em TypeScript, strings são apenas strings.

Com o tempo, fui criando minhas próprias regras, baseadas no que funcionava para mim. Recentemente, porém, esbarrei nesse ótimo post de Steve Klabnik. Para quem quiser uma explicação mais detalhada, recomendo a leitura do artigo original. Ele apresenta regras utilizando uma lógica de adoção por níveis.

Sem a pretensão de traduzir inteiramente o conteúdo, decidi sintetizar aqui o conjunto de regras que, segundo o próprio autor, cobre a maior parte das situações de uso de strings em Rust. O raciocínio não difere muito do que eu já vinha aplicando, mas é sempre bom ter suas ideias validadas por alguém mais experiente, o que traz mais confiança na aplicação delas.

Regra 1

Sempre utilize String em structs.

1struct Person {
2    name: String,
3}

Steve destaca, no nível 4 de suas regras, que existem algumas exceções em que &str pode ser uma opção melhor. No entanto, se você ainda está se perguntando quando isso se aplica, é melhor simplificar e sempre usar String em structs. A energia mental necessária para esse tipo de otimização muitas vezes não compensa.

Regra 2

Com funções, utilize o tipo &str para parâmetros e o tipo String para valores de retorno.

Consideremos o código abaixo, que utiliza somente o tipo String:

 1fn first_word(words: String) -> String {
 2    words
 3        .split_whitespace()
 4        .next()
 5        .expect("words should not be empty")
 6        .to_string()
 7}
 8
 9fn main() {
10    let sentence = "Hello, world!";
11
12    println!("{}", first_word(sentence.to_string()));
13
14    let owned = String::from("A string");
15
16    println!("{}", first_word(owned.clone()));
17    println!("{}", first_word(owned));
18}

Perceba que, na linha 16, precisamos chamar owned.clone(), ou não seria possível usar o valor novamente na linha 17.

Podemos, então, converter nosso código para a segunda versão abaixo:

 1fn first_word(words: &str) -> String {
 2    words
 3        .split_whitespace()
 4        .next()
 5        .expect("words should not be empty")
 6        .to_string()
 7}
 8
 9fn main() {
10    let sentence = "Hello, world!";
11
12    println!("{}", first_word(sentence));
13
14    let owned = String::from("A string");
15
16    println!("{}", first_word(&owned));
17    println!("{}", first_word(&owned));
18}

Agora, na linha 1, declaramos o parâmetro do tipo &str. Assim, nas linhas 16 e 17, passamos referências imutáveis como argumento para a função, dispensando o clone e, assim, uma cópia desnecessária.

Não se preocupe com o risco de não utilizar o operador & no argumento ao chamar a função, pois o compilador Rust estará lá para te avisar sobre o que está faltando, se for o caso.

E é isso. O autor diz (de forma empírica, sem dados precisos) que essas duas regras simples resolverão 95% dos casos. Para mim, isso é mais do que suficiente. Pelo menos por enquanto.