Módulos e Sistema de Crates do Rust
O código Rust é organizado em três níveis aninhados que são constantemente confundidos porque compartilham vocabulário: módulos, que formam uma árvore de namespace e privacidade dentro de um crate; crates, que são a unidade real que o compilador compila; e pacotes (ou workspaces, para vários pacotes juntos), que é o que o Cargo gerencia. Confundir esses - tratar "módulo" e "crate" como sinônimos, por exemplo - é a fonte mais comum de confusão para pessoas que organizam seu primeiro projeto Rust não trivial.
Esta página é a âncora conceitual para a seção de módulos e crates. Noções Básicas de Módulos aborda mod, layout de arquivos e sintaxe use na prática; esta página explica o que cada nível realmente é para o compilador, como o modelo de privacidade do Rust difere de "público vs. privado" na maioria das outras linguagens e como o modelo mental de workspace escala essa mesma estrutura em um projeto multi-crate.
Resumo
- Módulos formam uma árvore que controla o namespace e a visibilidade dentro de um crate; crates são a unidade real de compilação do compilador; pacotes (e workspaces de pacotes) são o que o Cargo gerencia e versiona.
- Por que Importa: Bases de código grandes precisam de uma maneira de ocultar detalhes de implementação, agrupar código relacionado e dividir a compilação em unidades independentemente compiláveis - e o Rust dá a cada uma dessas três necessidades um mecanismo distinto e construído para fins específicos em vez de um conceito sobrecarregado.
- Conceitos Chave: árvore de módulos, privacidade (
pub,pub(crate),pub(super)), raiz do crate, pacote vs. crate, workspace. - Quando Usar: Módulos para organizar e encapsular código dentro de um crate; um novo limite de crate quando o código precisa de compilação independente, versionamento ou uma superfície de API
dyn-free; um workspace quando vários crates no mesmo repositório devem compartilhar um lockfile e versões de dependência. - Limitações / Trade-offs: Mais crates significam limites mais limpos, mas mais superfícies de API inter-crate para projetar e versionar; módulos profundamente aninhados melhoram a organização, mas adicionam verbosidade de caminho que
useapenas parcialmente compensa. - Tópicos Relacionados: propriedade e empréstimo (a privacidade protege invariantes da mesma forma que a propriedade protege o aliasing), objetos de trait e design de API pública, resolução de dependências do Cargo.
Fundamentos
Todo crate tem exatamente uma árvore de módulos, enraizada em sua raiz de crate - src/lib.rs para um crate de biblioteca, src/main.rs para um binário. Todos os outros módulos no crate são nós nessa árvore, declarados com mod nome; (carregando nome.rs ou nome/mod.rs) ou mod nome { ... } (declarado inline). A árvore existe para fazer duas coisas: dar a cada item um caminho totalmente qualificado (crate::api::routes::health) e atuar como o limite no qual a privacidade é aplicada.
A privacidade no Rust é mais rigorosa, por padrão, do que na maioria das linguagens que você pode ter usado antes: cada item - funções, structs, campos de struct, variantes de enum, blocos impl - é privado, a menos que explicitamente marcado de outra forma, e "privado" significa visível apenas dentro do módulo que o define e dos descendentes desse módulo. Este é o padrão oposto de linguagens onde "público, a menos que marcado como privado" é a norma, e isso significa que um novo módulo Rust, pronto para uso, é um forte limite de encapsulamento em vez de um aberto.
pub é o marcador mais permissivo, expondo um item a qualquer coisa que possa ver o caminho do módulo que leva a ele (incluindo, em última análise, outros crates, se o caminho for reexportado publicamente). Marcadores mais restritos existem para necessidades mais restritas: pub(crate) expõe um item em qualquer lugar dentro do crate atual, mas em nenhum lugar fora dele - a escolha idiomática para "API interna compartilhada, não faz parte do nosso contrato público". pub(super) expõe um item apenas ao módulo pai. pub(in some::path) restringe a visibilidade a uma subárvore arbitrária e nomeada. Esse sistema graduado é o que permite que um crate tenha uma estrutura interna rica - módulos auxiliares, utilitários internos, detalhes de implementação - sem que nada disso vaze acidentalmente para a API pública real do crate.
Mecânicas e Interações
Ajuda pensar nos três níveis como respondendo a três perguntas diferentes:
módulo -> "O que pode ver este item?" (namespace + privacidade, dentro de um crate)
crate -> "O que o compilador compila como uma unidade?" (uma raiz, uma árvore de módulos)
pacote -> "O que o Cargo constrói, versiona e publica?" (um Cargo.toml, um ou mais crates)
Um crate é a coisa real que rustc compila: uma raiz de crate, sua árvore de módulos inteira, compilada junta em uma biblioteca (.rlib) ou binário. Limites de crate importam por razões reais e técnicas além da organização - código genérico e implementações de trait são monomorfizados por crate, caches de compilação incremental por crate, e a regra do órfão (que restringe a implementação de um trait estrangeiro para um tipo estrangeiro) é definida em termos de limites de crate especificamente.
Um pacote é o que um único Cargo.toml descreve: um nome, uma versão, dependências e até um crate de biblioteca mais quaisquer crates binários, de exemplo, de teste e de benchmark. Esta é a camada onde "crate" e "pacote" são mais frequentemente confundidos, porque o caso comum - um Cargo.toml, um lib.rs ou main.rs - faz com que pacote e crate pareçam a mesma coisa. Eles divergem assim que um pacote define tanto um crate de biblioteca (src/lib.rs) quanto um ou mais binários (src/bin/*.rs, ou src/main.rs junto com a biblioteca): isso é um pacote contendo múltiplos crates, compartilhando dependências, mas cada um compilado e monomorfizado separadamente.
# Um pacote, dois crates: uma biblioteca mais um binário CLI que a utiliza.
[package]
name = "toolkit"
version = "0.1.0"
[lib]
name = "toolkit" # crate 1: a biblioteca, src/lib.rs
[[bin]]
name = "toolkit-cli" # crate 2: o binário, src/bin/cli.rs
path = "src/bin/cli.rs"Um workspace é a camada mais externa: vários pacotes, listados sob uma tabela raiz [workspace], compartilhando um único Cargo.lock para que cada crate no workspace resolva a mesma versão de cada dependência compartilhada. Crucialmente, um workspace não altera os limites de privacidade ou compilação - cada pacote membro ainda compila seus próprios crate(s) independentemente, com sua própria árvore de módulos e sua própria superfície pub voltada para os outros. O que um workspace muda é puramente uma preocupação do nível do Cargo: unificação de versões de dependência, um lockfile compartilhado e a capacidade de executar cargo build --workspace ou cargo test -p some-crate em todo o conjunto a partir de uma raiz.
Considerações Avançadas e Aplicações
Escolher a profundidade do módulo versus os limites do crate versus os membros do workspace é uma decisão arquitetônica real, não apenas estilística, porque cada nível impõe uma força diferente de separação.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Mais módulos, um crate | Mais barato de introduzir; amigável a refatoração; privacidade ainda aplicada nas bordas dos módulos | Sem compilação independente, versionamento ou isolamento de dependência | Organizando a estrutura interna de uma única biblioteca ou aplicativo |
| Um novo crate (mesmo workspace) | Unidade de compilação independente; força uma superfície de API pública deliberada via pub | Mais cerimônia (próprio Cargo.toml); mudanças na API inter-crate se propagam mais longe | Código que deve ser reutilizável, testado separadamente ou compilado uma vez e reutilizado |
| Um novo membro do workspace (pacote separado) | Versionamento/publicação independente, se necessário posteriormente; lockfile compartilhado mantém as dependências alinhadas | Maior sobrecarga de configuração; requer pensar no crate como um produto, não apenas uma pasta | Divisão CLI + biblioteca, crates de plugin, código destinado a ser publicado separadamente no futuro |
O modelo de privacidade interage com os limites do crate de uma forma que surpreende pessoas vindas de linguagens onde "público" é um conceito único e plano: um item pode ser pub em toda a sua árvore de módulos e ainda ser completamente invisível fora do crate, porque nada o reexporta através da raiz do crate. Inversamente, pub use permite que um crate reexporte um item profundamente aninhado em um caminho raso e conveniente - é assim que um crate apresenta uma superfície de API pública limpa no estilo "prelude", mantendo sua estrutura de módulo interna real livre para ser reorganizada sem quebrar essa superfície pública, desde que o caminho de reexportação permaneça estável. Projetar essa superfície pública deliberadamente - decidir o que é pub, o que é pub(crate) e o que é reexportado na raiz do crate - é efetivamente projetar o contrato semver do seu crate, já que qualquer item publicamente alcançável é algo em que o código downstream agora pode depender.
A regra do órfão é o exemplo mais aguçado de limites de crate importando além da organização: você pode implementar um trait que define para qualquer tipo, e você pode implementar um trait estrangeiro para um tipo que define, mas geralmente não pode implementar um trait estrangeiro para um tipo estrangeiro - essa combinação específica é bloqueada no nível do limite do crate para evitar que dois crates definam implementações conflitantes do mesmo trait para o mesmo tipo. Nenhum nível de aninhamento de módulo muda isso; é fundamentalmente uma regra de nível de crate, que é um dos sinais mais claros de que "módulo" e "crate" não são conceitos intercambiáveis, mesmo quando a estrutura de um projeto pequeno os faz parecer assim.
Conceitos Errôneos Comuns
- "Módulos e crates são basicamente a mesma unidade organizacional" - Um módulo é um nó de namespace e privacidade dentro da árvore de um crate; um crate é a coisa real que o compilador compila como uma unidade. Um crate pode (e geralmente faz) conter muitos módulos, mas um módulo nunca é compilado independentemente.
- "Um pacote sempre significa um crate" - O caso comum (um
Cargo.toml, umlib.rs) faz parecer que isso é verdade, mas um pacote pode definir um crate de biblioteca mais vários crates binários simultaneamente, cada um compilado separadamente do mesmoCargo.toml. - "
pubem um campo de struct ou item o torna automaticamente visível fora do crate" -pubtorna um item tão visível quanto o caminho que leva a ele permite; se nada reexportar esse caminho na raiz do crate ou acima dela, o item permanece efetivamente privado para crates externos, mesmo que esteja marcado comopub. - "Tudo o que não está marcado como
pubé visível dentro do mesmo arquivo" - A privacidade é escopo ao módulo, não ao arquivo; uma struct declarada privada em um módulo é invisível mesmo para código irmão em um módulo diferente dentro do mesmo arquivo, a menos que esse código esteja aninhado dentro do mesmo módulo. - "Um workspace une seus crates membros em uma única unidade de compilação" - Não une; cada membro do workspace ainda compila como seu próprio crate independente com sua própria árvore de módulos e limite de privacidade. Um workspace apenas unifica a resolução de dependências (um
Cargo.lockcompartilhado) e a conveniência da ferramenta de compilação.
FAQs
Qual é a diferença real entre um módulo, um crate e um pacote?
Um módulo é um nó de namespace e privacidade dentro da árvore de um crate. Um crate é a unidade que o compilador realmente compila - uma raiz de crate, uma árvore de módulos. Um pacote é o que um Cargo.toml descreve, e pode conter um crate de biblioteca mais vários crates binários ao mesmo tempo.
Por que tudo é privado por padrão em Rust, em vez de público por padrão?
Porque o encapsulamento é o padrão mais seguro para manter invariantes - os campos de uma struct serem privados por padrão força o código externo a passar por funções construtoras e métodos que podem impor validade, em vez de permitir mutação direta arbitrária de qualquer lugar que possa ver o tipo.
O que `pub(crate)` realmente oferece em comparação com `pub` simples?
Ele expõe um item em todos os lugares dentro do crate atual, mas em nenhum lugar fora dele, que é a maneira idiomática de marcar algo como API interna compartilhada - utilizável em seus próprios módulos - sem torná-lo acidentalmente parte da superfície pública externa garantida por semver do crate.
Um item pode ser `pub` e ainda ser invisível para outros crates?
Sim - pub apenas torna um item tão visível quanto o caminho que leva a ele. Se esse caminho estiver dentro de um módulo privado que nada reexporta até a raiz do crate, os crates externos não terão como alcançá-lo, embora o próprio item esteja marcado como pub.
Como `mod nome;` decide qual arquivo carregar?
Ele procura por nome.rs no diretório do módulo atual, ou nome/mod.rs (ou, em layouts de arquivo mais recentes, um diretório nome/ contendo mais declarações de mod sem um mod.rs). O nome declarado do módulo e seu caminho de arquivo são conectados por essa convenção, não por algo dentro do próprio arquivo.
O `Cargo.lock` de um workspace é compartilhado entre todos os crates membros?
Sim - essa é a principal coisa que um workspace muda. Todos os pacotes membros resolvem dependências compartilhadas para as mesmas versões através de um único lockfile na raiz do workspace, o que evita desvios de versão entre crates que vivem no mesmo repositório.
Colocar dois crates em um workspace muda como eles compilam?
Não - cada membro do workspace ainda compila como seu próprio crate independente com sua própria árvore de módulos, seu próprio limite de privacidade e sua própria unidade de compilação incremental. O workspace afeta apenas a resolução de dependências e a conveniência da ferramenta de compilação/teste (como cargo test --workspace), não os limites de compilação.
Por que eu dividiria o código em um crate separado em vez de apenas um novo módulo?
Um novo crate força uma superfície de API deliberada, definida por pub, entre ele e seus consumidores, dá a ele compilação independente (útil para tempos de compilação incremental) e o torna independentemente testável, versionável e potencialmente publicável - nada disso um limite de módulo fornece por si só.
O que é `pub use` e por que os crates o usam?
Ele reexporta um item em um novo caminho, permitindo que um crate apresente uma API pública limpa e rasa (frequentemente chamada de "prelude") em sua raiz, mantendo a organização da implementação real em módulos internos mais profundos e privados por padrão. Os consumidores dependem do caminho reexportado, que permanece estável mesmo que a estrutura do módulo interno seja reorganizada.
O que é a regra do órfão e por que ela está ligada especificamente a crates?
É a restrição de que você geralmente não pode implementar um trait estrangeiro para um tipo estrangeiro - apenas para tipos ou traits que seu próprio crate define. É aplicada no limite do crate porque seu propósito é impedir que dois crates diferentes definam uma implementação conflitante do mesmo trait para o mesmo tipo, o que o aninhamento de módulos não afeta.
Um único pacote pode produzir mais de um binário compilado?
Sim - um pacote pode definir múltiplos crates binários via entradas [[bin]] ou arquivos em src/bin/, cada um compilado como um crate separado, todos compartilhando o mesmo Cargo.toml, dependências e (frequentemente) um crate de biblioteca interno do qual ambos dependem.
Como módulos auxiliares privados coexistem com uma pequena API pública no mesmo crate?
Aninhando os auxiliares dentro de módulos que são privados (ou pub(crate)) por padrão, e marcando apenas o pequeno conjunto de itens que formam a API pretendida como pub, opcionalmente reexportados via pub use na raiz do crate. A árvore de módulos pode ser arbitrariamente profunda e reorganizada livremente, desde que os caminhos da superfície pública permaneçam estáveis.
Relacionados
- Noções Básicas de Módulos -
modprático, layout de arquivos e sintaxeuse. - Visibilidade e Privacidade -
pub,pub(crate),pub(super)e padrões de encapsulamento em profundidade. - Crates e Pacotes - a distinção pacote/crate com exemplos concretos de
Cargo.toml. - Projetando uma API Pública - moldando deliberadamente a superfície
pubde um crate como um contrato semver. - Workspaces - repositórios multi-crate, dependências compartilhadas e comandos Cargo em nível de workspace.
- Workspaces do Cargo - o lado de ferramentas do Cargo da configuração do workspace.
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024).