Módulos e visibilidade
Nesta aula, exploramos o sistema de módulos do Rust, que organiza o código em unidades lógicas e controla a visibilidade de itens. Aprendemos a declarar módulos com 'mod', tornar itens públicos com 'pub', importar caminhos com 'use', e navegar por caminhos absolutos e relativos. Ao final, você será capaz de estruturar projetos Rust de forma modular e eficiente.
Bem-vindo à aula sobre módulos e visibilidade em Rust! Este é um dos pilares da organização de código na linguagem. Em qualquer projeto que vá além de um arquivo simples, você precisará dividir o código em módulos para manter tudo legível, reutilizável e com fronteiras claras de responsabilidade. Nesta aula, vamos mergulhar no sistema de módulos do Rust, que é ao mesmo tempo poderoso e um pouco diferente do que você pode estar acostumado em outras linguagens.
O sistema de módulos do Rust é baseado em arquivos e diretórios, mas também permite declarar módulos inline. A visibilidade (quem pode acessar o quê) é controlada por palavras-chave como pub e crate. Vamos entender como tudo isso se encaixa, desde a declaração de um módulo até a importação de itens com use. Prepare-se para ver muitos exemplos práticos, porque a melhor forma de aprender isso é na prática.
mod
A palavra-chave mod é usada para declarar um módulo. Um módulo é uma coleção de itens: funções, structs, enums, traits, constantes, e até mesmo outros módulos. Eles ajudam a agrupar código relacionado e a controlar a visibilidade. Por padrão, tudo dentro de um módulo é privado, ou seja, só é acessível dentro do próprio módulo e seus submódulos.
Existem duas formas de declarar um módulo: inline, usando mod com um bloco, ou em um arquivo separado. Vamos ver a forma inline primeiro:
mod math {
fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
// Isso não compila, pois add é privado
// println!("{}", math::add(2, 3));
}
Neste exemplo, criamos um módulo math com uma função add. Como não usamos pub, a função é privada e não pode ser acessada de fora do módulo. Para acessar, precisamos tornar a função pública com pub, o que veremos em seguida.
Quando o projeto cresce, é comum colocar cada módulo em um arquivo separado. Por exemplo, você pode ter um arquivo math.rs e declará-lo no main.rs com mod math;. O Rust procura o arquivo math.rs no mesmo diretório do arquivo que contém a declaração. Para módulos dentro de módulos, a estrutura de diretórios segue o mesmo padrão.
// main.rs
mod math;
fn main() {
println!("{}", math::add(2, 3));
}
// math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Nesse caso, a função add é pública (pub) e pode ser acessada de main. Note que o módulo math em si é privado por padrão, mas como estamos no mesmo crate, isso não impede o acesso. Se quiséssemos que o módulo math fosse acessível de fora do crate (por exemplo, se fosse uma biblioteca), precisaríamos declarar pub mod math;.
pub
A palavra-chave pub é usada para tornar um item público, ou seja, acessível fora do módulo onde ele está definido. Sem pub, tudo é privado. Isso é uma das grandes vantagens do Rust: o controle de visibilidade é explícito e rigoroso.
Vamos expandir o exemplo anterior e ver como pub funciona em diferentes níveis:
mod outer {
pub fn public_fn() {
println!("Chamando função pública");
}
fn private_fn() {
println!("Chamando função privada");
}
pub mod inner {
pub fn inner_public() {
println!("Função pública interna");
}
fn inner_private() {
println!("Função privada interna");
}
}
}
fn main() {
outer::public_fn(); // OK
// outer::private_fn(); // Erro: private_fn é privada
outer::inner::inner_public(); // OK
// outer::inner::inner_private(); // Erro: inner_private é privada
}
Além de pub, existem outras formas de visibilidade: pub(crate) torna o item público apenas dentro do crate, pub(super) torna visível para o módulo pai, e pub(in path) permite especificar um caminho exato. Isso é útil para APIs públicas de bibliotecas, onde você quer expor apenas o necessário.
Por exemplo, se você está criando uma biblioteca, pode usar pub(crate) para itens que são usados internamente, mas não devem ser expostos aos usuários da biblioteca. Isso ajuda a manter a API limpa e evita que usuários dependam de detalhes internos que podem mudar.
pub mod library {
pub fn public_api() {}
pub(crate) fn internal_helper() {}
}
Neste caso, internal_helper é acessível dentro do crate, mas não para quem usa a biblioteca externamente.
use
A palavra-chave use é usada para trazer caminhos para o escopo atual, facilitando a referência a itens sem precisar digitar o caminho completo toda vez. É como um atalho.
Por exemplo, em vez de escrever math::add toda vez, você pode usar use math::add; e depois chamar apenas add.
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
use math::add;
fn main() {
println!("{}", add(2, 3)); // agora podemos chamar add diretamente
}
Você também pode usar use com chaves para importar vários itens de um mesmo módulo:
mod math {
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn sub(a: i32, b: i32) -> i32 { a - b }
}
use math::{add, sub};
fn main() {
println!("{}", add(2, 3));
println!("{}", sub(5, 2));
}
E ainda, pode usar self para incluir o próprio módulo, e * para importar tudo que é público (embora isso não seja recomendado, pois pode poluir o escopo e causar ambiguidades).
use math::*;
Além disso, use também pode ser usado para criar aliases com as. Isso é útil quando dois itens têm o mesmo nome ou quando você quer encurtar um caminho longo.
use std::collections::HashMap as Map;
fn main() {
let mut map = Map::new();
map.insert("chave", 42);
}
Outra característica importante do use é que ele é lexical, ou seja, só vale no escopo onde foi declarado. Se você declarar um use dentro de uma função, ele só é válido dentro daquela função.
Caminhos absolutos e relativos
Para referenciar itens em módulos, usamos caminhos. Existem dois tipos: absolutos e relativos. Caminhos absolutos começam com crate (a raiz do crate atual) ou o nome de uma crate externa. Caminhos relativos começam com self, super ou um identificador.
No Rust 2018 e posteriores, os caminhos absolutos geralmente começam com crate para o crate atual, enquanto que antes usavam ::. Por exemplo:
crate::math::add(2, 3)
Isso significa: a partir da raiz do crate, vá para o módulo math e chame add. Caminhos relativos usam self para o módulo atual e super para o módulo pai. Por exemplo:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn add_twice(a: i32, b: i32) -> i32 {
// Caminho relativo: self::add
self::add(a, b) + self::add(a, b)
}
}
Também podemos usar super para acessar o módulo pai. Por exemplo, dentro de um módulo aninhado, podemos chamar funções do módulo pai com super::.
mod outer {
pub fn foo() { println!("foo"); }
pub mod inner {
pub fn bar() {
// chama a função do módulo pai
super::foo();
}
}
}
fn main() {
outer::inner::bar();
}
Na prática, você vai usar mais caminhos absolutos com crate quando estiver dentro do crate, e caminhos relativos quando estiver em módulos aninhados e quiser referenciar o pai ou o atual. A escolha entre absoluto e relativo depende do contexto e da legibilidade.
Uma dica: em projetos grandes, é comum usar use com caminhos absolutos no início dos arquivos, para evitar repetição e facilitar a leitura. Por exemplo, em vez de escrever crate::utils::helpers::format várias vezes, você pode fazer use crate::utils::helpers::format; no topo do arquivo.
Boas práticas e observações finais
Organizar o código em módulos é uma arte. Aqui vão algumas boas práticas:
- Mantenha módulos pequenos e focados: cada módulo deve ter uma responsabilidade clara.
- Use
pubapenas para o que é realmente necessário expor. Isso reduz a superfície de API e evita que usuários dependam de detalhes internos. - Prefira caminhos absolutos com
cratepara evitar ambiguidades, especialmente em módulos aninhados. - Use
usepara encurtar caminhos longos, mas eviteuse *em módulos grandes, pois pode causar conflitos de nomes. - Coloque as declarações
useno topo do arquivo, em ordem alfabética ou agrupadas, para facilitar a leitura.
Lembre-se de que o sistema de módulos do Rust é estático e resolvido em tempo de compilação, o que significa que erros de caminho são detectados cedo. Aproveite isso para refatorar sem medo.
Referências
- The Rust Programming Language - Managing Growing Projects
- Rust Reference - Modules
- Rust by Example - Modules
- Paths for Referring to an Item in the Module Tree
- Bringing Paths into Scope with the use Keyword
- Rust Reference - Visibility and Privacy
Exercícios
- Crie um módulo chamado
greetingscom uma função públicahelloque imprime "Olá, mundo!". Depois, chame essa função a partir domainusando caminho absoluto comcrate.
mod greetings {
pub fn hello() {
println!("Olá, mundo!");
}
}
fn main() {
crate::greetings::hello();
}
greetings do exercício anterior, adicione uma função privada internal_helper que imprime "Ajuda interna". Tente chamá-la de fora e veja o erro. Depois, torne-a pública e chame-a.mod greetings {
pub fn hello() {
println!("Olá, mundo!");
}
// privada
fn internal_helper() {
println!("Ajuda interna");
}
}
fn main() {
crate::greetings::hello();
// crate::greetings::internal_helper(); // erro: private function
}
Para torná-la pública, basta adicionar pub:
pub fn internal_helper() {
println!("Ajuda interna");
}
use para importar a função hello do módulo greetings e chamá-la sem o prefixo greetings::. Faça isso dentro da função main.mod greetings {
pub fn hello() {
println!("Olá, mundo!");
}
}
fn main() {
use crate::greetings::hello;
hello();
}
outer e outer::inner. No módulo inner, defina uma função pública print_message que imprime "Mensagem do inner". No módulo outer, defina uma função pública call_inner que chama inner::print_message usando caminho relativo com self.mod outer {
pub mod inner {
pub fn print_message() {
println!("Mensagem do inner");
}
}
pub fn call_inner() {
self::inner::print_message();
}
}
fn main() {
outer::call_inner();
}
crate ou o nome da crate. Por exemplo, crate::outer::inner::print_message(). Caminhos relativos começam a partir do módulo atual, usando self (módulo atual) ou super (módulo pai). Por exemplo, dentro de outer, podemos usar self::inner::print_message() ou super::outer::inner::print_message() se estivermos em um módulo mais profundo. A diferença é que absolutos são independentes da posição atual, enquanto relativos dependem do contexto.