Rust: Variáveis e Imutabilidade

As variáveis no Rust são imutáveis por padrão, e esse detalhe muda a forma como você escreve e lê o código desde a primeira linha. O Rust tem fama de ser uma linguagem exigente, por isso vale entender bem um dos seus conceitos centrais: a imutabilidade e o efeito dela sobre cada valor que você declara.

Como declarar variáveis no Rust

O Rust adota um esquema de variáveis que surpreende quem vem de outras linguagens. Afinal, uma variável comum não pode ter o valor alterado depois de criada.

Assim como na maioria das linguagens, as variáveis no Rust precisam ser declaradas antes do uso. Além disso, o compilador exige que cada variável receba um valor antes de ser lida.

A declaração usa a palavra reservada let, bem familiar para quem programa em JavaScript ou TypeScript. Inclusive, se você quiser comparar os dois mundos, vale ler o artigo sobre variáveis no JavaScript.

// Exemplo de declaração de variáveis
let a = 10;
let b: u32 = 20;
let c = a + b;

Nesse trecho, criamos três variáveis no Rust, mas só a variável b tem o tipo escrito de forma explícita. Mesmo assim, o código compila sem nenhum problema.

Definição do tipo

O Rust é uma linguagem de tipagem estática e forte. Em outras palavras, o compilador precisa conhecer o tipo de todas as variáveis no momento da compilação.

No entanto, você não precisa escrever o tipo em todo lugar. Quando a anotação não existe, o compilador infere o tipo a partir do valor e da forma como você usa a variável.

Na variável b, usamos o tipo u32, um inteiro sem sinal de 32 bits. Ou seja, ele ocupa 4 bytes e aceita apenas valores de 0 a 4.294.967.295.

Já nas variáveis a e c, não escrevemos o tipo. Se a aparecesse sozinha, o compilador escolheria i32, porque esse é o tipo inteiro padrão da linguagem.

Porém, a variável a participa de uma soma com b, que é u32. Por isso, a inferência conclui que a também é u32, e o resultado em c segue o mesmo tipo.

Esse comportamento mostra como a inferência considera o contexto inteiro, e não apenas o literal. Os tipos de dados merecem um estudo próprio, mas aqui o foco são realmente as variáveis no Rust.

As partes de uma declaração

De qualquer forma, podemos dividir uma declaração em quatro partes principais. Primeiro vem a palavra reservada, depois o nome, em seguida o tipo opcional e, por fim, o valor inicial.

keyword       variable type
   |               |
let <name>: <type> = <value>;
      |                 |
 variable name     initial value

Na prática, a anotação de tipo fica entre o nome e o sinal de igual, sempre precedida por dois pontos. Dessa forma, a leitura da linha segue a ordem natural: nome, tipo e valor.

Variáveis no Rust sem valor inicial

O valor inicial pode ficar para depois, desde que a variável receba um valor antes da primeira leitura. Nesse caso, o compilador acompanha todos os caminhos do código para garantir essa regra.

fn main() {
    let status;
    let is_active = true;

    // A variável recebe o valor uma única vez, em cada caminho
    if is_active {
        status = "active";
    } else {
        status = "inactive";
    }

    println!("Status: {}", status);
}

Repare que a variável status continua imutável, porque recebe o valor uma única vez. Por outro lado, se você tentar ler status antes da atribuição, o compilador recusa o código.

Essa regra elimina uma classe inteira de bugs comuns em outras linguagens, como a leitura de memória não inicializada. Com isso, as variáveis no Rust sempre têm um valor válido quando você as usa.

Imutabilidade das variáveis no Rust

Agora vamos analisar outro código, com duas operações bem simples. Antes de ler o resultado, tente prever o que acontece.

fn main() {
    let a: u32 = 255;
    a = 200;
    println!("{}", a);
}

Se você achou que a tela mostraria 200, errou. Na verdade, essa aplicação nem chega a compilar.

error[E0384]: cannot assign twice to immutable variable `a`

Esse erro diz que não podemos atribuir um valor duas vezes a uma variável imutável. Afinal, as variáveis no Rust nascem imutáveis, e o compilador recusa qualquer nova atribuição.

Talvez você pense que, se o valor não muda, então se trata de uma constante. Contudo, constantes e variáveis imutáveis têm diferenças importantes, e vamos ver cada uma delas mais adiante.

Por que forçar a imutabilidade

Uma variável é um espaço na memória ligado a um nome que reservamos no código. Com ela, reaproveitamos conteúdos, guardamos resultados e passamos dados entre partes do programa.

Se uma variável sofre uma mutação silenciosa no meio da execução, o programa pode falhar. De fato, boa parte dos bugs nasce exatamente de um valor que mudou quando ninguém esperava.

No mundo do .NET, temos conceitos como efeitos colaterais e o Open-Closed Principle, do SOLID, que buscam reduzir esse risco. Da mesma forma, o C# trata strings como imutáveis para evitar alterações inesperadas.

O Rust foi além, porque tornou a imutabilidade o comportamento padrão. Assim, o primeiro valor atribuído permanece até o fim do escopo, a menos que você peça o contrário de forma explícita.

Segundo a documentação oficial do Rust, essa escolha evita que uma parte do código altere um valor que outra parte considera fixo. Em suma, o compilador garante que o valor não muda, e você não precisa rastrear isso de cabeça.

Por consequência, as variáveis no Rust tornam o código mais fácil de entender. Quando você lê um let sem mut, já sabe que aquele valor não vai mudar em lugar nenhum.

Imutabilidade e threads

A imutabilidade é um dos pilares que tornam o Rust seguro, principalmente em código concorrente. Para entender isso, imagine duas threads que compartilham uma variável.

Vamos criar um cenário em que as duas tentam incrementar o mesmo contador. Nesse caso, a consistência dos dados depende de quem altera o valor e quando.

use std::thread;

fn main() {
    // Criamos uma variável imutável chamada "counter"
    let counter = 0;

    // Criamos duas threads que tentam incrementar o contador
    let thread1 = thread::spawn(|| {
        for _ in 0..1000 {
            // Tentativa de incrementar o contador
            // Isso resulta em um erro de compilação!
            counter += 1;
        }
    });

    let thread2 = thread::spawn(|| {
        for _ in 0..1000 {
            // Tentativa de incrementar o contador
            // Isso também resulta em um erro de compilação!
            counter += 1;
        }
    });

    // Aguardamos a conclusão das threads
    thread1.join().unwrap();
    thread2.join().unwrap();

    // Exibimos o valor final do contador
    println!("Final counter value: {}", counter);
}

Nesse exemplo, criamos uma variável imutável chamada counter. Em seguida, duas threads tentam incrementar o mesmo valor em paralelo.

No entanto, o compilador impede a operação, pois a variável é imutável. Portanto, o programa nem chega a rodar.

Em outras linguagens, bastaria tornar o contador mutável para cair em uma condição de corrida, com resultados diferentes a cada execução. No Rust, adicionar mut não resolve, porque o compilador também recusa duas threads com acesso mutável ao mesmo valor.

Em outras palavras, o sistema de ownership e borrowing barra o problema antes da execução. Se você usar move nas closures, cada thread recebe uma cópia do inteiro, e o contador original continua valendo zero.

Para compartilhar um valor mutável entre threads, o Rust pede ferramentas próprias. A solução clássica combina Arc, que conta referências de forma atômica, com Mutex, que garante acesso exclusivo.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc permite compartilhar o valor; Mutex controla o acesso
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..2 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..1000 {
                // lock() garante que só uma thread altera o valor por vez
                let mut num = counter.lock().unwrap();
                *num += 1;
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    // Sempre exibe 2000
    println!("Final counter value: {}", *counter.lock().unwrap());
}

Repare que a variável counter continua imutável, mas o Mutex libera a alteração de forma controlada. Dessa forma, a mutação existe, porém sempre com regras claras e verificadas.

Se você trabalha com C#, a ideia lembra o uso de travas e semáforos, como mostra o artigo sobre Semaphore e SemaphoreSlim. A diferença é que, no Rust, o compilador cobra essa disciplina.

Como tornar variáveis no Rust mutáveis

Embora as variáveis no Rust sejam imutáveis por padrão, podemos torná-las mutáveis com a palavra-chave mut. Ela fica logo depois do let e antes do nome.

Então, se pegarmos o exemplo anterior, que falhava com o erro E0384, e adicionarmos mut à variável a, o código passa a funcionar.

fn main() {
    let mut a: u32 = 255;
    a = 200;
    println!("{}", a);
}

// Saída: 200

Nesse caso, a variável aceita uma nova atribuição, desde que o novo valor tenha o mesmo tipo. Ou seja, mut libera a troca do valor, mas nunca a troca do tipo.

Além disso, o mut funciona como uma declaração de intenção. Qualquer pessoa que leia o código já sabe que aquele valor vai mudar em algum ponto.

Por outro lado, se você declarar uma variável com mut e nunca alterá-la, o compilador emite um aviso. Assim, a linguagem incentiva você a marcar como mutável apenas o que realmente muda.

Mutabilidade e imutabilidade em funções

Agora considere uma função que soma dois inteiros e retorna o resultado, que também é um inteiro.

fn main() {
    let a = 10;
    let b = 10;
    let result: u32 = add(a, b);
    println!("Result: {}", result);
}

fn add(x: u32, y: u32) -> u32 {
    return x + y;
}

Esse código executa sem problemas, porque não modificamos nenhuma variável. Tudo o que fazemos é ler x e y e devolver um novo valor com a soma dos dois.

Inclusive, a inferência entra em ação aqui também. Como add recebe parâmetros u32, o compilador conclui que a e b também são u32.

Porém, vamos supor que precisamos incrementar os valores antes da soma. Em outras palavras, precisamos modificar os parâmetros.

fn main() {
    let a = 10;
    let b = 10;
    let result: u32 = increment_and_sum(a, b);
    println!("Result: {}", result);
}

fn increment_and_sum(x: u32, y: u32) -> u32 {
    x += 1;
    y += 1;
    return x + y;
}

Novamente, o compilador mostra o erro error[E0384]: cannot assign to immutable argument x. Afinal, tentamos alterar um valor imutável.

Onde colocar o mut

Agora vem a questão: onde você adicionaria o mut? Nas variáveis do main ou nos parâmetros da função?

A resposta correta é nos parâmetros. Isso mesmo, quando a função precisa modificar um parâmetro, ele também recebe o mut.

fn increment_and_sum(mut x: u32, mut y: u32) -> u32 {
    x += 1;
    y += 1;
    return x + y;
}

Isso acontece porque os parâmetros recebem uma cópia dos valores a e b, já que inteiros implementam a trait Copy. Portanto, as variáveis do main continuam imutáveis e não mudam depois da chamada.

Também vale notar que o return explícito é opcional. Em código idiomático, a última expressão da função, sem ponto e vírgula, já define o retorno.

Constantes e variáveis no Rust

Mais cedo, surgiu a dúvida sobre constantes. Embora pareçam iguais a variáveis imutáveis, elas seguem regras próprias.

Primeiro, você declara uma constante com const, e não com let. Além disso, a anotação de tipo é obrigatória, e o mut nunca pode acompanhar uma constante.

// Constantes usam letras maiúsculas e exigem o tipo
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Também é possível declarar constantes em qualquer escopo, inclusive no escopo global. Por outro lado, o valor precisa ser uma expressão constante, calculada em tempo de compilação.

Por fim, a convenção do Rust pede nomes em maiúsculas, com sublinhado entre as palavras. Enquanto as variáveis no Rust guardam valores que surgem durante a execução, as constantes registram valores fixos que valem para o programa inteiro.

Shadowing de variáveis no Rust

O Rust tem um conceito chamado shadowing, que permite declarar uma nova variável com o mesmo nome de outra que já existe. Isso costuma assustar no primeiro contato.

A nova variável faz sombra à anterior. Com isso, a antiga deixa de ser acessível no restante daquele escopo.

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}

// The value of x is: 12

Cada linha com let cria uma variável nova, calculada a partir da anterior. Assim, o valor final é 12, mesmo sem nenhuma variável mutável no código.

Shadowing e escopo

O shadowing respeita os blocos de código. Quando você faz sombra dentro de um bloco interno, a variável externa volta a valer assim que o bloco termina.

fn main() {
    let x = 5;
    let x = x + 1;
    {
        // Esta sombra vale apenas dentro do bloco
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }
    println!("The value of x is: {x}");
}

// The value of x in the inner scope is: 12
// The value of x is: 6

Nesse exemplo, o bloco interno enxerga o valor 12. Contudo, depois das chaves, o x externo continua valendo 6.

Shadowing não é o mesmo que mut

Shadowing não equivale a tornar uma variável mutável. Com shadowing, você cria uma nova variável, e isso permite inclusive trocar o tipo do valor.

fn main() {
    let x: u32 = 5;
    let x = String::from("Hello, world!");
    println!("The value of x is: {}", x);
}

// The value of x is: Hello, world!

Se você tentar o mesmo com mut, o compilador recusa. Afinal, uma variável mutável aceita outro valor, mas sempre do mesmo tipo.

fn main() {
    let mut spaces = "   ";
    // error[E0308]: mismatched types
    spaces = spaces.len();
}

Além disso, o shadowing exige um novo let. Por isso, uma atribuição acidental sem let continua gerando erro de compilação, e a variável segue imutável depois das transformações.

O shadowing também permite reaproveitar nomes em vez de inventar variações como spaces_str e spaces_num. Dessa forma, você transforma valores sem torná-los mutáveis e converte tipos quando necessário.

Pode parecer estranho no começo. Porém, quando você estudar borrowing e ownership, vai perceber que esse conceito se encaixa muito bem no restante da linguagem.

Resumo das regras das variáveis no Rust

Para fixar o conteúdo, vale reunir as regras em uma lista curta. Assim, você consegue consultar rapidamente quando surgir uma dúvida.

  • let cria variáveis no Rust, que são imutáveis por padrão.
  • let mut permite novas atribuições, sempre com o mesmo tipo.
  • const cria valores fixos, com tipo obrigatório e nome em maiúsculas.
  • Um novo let com o mesmo nome faz shadowing e pode trocar o tipo.
  • Parâmetros de função também são imutáveis, a menos que recebam mut.

Na prática, comece sempre com variáveis imutáveis e adicione mut apenas quando o código realmente precisar. Então, deixe o compilador apontar onde a mutação faz falta.

Conclusão

O Rust oferece bastante flexibilidade na criação de variáveis, mas impõe regras que colocam a segurança sempre em primeiro plano. A imutabilidade padrão, o mut explícito e o shadowing trabalham juntos para evitar mudanças inesperadas.

Na verdade, a linguagem inteira segue essa filosofia. Por isso, entender bem as variáveis no Rust ajuda você a progredir com mais confiança nos próximos conceitos, como tipos de dados, ownership e borrowing.

Se você ainda está decidindo qual linguagem estudar, o artigo Além do código: qual linguagem devo aprender pode ajudar na escolha.

FAQ

As variáveis no Rust são sempre imutáveis?

Elas são imutáveis por padrão, mas você pode torná-las mutáveis com a palavra-chave mut. Assim, a mutação sempre fica explícita no código.

Qual a diferença entre const e let no Rust?

O const exige anotação de tipo, aceita apenas expressões constantes e pode ficar no escopo global. Já o let cria variáveis com valores calculados durante a execução.

Shadowing é o mesmo que usar mut?

Não, porque o shadowing cria uma nova variável com o mesmo nome. Por isso, ele permite trocar o tipo, algo que o mut não permite.

O que significa o erro E0384?

O erro E0384 indica uma tentativa de atribuir um novo valor a uma variável ou parâmetro imutável. A correção é adicionar mut ou usar shadowing.

Qual é o tipo inteiro padrão no Rust?

Quando o contexto não define outro tipo, o compilador usa i32. Contudo, a inferência pode escolher outro tipo conforme o uso da variável.

Glossário

Termo Significado
Imutabilidade Característica de um valor que não pode ser alterado depois de criado
let Palavra-chave que declara uma variável no Rust
mut Palavra-chave que torna uma variável ou parâmetro mutável
const Palavra-chave que declara uma constante avaliada em tempo de compilação
Shadowing Declaração de uma nova variável com o mesmo nome de outra, que fica oculta
Inferência de tipo Capacidade do compilador de deduzir o tipo a partir do valor e do uso
u32 Inteiro sem sinal de 32 bits
i32 Inteiro com sinal de 32 bits, tipo inteiro padrão do Rust
Condição de corrida Falha em que o resultado depende da ordem de execução de threads concorrentes
Arc Ponteiro com contagem de referências atômica, seguro entre threads
Mutex Trava de exclusão mútua que permite um único acesso por vez ao dado

Fontes