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.
O escopo é definido pelas chaves
Seção intitulada “O escopo é definido pelas chaves”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 = 10x = 5O 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.
O corpo de uma função também é um escopo
Seção intitulada “O corpo de uma função também é um escopo”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:
10O 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.
Blocos também são expressões
Seção intitulada “Blocos também são expressõ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 = 49Dois 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 oprintln!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.
Por que o escopo importa para o ownership
Seção intitulada “Por que o escopo importa para o ownership”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áÉ 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.
Shadowing e escopo
Seção intitulada “Shadowing e escopo”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 = 10x externo = 5Prática
Seção intitulada “Prática”1. Corrija o erro de escopo
Seção intitulada “1. Corrija o erro de escopo”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 = 12Agora 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.
2. Níveis de aninhamento
Seção intitulada “2. Níveis de aninhamento”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 1nível 2nível 3de volta ao nível 2de volta ao nível 1Cada 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Variable Scope — The Rust Book (em inglês): como o escopo funciona, explicado como o primeiro passo rumo ao ownership.
- Scope and Shadowing — Rust by Example (em inglês): um exemplo curto e executável de blocos e shadowing.
- RAII — Rust by Example (em inglês): o que acontece com os valores quando o escopo deles termina, para quando você estiver pronto para o próximo passo.
- Block expressions — The Rust Reference (em inglês): as regras exatas dos blocos e dos valores que eles produzem.