String vs &str em Rust: Entendendo as Duas Formas de Texto
O que são String e &str?
Em Rust, temos duas formas principais de representar texto: String e &str. A diferença fundamental está em como a memória é gerenciada e onde os dados são armazenados. String é um tipo alocado dinamicamente no heap, você é responsável por seu gerenciamento, enquanto &str é uma referência a uma sequência de bytes UTF-8 válida, geralmente alocada na stack ou na seção de dados do binário.
Pense em String como um vetor de caracteres que você pode modificar livremente — tem capacidade, comprimento e proprietário. Já &str é uma visão imutável sobre esses dados: um "empréstimo" que aponta para um local de memória sem poder alterar o conteúdo. Essa distinção é central na filosofia de segurança de memória do Rust.
fn main() {
// String: alocado no heap, mutável, proprietário
let mut s1 = String::from("Olá");
s1.push_str(", Mundo!");
println!("{}", s1); // "Olá, Mundo!"
// &str: referência imutável, literais são na stack/binário
let s2: &str = "Olá, Rust!";
println!("{}", s2); // "Olá, Rust!"
// &str pode vir de uma String
let s3 = String::from("Teste");
let referencia: &str = &s3;
println!("{}", referencia); // "Teste"
}
Diferenças de Performance e Memória
String consome mais memória porque armazena três informações no stack: um ponteiro para os dados no heap, a capacidade e o comprimento atual. A cada alocação, pode haver fragmentação de memória. &str, por sua vez, é apenas um ponteiro (64 bits) e um comprimento (64 bits) — 16 bytes no total em arquiteturas de 64 bits — tornando-a muito mais leve para passar entre funções.
Quando você trabalha com strings literais como "Olá", o compilador as coloca como dados imutáveis no binário (seção .rodata), e &str simplesmente aponta para lá. Não há alocação dinâmica. Use &str como padrão em parâmetros de funções; use String apenas quando você realmente precisa possuir e modificar o texto.
// Não faça isto (ineficiente):
fn processar_texto(texto: String) {
println!("{}", texto);
}
// Faça isto:
fn processar_texto(texto: &str) {
println!("{}", texto);
}
fn main() {
let s = String::from("Dados importantes");
processar_texto(&s); // passa &str, não String
processar_texto("Literal direto"); // funciona naturalmente
}
Conversão e Coerção
Rust permite coerção automática de String para &str em muitos contextos. Quando você passa uma String para uma função que espera &str, o compilador automaticamente converte. Isso é uma das características mais úteis da linguagem — você possui uma String, mas empresta apenas uma visão quando necessário.
Para converter explicitamente, use &string[..] ou &string (quando esperado &str). Para ir na direção oposta, de &str para String, use String::from(), .to_string() ou .to_owned(). Entender essas conversões é crucial para não lutar contra o borrow checker do Rust.
fn saudacao(nome: &str) -> String {
format!("Olá, {}!", nome)
}
fn main() {
// Coerção automática: String vira &str
let nome = String::from("Alice");
let msg = saudacao(&nome); // &nome é &str
// Conversão explícita
let slice: &str = &nome[0..5]; // "&str" a partir de parte de String
println!("{}", slice); // "Alice"
// &str para String
let literai: &str = "Bob";
let proprietario: String = literai.to_string();
println!("{}", proprietario); // "Bob"
}
Quando Usar Cada Uma
Use &str em assinaturas de funções como argumento padrão — é ergonômico e eficiente. Use String quando você precisa possuir, modificar ou construir texto dinamicamente, como em loops que concatenam dados ou ao ler de entrada do usuário. Em estruturas de dados, String é a escolha padrão quando você precisa armazenar texto; &str é usada para referências que vivem enquanto seus dados subjacentes existem.
Não tenha medo de ter String em suas estruturas. A cláusula de tempo de vida em &str frequentemente torna o código mais complexo sem benefício real. Reserve &str para quando realmente está apenas emprestando texto, não para quando é responsável por sua existência.
#[derive(Debug)]
struct Pessoa {
nome: String, // proprietária dos dados
email: String,
}
impl Pessoa {
fn new(nome: &str, email: &str) -> Self {
Pessoa {
nome: nome.to_string(),
email: email.to_string(),
}
}
fn apresentar(&self) -> String {
format!("Sou {} ({})", self.nome, self.email)
}
}
fn main() {
let pessoa = Pessoa::new("Carlos", "carlos@email.com");
println!("{}", pessoa.apresentar());
}
Conclusão
Os três pontos essenciais: primeiro, String é alocado dinamicamente e mutável, enquanto &str é uma referência imutável e leve; segundo, a coerção automática permite passar String onde &str é esperado, facilitando a vida; terceiro, use &str em parâmetros de funções e String em propriedades de estruturas e quando você controla o ciclo de vida do dado.
Dominar essa distinção é fundamental em Rust. Investir tempo compreendendo String vs &str elimina frustrações futuras com o borrow checker e resulta em código mais eficiente.