Pular para o conteúdo

Escopo em Rust

Escopo (scope) é a região do código em que uma variável é válida. É uma ideia simples, mas entendê-la faz muitas outras regras do Rust (principalmente o ownership, ou posse) se encaixarem.

Uma variável existe a partir do ponto em que é declarada até o } que fecha o bloco em que ela está:

fn main() {
let x = 5; // x entra no escopo aqui
{
let y = 10; // y entra no escopo aqui
println!("x = {x}, y = {y}"); // as duas são válidas
} // y sai do escopo aqui
println!("x = {x}");
// println!("{y}"); // ❌ erro: y não está mais no escopo
}

Saída:

x = 5, y = 10
x = 5
▶ Experimente você mesmo

O y só existe dentro do bloco { } interno. Quando esse bloco termina, o y desaparece. Tentar usá-lo depois disso é um erro de compilação, e não uma surpresa durante a execução.

O escopo só funciona em um sentido: um bloco interno enxerga as variáveis dos blocos ao redor dele (é assim que o bloco interno imprime o x), mas um bloco externo não enxerga o que está dentro de um bloco interno.

Os parâmetros e as variáveis que você cria dentro de uma função só existem enquanto aquela chamada da função durar:

fn calculate() -> i32 {
let result = 5 * 2; // result só existe dentro de calculate
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ erro: result não existe aqui fora
}

Saída:

10
▶ Experimente você mesmo

O mesmo vale no sentido contrário: uma função não enxerga as variáveis locais do main. Se ela precisa de um valor, você o passa como parâmetro, como viu na página de Funções.

Um bloco { } não serve só para limitar onde as variáveis vivem. Ele também pode produzir um valor: a última expressão dentro dele, escrita sem ponto e vírgula. É a mesma regra que faz uma função retornar sua última linha.

Isso é útil quando você precisa de algumas variáveis auxiliares para calcular algo, mas não quer que elas fiquem sobrando depois:

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax só vive dentro deste bloco
let shipping = 5;
price + tax + shipping // sem ponto e vírgula: este é o valor do bloco
};
println!("total = {total}");
// println!("{tax}"); // ❌ erro: tax está fora do escopo
}

Saída:

total = 49
▶ Experimente você mesmo

Dois detalhes para ficar de olho:

  • A última linha, price + tax + shipping, não tem ponto e vírgula. Se você colocar um, o bloco devolve () (nada) em vez de um número, e o println! não consegue imprimi-lo.
  • A coisa toda é uma instrução let, então termina com }; depois da chave de fechamento.

Quando o bloco termina, tax (imposto) e shipping (frete) desaparecem. Só o resultado, total, fica. Manter variáveis auxiliares em um bloco pequeno assim deixa claro que elas só são necessárias para um cálculo.

Isso se liga diretamente ao ownership, que você vai conhecer um pouco mais adiante: um valor é limpo automaticamente no momento em que o escopo do seu dono termina.

fn main() {
{
let text = String::from("olá");
println!("{text}");
} // o escopo de text termina aqui: o Rust o libera automaticamente
}

Saída:

olá
▶ Experimente você mesmo

É todo esse mecanismo que permite ao Rust não precisar de um coletor de lixo (garbage collector): ele já sabe, na hora de compilar, exatamente onde cada escopo termina.

Lembra do shadowing (sombreamento) da página de Variáveis? Cada let com o mesmo nome dentro de um escopo cria uma variável diferente. Ela não apaga a de fora: só assume o lugar dela até o fim daquele escopo:

fn main() {
let x = 5;
{
let x = x * 2; // um novo x, só dentro deste bloco
println!("x interno = {x}"); // 10
}
println!("x externo = {x}"); // 5, sem alteração
}

Saída:

x interno = 10
x externo = 5
▶ Experimente você mesmo

Este programa não compila, porque area é usada depois que o bloco dela terminou:

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("área = {area}");
}

Mude o código para que ele imprima área = 12, mantendo height viva só dentro do bloco. Dica: faça o bloco produzir a área e guarde o resultado em uma variável declarada fora dele.

Ver solução
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("área = {area}");
}

Saída:

área = 12
▶ Experimente você mesmo

Agora area vive no escopo do main, então o println! consegue enxergá-la. A height continua sendo necessária só dentro do bloco, e é liberada quando o bloco termina.

Crie uma variável level com o valor 1. Abra um bloco que faz shadowing dela com 2 e, dentro dele, outro bloco que faz shadowing com 3. Imprima level em cada bloco e de novo logo depois que cada bloco fecha. Dica: os números devem aparecer como 1, 2, 3 e depois voltar para 2 e 1.

Ver solução
fn main() {
let level = 1;
println!("nível {level}");
{
let level = 2;
println!("nível {level}");
{
let level = 3;
println!("nível {level}");
}
println!("de volta ao nível {level}");
}
println!("de volta ao nível {level}");
}

Saída:

nível 1
nível 2
nível 3
de volta ao nível 2
de volta ao nível 1
▶ Experimente você mesmo

Cada level interno esconde o de fora só até o fim do seu bloco. As variáveis de fora nunca foram alteradas, então continuam ali, esperando.

  • Escopo é a região (marcada por { }) em que uma variável é válida.
  • Uma variável deixa de existir no instante em que o } que fecha o seu escopo é alcançado.
  • O corpo de uma função também é um escopo: as variáveis locais desaparecem quando a função retorna.
  • Um bloco é uma expressão: a última linha dele (sem ponto e vírgula) é o seu valor, e as variáveis auxiliares desaparecem quando ele termina.
  • É o escopo que diz ao Rust exatamente quando liberar um valor, e é por isso que não é preciso um coletor de lixo.

Teste o que aprendeu

1. Segundo esta página, por que o Rust não precisa de um coletor de lixo?

2. O que acontece quando você faz shadowing de uma variável com let x = x * 2; dentro de um bloco interno?

3. Qual é o valor de total depois de let total = { let a = 2; a * 10 };?

Pontuação: 0 / 3