Variáveis, mutabilidade e shadowing

1. Introdução às Variáveis em Rust

Em Rust, toda variável precisa ser declarada antes de ser usada. A forma mais básica de declaração utiliza a palavra-chave let, que permite ao compilador inferir o tipo da variável automaticamente na maioria dos casos:

let idade = 25;           // i32 inferido
let nome = "Ana";         // &str inferido
let altura = 1.75;        // f64 inferido

A característica mais marcante das variáveis em Rust é que elas são imutáveis por padrão. Isso significa que, uma vez atribuído um valor, você não pode alterá-lo:

let x = 5;
x = 6; // Erro! Não é possível atribuir duas vezes a uma variável imutável

Essa imutabilidade padrão não é um capricho — ela traz benefícios reais de segurança e previsibilidade. Em programas concorrentes, variáveis imutáveis eliminam inteiramente a possibilidade de data races. Em código sequencial, torna mais fácil raciocinar sobre o fluxo do programa, pois você sabe que um valor não será modificado inesperadamente.

Boas práticas: prefira sempre variáveis imutáveis. Use mutabilidade apenas quando houver uma razão clara e necessária.

2. Mutabilidade com let mut

Quando você precisa que uma variável mude de valor ao longo do tempo, use let mut:

let mut contador = 0;
contador += 1; // Agora contador vale 1
contador += 1; // Agora contador vale 2

Situações comuns que exigem mutabilidade incluem:

  • Loops e acumuladores: somar valores, contar iterações
  • Estado mutante: variáveis que representam estado de uma aplicação (posição em um jogo, saldo bancário)
  • Buffers e coleções: vetores que recebem novos elementos
let mut soma = 0;
for i in 1..=10 {
    soma += i;
}
println!("Soma de 1 a 10: {}", soma); // 55

É importante distinguir mutabilidade da variável de mutabilidade do dado. A variável mut indica que podemos alterar a referência ou o valor que ela armazena. Dados em si (como structs) podem ter campos mutáveis independentemente.

struct Ponto {
    x: i32,
    y: i32,
}

let mut p = Ponto { x: 10, y: 20 };
p.x = 30; // Permitido porque a variável p é mutável

3. Constantes com const

Constantes são declaradas com const e exigem tipo explícito:

const PI: f64 = 3.14159;
const MAX_USUARIOS: u32 = 100_000;

Diferenças cruciais entre const e let imutável:

Característica const let (imutável)
Tipo explícito Obrigatório Opcional (inferido)
Momento da avaliação Compilação Runtime
Escopo Global ou local Local
Expressões permitidas Apenas constantes Qualquer expressão
const TAXA: f64 = 0.15;

fn calcula_imposto(valor: f64) -> f64 {
    valor * TAXA // TAXA é conhecida em tempo de compilação
}

Use constantes para valores mágicos, limites máximos, taxas e configurações que não mudam. Isso melhora a legibilidade e facilita manutenção.

4. Shadowing: Reutilizando Nomes

Shadowing (sombreamento) permite redeclarar uma variável com o mesmo nome, criando uma nova variável que "esconde" a anterior:

let x = 5;
let x = x + 1; // Novo x vale 6, o antigo não existe mais
let x = x * 2; // Novo x vale 12
println!("x = {}", x); // 12

Uma característica poderosa do shadowing é que você pode mudar o tipo da variável:

let espaco = "   ";           // &str
let espaco = espaco.len();    // usize (número de espaços)
println!("Tamanho: {}", espaco); // 3

Isso é útil em transformações em etapas, onde uma variável passa por diferentes representações:

let entrada = "42";
let entrada: i32 = entrada.parse().expect("Não é número");
let entrada = entrada + 10;
println!("Resultado: {}", entrada); // 52

5. Shadowing vs. Mutabilidade

Embora ambos permitam alterar o valor associado a um nome, shadowing e mutabilidade são conceitos fundamentalmente diferentes:

Mutabilidade (let mut) altera a mesma variável. A variável mantém sua identidade e tipo.

Shadowing (let) cria uma nova variável. A antiga é descartada, e a nova pode ter tipo diferente.

// Mutabilidade
let mut y = 10;
y = y + 5; // y é a mesma variável, ainda i32

// Shadowing
let z = 10;
let z = z + 5; // z é uma nova variável

Quando preferir shadowing:

  • Transformações em etapas com mudança de tipo
  • Operações temporárias que não precisam de escopo adicional
  • Melhorar clareza ao reutilizar um nome significativo
// Shadowing elegante para transformações
let nome_completo = "Maria Silva";
let nome_completo = nome_completo.trim();
let nome_completo = nome_completo.to_lowercase();

6. Escopo e Ciclo de Vida das Variáveis

Variáveis em Rust existem dentro de blocos {}. Ao sair do bloco, a variável é destruída:

let a = 10;
{
    let b = 20;
    println!("Dentro: a={}, b={}", a, b); // a=10, b=20
}
// println!("{}", b); // Erro! b não existe mais

Shadowing dentro de escopos internos é uma técnica poderosa:

let x = 5;
{
    let x = 10; // Sombra a variável externa
    println!("Interno: {}", x); // 10
}
println!("Externo: {}", x); // 5 (a original permanece)

Isso permite limitar o efeito de transformações temporárias sem poluir o escopo externo.

7. Exemplos Práticos e Boas Práticas

Exemplo 1: Calcular média com shadowing

fn main() {
    let notas = vec![8.5, 7.0, 9.2, 6.8];

    // Shadowing para transformar a soma em média
    let media = notas.iter().sum::<f64>();
    let media = media / notas.len() as f64;

    println!("Média: {:.2}", media);
}

Exemplo 2: Processamento de entrada do usuário

use std::io;

fn main() {
    println!("Digite sua idade:");

    let mut entrada = String::new();
    io::stdin().read_line(&mut entrada).expect("Erro de leitura");

    let entrada = entrada.trim(); // Remove quebra de linha
    let idade: u32 = entrada.parse().expect("Idade inválida");

    if idade >= 18 {
        println!("Maior de idade");
    } else {
        println!("Menor de idade");
    }
}

Dicas finais:

  • Evite shadowing excessivo que dificulte a leitura
  • Prefira nomes descritivos diferentes para conceitos diferentes
  • Use shadowing para transformações claras em etapas
  • Prefira imutabilidade sempre que possível
  • Use const para valores que são realmente constantes em tempo de compilação

Dominar variáveis, mutabilidade e shadowing é essencial para escrever código Rust idiomático, seguro e eficiente. Esses conceitos formam a base para entender ownership, empréstimos e lifetimes — os pilares da segurança de memória em Rust.

Referências