Módulos e visibilidade
Nesta aula, você aprenderá a organizar código em Rust usando módulos, a controlar a visibilidade de itens com 'pub' e a simplificar referências com 'use'. Também exploraremos caminhos absolutos e relativos, fundamentais para projetos Rust bem estruturados.
Módulos são a base da organização de código em Rust. Eles permitem agrupar funções, structs, enums, constantes e até outros módulos, criando uma hierarquia que reflete a estrutura lógica do seu projeto. Sem módulos, todo o código ficaria em um único arquivo, o que é inviável para projetos reais. Com eles, você pode dividir o código em partes menores, reutilizáveis e de fácil navegação.
Além de organizar, os módulos controlam a visibilidade: o que é público (acessível fora do módulo) e o que é privado (acessível apenas dentro do módulo). Isso é essencial para encapsulamento e para criar APIs limpas. Nesta aula, veremos como declarar módulos com mod, como expor itens com pub, como importar caminhos com use e como navegar entre módulos usando caminhos absolutos e relativos.
mod
A palavra-chave mod é usada para declarar um módulo. Um módulo pode ser definido inline no mesmo arquivo ou em um arquivo separado. Quando você escreve mod nome; dentro de um arquivo, o Rust procura por um arquivo nome.rs ou nome/mod.rs no mesmo diretório. Isso permite dividir o código em múltiplos arquivos.
Exemplo de módulo inline:
mod matematica {
pub fn soma(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
println!("{}", matematica::soma(2, 3));
}Neste exemplo, definimos um módulo chamado matematica com uma função pública soma. Para acessar a função, usamos o caminho matematica::soma. Sem o pub, a função não seria acessível fora do módulo, causando um erro de compilação.
Para módulos em arquivos separados, crie um arquivo matematica.rs com o mesmo conteúdo da função e, no arquivo principal, declare mod matematica;. O Rust automaticamente carrega o arquivo. Isso é fundamental para manter o código organizado em projetos maiores.
pub
A visibilidade em Rust é privada por padrão. Isso significa que itens (funções, structs, campos, etc.) definidos em um módulo só podem ser acessados dentro do próprio módulo e seus submódulos. Para tornar um item acessível fora do módulo, usamos a palavra-chave pub.
Exemplo:
mod mensagens {
// Função privada - acessível apenas dentro de 'mensagens'
fn formatar(texto: &str) -> String {
format!("Mensagem: {}", texto)
}
// Função pública - acessível de fora
pub fn exibir(texto: &str) {
println!("{}", formatar(texto));
}
}
fn main() {
// mensagens::formatar("Olá"); // Erro: função privada
mensagens::exibir("Olá"); // OK
}No exemplo, formatar é privada, então não pode ser chamada de fora. exibir é pública e pode ser chamada. Esse controle é crucial para esconder detalhes de implementação e expor apenas o que é necessário.
Para structs, você precisa marcar cada campo como pub se quiser que eles sejam acessíveis externamente. Caso contrário, a struct pode ser construída, mas os campos não podem ser lidos ou alterados.
mod pessoa {
pub struct Pessoa {
pub nome: String,
idade: u8, // privado
}
impl Pessoa {
pub fn nova(nome: String, idade: u8) -> Self {
Self { nome, idade }
}
pub fn idade(&self) -> u8 {
self.idade
}
}
}
fn main() {
let p = pessoa::Pessoa::nova("Ana".to_string(), 30);
println!("Nome: {}", p.nome); // OK
// println!("{}", p.idade); // Erro: campo privado
println!("Idade: {}", p.idade()); // OK via método
}Aqui, o campo nome é público, mas idade é privado. Para acessar a idade, fornecemos um método público idade(). Isso é uma prática comum para encapsulamento.
use
A palavra-chave use permite trazer itens para o escopo atual, evitando escrever caminhos completos toda vez. Isso melhora a legibilidade e reduz repetição.
Exemplo sem use:
mod matematica {
pub fn soma(a: i32, b: i32) -> i32 { a + b }
}
fn main() {
println!("{}", matematica::soma(1, 2));
println!("{}", matematica::soma(3, 4));
}Com use:
mod matematica {
pub fn soma(a: i32, b: i32) -> i32 { a + b }
}
use matematica::soma;
fn main() {
println!("{}", soma(1, 2));
println!("{}", soma(3, 4));
}Você também pode usar use para importar múltiplos itens ou usar as para renomear:
use matematica::{soma, multiplicacao};
use matematica::soma as adicionar;
Além disso, use pode ser usado com caminhos relativos e absolutos. Por padrão, use assume caminhos absolutos a partir da crate root, mas você pode usar self ou super para caminhos relativos.
Caminhos absolutos e relativos
Em Rust, existem duas formas de se referir a itens em módulos: caminhos absolutos e relativos. Caminhos absolutos começam com crate (para a raiz do crate atual) ou com o nome de uma crate externa. Caminhos relativos começam com self (módulo atual), super (módulo pai) ou o nome de um item no módulo atual.
Exemplo de estrutura:
mod a {
pub fn funcao_a() {}
pub mod b {
pub fn funcao_b() {}
pub mod c {
pub fn funcao_c() {}
}
}
}
Para acessar funcao_c de dentro de main (raiz), podemos usar caminho absoluto: crate::a::b::c::funcao_c() ou caminho relativo: a::b::c::funcao_c() (a partir da raiz, ambos são equivalentes).
Dentro de um módulo, você pode usar super para se referir ao módulo pai. Por exemplo, dentro de c, para acessar funcao_a, você pode usar super::super::funcao_a (subindo dois níveis).
Exemplo prático:
mod a {
pub fn funcao_a() { println!("a"); }
pub mod b {
pub fn funcao_b() { println!("b"); }
pub mod c {
pub fn funcao_c() {
// Caminho absoluto
crate::a::funcao_a();
// Caminho relativo
super::super::funcao_a();
super::funcao_b();
}
}
}
}
fn main() {
// Caminhos a partir da raiz
a::b::c::funcao_c();
// Caminho absoluto
crate::a::b::c::funcao_c();
}Entender a diferença entre caminhos absolutos e relativos é importante para escrever código que seja robusto a refatorações. Caminhos absolutos são mais explícitos e menos propensos a quebrar se a hierarquia mudar, mas são mais longos. Caminhos relativos são mais curtos, mas podem quebrar se você mover o módulo.
Boas práticas
Ao trabalhar com módulos, siga estas boas práticas:
- Use módulos para agrupar funcionalidades relacionadas, não para criar hierarquias profundas desnecessárias.
- Exponha apenas o necessário com
pub; mantenha detalhes internos privados. - Prefira caminhos absolutos (
crate::...) em código de nível superior para evitar ambiguidades. - Use
usepara encurtar caminhos repetidos, mas evite importar itens que você não usa. - Organize módulos em arquivos separados para projetos maiores, seguindo a convenção de diretórios.
Referências
- The Rust Book - Managing Growing Projects
- Rust Reference - Modules
- Rust by Example - Modules
- The Rust Book - Paths for Referring to an Item
- The Rust Book - Bringing Paths into Scope
- Rust Reference - Use Declarations
Exercícios
Crie um módulo chamado
geometriaque contenha uma função públicaarea_retangulo(largura: f64, altura: f64) -> f64e uma função privadadobro(x: f64) -> f64. Nomain, chame a função pública e tente chamar a privada (comente a chamada que causa erro).✓ Resposta:mod geometria { pub fn area_retangulo(largura: f64, altura: f64) -> f64 { largura * altura } fn dobro(x: f64) -> f64 { x * 2.0 } } fn main() { println!("Área: {}", geometria::area_retangulo(3.0, 4.0)); // println!("{}", geometria::dobro(2.0)); // Erro: função privada }Defina um módulo
saudacaocom uma struct públicaPessoaque tenha um campo públiconomee um campo privadoidade. Implemente um método públiconew(nome: String, idade: u8) -> Pessoae um método públicoidade(&self) -> u8. Nomain, crie uma instância e acesse o nome e a idade via método.✓ Resposta:mod saudacao { pub struct Pessoa { pub nome: String, idade: u8, } impl Pessoa { pub fn new(nome: String, idade: u8) -> Pessoa { Pessoa { nome, idade } } pub fn idade(&self) -> u8 { self.idade } } } fn main() { let p = saudacao::Pessoa::new("João".to_string(), 25); println!("Nome: {}", p.nome); println!("Idade: {}", p.idade()); }Usando
use, importe duas funções de um módulooperacoes(por exemplo,somaemultiplicacao) e chame-as diretamente nomain. Depois, importe uma delas com um alias usandoas.✓ Resposta:mod operacoes { pub fn soma(a: i32, b: i32) -> i32 { a + b } pub fn multiplicacao(a: i32, b: i32) -> i32 { a * b } } use operacoes::{soma, multiplicacao}; use operacoes::soma as adicionar; fn main() { println!("{}", soma(2, 3)); println!("{}", multiplicacao(2, 3)); println!("{}", adicionar(4, 5)); }Crie uma hierarquia de módulos:
acontémb, que contémc. Dentro dec, crie uma função que chame uma função definida emausando um caminho absoluto e outra usando um caminho relativo comsuper.✓ Resposta:mod a { pub fn funcao_a() { println!("a"); } pub mod b { pub mod c { pub fn funcao_c() { // Caminho absoluto crate::a::funcao_a(); // Caminho relativo super::super::funcao_a(); } } } } fn main() { a::b::c::funcao_c(); }Escreva um programa que declare um módulo inline
configcom uma constante públicaVERSAO: &str = "1.0.0"e uma função públicaexibir()que imprime a versão. Nomain, importe a constante e a função comusee chame-as.✓ Resposta:mod config { pub const VERSAO: &str = "1.0.0"; pub fn exibir() { println!("Versão: {}", VERSAO); } } use config::{VERSAO, exibir}; fn main() { println!("Constante: {}", VERSAO); exibir(); }