Caminhos na Árvore de Módulos do Rust

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo a Caminhos para Referenciar um Item na Árvore de Módulos. Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, aprendemos que caminhos são usados em Rust para referenciar itens na árvore de módulos, e podem tomar a forma de caminhos absolutos ou caminhos relativos.

Caminhos para Referenciar um Item na Árvore de Módulos

Para mostrar ao Rust onde encontrar um item em uma árvore de módulos, usamos um caminho da mesma forma que usamos um caminho ao navegar em um sistema de arquivos. Para chamar uma função, precisamos saber seu caminho.

Um caminho pode ter duas formas:

  • Um caminho absoluto é o caminho completo começando da raiz da crate; para código de uma crate externa, o caminho absoluto começa com o nome da crate, e para código da crate atual, ele começa com o literal crate.
  • Um caminho relativo começa do módulo atual e usa self, super ou um identificador no módulo atual.

Tanto os caminhos absolutos quanto os relativos são seguidos por um ou mais identificadores separados por dois pontos (::).

Voltando à Listagem 7-1, digamos que queremos chamar a função add_to_waitlist. Isso é o mesmo que perguntar: qual é o caminho da função add_to_waitlist? A Listagem 7-3 contém a Listagem 7-1 com alguns dos módulos e funções removidos.

Mostraremos duas maneiras de chamar a função add_to_waitlist de uma nova função, eat_at_restaurant, definida na raiz da crate. Esses caminhos estão corretos, mas ainda há outro problema que impedirá que este exemplo seja compilado como está. Explicaremos o porquê em breve.

A função eat_at_restaurant faz parte da API pública da nossa biblioteca crate, então a marcamos com a palavra-chave pub. Em "Expondo Caminhos com a Palavra-chave pub", entraremos em mais detalhes sobre pub.

Nome do arquivo: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Listagem 7-3: Chamando a função add_to_waitlist usando caminhos absolutos e relativos

A primeira vez que chamamos a função add_to_waitlist em eat_at_restaurant, usamos um caminho absoluto. A função add_to_waitlist é definida na mesma crate que eat_at_restaurant, o que significa que podemos usar a palavra-chave crate para iniciar um caminho absoluto. Em seguida, incluímos cada um dos módulos sucessivos até chegarmos a add_to_waitlist. Você pode imaginar um sistema de arquivos com a mesma estrutura: especificaríamos o caminho /front_of_house/hosting/add_to_waitlist para executar o programa add_to_waitlist; usar o nome da crate para começar da raiz da crate é como usar / para começar da raiz do sistema de arquivos em seu shell.

A segunda vez que chamamos add_to_waitlist em eat_at_restaurant, usamos um caminho relativo. O caminho começa com front_of_house, o nome do módulo definido no mesmo nível da árvore de módulos que eat_at_restaurant. Aqui, o equivalente do sistema de arquivos seria usar o caminho front_of_house/hosting/add_to_waitlist. Começar com um nome de módulo significa que o caminho é relativo.

Escolher entre usar um caminho relativo ou absoluto é uma decisão que você tomará com base no seu projeto, e depende se é mais provável que você mova o código de definição do item separadamente ou junto com o código que usa o item. Por exemplo, se movêssemos o módulo front_of_house e a função eat_at_restaurant para um módulo chamado customer_experience, precisaríamos atualizar o caminho absoluto para add_to_waitlist, mas o caminho relativo ainda seria válido. No entanto, se movêssemos a função eat_at_restaurant separadamente para um módulo chamado dining, o caminho absoluto para a chamada add_to_waitlist permaneceria o mesmo, mas o caminho relativo precisaria ser atualizado. Nossa preferência em geral é especificar caminhos absolutos porque é mais provável que queiramos mover definições de código e chamadas de itens independentemente umas das outras.

Vamos tentar compilar a Listagem 7-3 e descobrir por que ela ainda não compilará! Os erros que obtemos são mostrados na Listagem 7-4.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

Listagem 7-4: Erros do compilador ao construir o código na Listagem 7-3

As mensagens de erro dizem que o módulo hosting é privado. Em outras palavras, temos os caminhos corretos para o módulo hosting e a função add_to_waitlist, mas o Rust não nos permitirá usá-los porque não tem acesso às seções privadas. Em Rust, todos os itens (funções, métodos, structs, enums, módulos e constantes) são privados para os módulos pai por padrão. Se você quiser tornar um item como uma função ou struct privado, você o coloca em um módulo.

Itens em um módulo pai não podem usar os itens privados dentro dos módulos filhos, mas itens em módulos filhos podem usar os itens em seus módulos ancestrais. Isso ocorre porque os módulos filhos envolvem e ocultam seus detalhes de implementação, mas os módulos filhos podem ver o contexto em que são definidos. Para continuar com nossa metáfora, pense nas regras de privacidade como sendo como os bastidores de um restaurante: o que acontece lá é privado para os clientes do restaurante, mas os gerentes de escritório podem ver e fazer tudo no restaurante que operam.

Rust escolheu que o sistema de módulos funcionasse dessa forma para que ocultar detalhes internos de implementação seja o padrão. Dessa forma, você sabe quais partes do código interno pode alterar sem quebrar o código externo. No entanto, o Rust oferece a opção de expor partes internas do código dos módulos filhos para os módulos ancestrais externos usando a palavra-chave pub para tornar um item público.

Expondo Caminhos com a Palavra-chave pub

Vamos voltar ao erro na Listagem 7-4 que nos informou que o módulo hosting é privado. Queremos que a função eat_at_restaurant no módulo pai tenha acesso à função add_to_waitlist no módulo filho, então marcamos o módulo hosting com a palavra-chave pub, conforme mostrado na Listagem 7-5.

Nome do arquivo: src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

--snip--

Listagem 7-5: Declarando o módulo hosting como pub para usá-lo de eat_at_restaurant

Infelizmente, o código na Listagem 7-5 ainda resulta em erros do compilador, conforme mostrado na Listagem 7-6.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

Listagem 7-6: Erros do compilador ao construir o código na Listagem 7-5

O que aconteceu? Adicionar a palavra-chave pub na frente de mod hosting torna o módulo público. Com essa alteração, se pudermos acessar front_of_house, podemos acessar hosting. Mas o conteúdo de hosting ainda é privado; tornar o módulo público não torna seu conteúdo público. A palavra-chave pub em um módulo só permite que o código em seus módulos ancestrais se refira a ele, não acesse seu código interno. Como os módulos são contêineres, não há muito que possamos fazer apenas tornando o módulo público; precisamos ir mais longe e escolher tornar um ou mais dos itens dentro do módulo público também.

Os erros na Listagem 7-6 dizem que a função add_to_waitlist é privada. As regras de privacidade se aplicam a structs, enums, funções e métodos, bem como a módulos.

Vamos também tornar a função add_to_waitlist pública adicionando a palavra-chave pub antes de sua definição, como na Listagem 7-7.

Nome do arquivo: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

--snip--

Listagem 7-7: Adicionar a palavra-chave pub a mod hosting e fn add_to_waitlist nos permite chamar a função de eat_at_restaurant.

Agora o código compilará! Para ver por que adicionar a palavra-chave pub nos permite usar esses caminhos em add_to_waitlist com relação às regras de privacidade, vamos analisar os caminhos absolutos e relativos.

No caminho absoluto, começamos com crate, a raiz da árvore de módulos da nossa crate. O módulo front_of_house é definido na raiz da crate. Embora front_of_house não seja público, como a função eat_at_restaurant é definida no mesmo módulo que front_of_house (ou seja, eat_at_restaurant e front_of_house são irmãos), podemos nos referir a front_of_house de eat_at_restaurant. Em seguida, está o módulo hosting marcado com pub. Podemos acessar o módulo pai de hosting, então podemos acessar hosting. Finalmente, a função add_to_waitlist é marcada com pub e podemos acessar seu módulo pai, então essa chamada de função funciona!

No caminho relativo, a lógica é a mesma do caminho absoluto, exceto pela primeira etapa: em vez de começar da raiz da crate, o caminho começa de front_of_house. O módulo front_of_house é definido dentro do mesmo módulo que eat_at_restaurant, então o caminho relativo começando do módulo em que eat_at_restaurant é definido funciona. Então, como hosting e add_to_waitlist são marcados com pub, o restante do caminho funciona, e essa chamada de função é válida!

Se você planeja compartilhar sua biblioteca crate para que outros projetos possam usar seu código, sua API pública é seu contrato com os usuários da sua crate que determina como eles podem interagir com seu código. Existem muitas considerações em torno do gerenciamento de alterações em sua API pública para facilitar a dependência de sua crate pelas pessoas. Essas considerações estão além do escopo deste livro; se você estiver interessado neste tópico, consulte as Diretrizes da API Rust em https://rust-lang.github.io/api-guidelines.

Melhores Práticas para Pacotes com um Binário e uma Biblioteca

Mencionamos que um pacote pode conter tanto uma raiz de crate binária src/main.rs quanto uma raiz de crate de biblioteca src/lib.rs, e ambas as crates terão o nome do pacote por padrão. Normalmente, pacotes com esse padrão de conter tanto uma biblioteca quanto uma crate binária terão código suficiente na crate binária para iniciar um executável que chama código com a crate de biblioteca. Isso permite que outros projetos se beneficiem da maior parte da funcionalidade que o pacote fornece, porque o código da crate de biblioteca pode ser compartilhado.

A árvore de módulos deve ser definida em src/lib.rs. Em seguida, quaisquer itens públicos podem ser usados na crate binária, começando os caminhos com o nome do pacote. A crate binária se torna um usuário da crate de biblioteca, assim como uma crate completamente externa usaria a crate de biblioteca: ela só pode usar a API pública. Isso ajuda você a projetar uma boa API; você não é apenas o autor, você também é um cliente!

No Capítulo 12, demonstraremos essa prática organizacional com um programa de linha de comando que conterá tanto uma crate binária quanto uma crate de biblioteca.

Iniciando Caminhos Relativos com super

Podemos construir caminhos relativos que começam no módulo pai, em vez do módulo atual ou da raiz da crate, usando super no início do caminho. Isso é como iniciar um caminho do sistema de arquivos com a sintaxe ... Usar super nos permite referenciar um item que sabemos que está no módulo pai, o que pode facilitar a reorganização da árvore de módulos quando o módulo está intimamente relacionado ao pai, mas o pai pode ser movido para outro lugar na árvore de módulos algum dia.

Considere o código na Listagem 7-8 que modela a situação em que um chef corrige um pedido incorreto e o leva pessoalmente ao cliente. A função fix_incorrect_order definida no módulo back_of_house chama a função deliver_order definida no módulo pai, especificando o caminho para deliver_order, começando com super.

Nome do arquivo: src/lib.rs

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Listagem 7-8: Chamando uma função usando um caminho relativo começando com super

A função fix_incorrect_order está no módulo back_of_house, então podemos usar super para ir para o módulo pai de back_of_house, que neste caso é crate, a raiz. De lá, procuramos por deliver_order e o encontramos. Sucesso! Achamos que o módulo back_of_house e a função deliver_order provavelmente permanecerão na mesma relação um com o outro e serão movidos juntos caso decidamos reorganizar a árvore de módulos da crate. Portanto, usamos super para que tenhamos menos lugares para atualizar o código no futuro, caso este código seja movido para um módulo diferente.

Tornando Structs e Enums Públicos

Também podemos usar pub para designar structs e enums como públicos, mas há alguns detalhes extras no uso de pub com structs e enums. Se usarmos pub antes de uma definição de struct, tornamos a struct pública, mas os campos da struct ainda serão privados. Podemos tornar cada campo público ou não, caso a caso. Na Listagem 7-9, definimos uma struct back_of_house::Breakfast pública com um campo toast público, mas um campo seasonal_fruit privado. Isso modela o caso em um restaurante onde o cliente pode escolher o tipo de pão que acompanha a refeição, mas o chef decide qual fruta acompanha a refeição com base no que está na estação e em estoque. A fruta disponível muda rapidamente, então os clientes não podem escolher a fruta ou mesmo ver qual fruta receberão.

Nome do arquivo: src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not
    // allowed to see or modify the seasonal fruit that comes
    // with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}

Listagem 7-9: Uma struct com alguns campos públicos e alguns campos privados

Como o campo toast na struct back_of_house::Breakfast é público, em eat_at_restaurant podemos escrever e ler no campo toast usando a notação de ponto. Observe que não podemos usar o campo seasonal_fruit em eat_at_restaurant, porque seasonal_fruit é privado. Tente descomentar a linha que modifica o valor do campo seasonal_fruit para ver qual erro você obtém!

Além disso, observe que, como back_of_house::Breakfast tem um campo privado, a struct precisa fornecer uma função associada pública que constrói uma instância de Breakfast (nós a nomeamos summer aqui). Se Breakfast não tivesse tal função, não poderíamos criar uma instância de Breakfast em eat_at_restaurant porque não poderíamos definir o valor do campo seasonal_fruit privado em eat_at_restaurant.

Em contraste, se tornarmos um enum público, todas as suas variantes serão públicas. Precisamos apenas do pub antes da palavra-chave enum, conforme mostrado na Listagem 7-10.

Nome do arquivo: src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Listagem 7-10: Designar um enum como público torna todas as suas variantes públicas.

Como tornamos o enum Appetizer público, podemos usar as variantes Soup e Salad em eat_at_restaurant.

Enums não são muito úteis a menos que suas variantes sejam públicas; seria irritante ter que anotar todas as variantes de enum com pub em todos os casos, então o padrão para variantes de enum é ser público. Structs são frequentemente úteis sem que seus campos sejam públicos, então os campos de struct seguem a regra geral de que tudo é privado por padrão, a menos que seja anotado com pub.

Há mais uma situação envolvendo pub que não cobrimos, e essa é nosso último recurso do sistema de módulos: a palavra-chave use. Cobriremos use por si só primeiro, e então mostraremos como combinar pub e use.

Resumo

Parabéns! Você concluiu o laboratório Caminhos para Referenciar um Item na Árvore de Módulos. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.