Четкое объяснение модульной системы Rust

Перевод | Автор оригинала: Shesh

Модульная система Rust на удивление сбивает с толку и вызывает много разочарований у новичков.

В этом посте я объясню модульную систему на практических примерах, чтобы вы получили четкое представление о том, как она работает, и сразу же применили ее в своих проектах.

Поскольку модульная система Rust довольно уникальна, я прошу читателя прочитать этот пост непредвзято и не сравнивать его с тем, как модули работают на других языках.

Давайте воспользуемся этой файловой структурой для моделирования реального проекта:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs

Вот как мы можем использовать наши модули:

Этих трех примеров должно быть достаточно, чтобы объяснить, как работает модульная система Rust.

Пример 1

Начнем с первого примера - импорта config.rs в main.rs.

// main.rs
fn main() {
  println!("main");
}

// config.rs
fn print_config() {
  println!("config");
}

Первая ошибка, которую делают все, - это потому, что у нас есть такие файлы, как config.rs, health_route.rs и т.д., Мы думаем, что эти файлы являются модулями, и можем импортировать их из других файлов.

Вот что мы видим (дерево файловой системы) и то, что видит компилятор (дерево модулей):

Удивительно, но компилятор видит только модуль crate, которым является наш файл main.rs. Это связано с тем, что нам нужно явно построить дерево модулей в Rust - не существует неявного сопоставления между деревом файловой системы и деревом модулей.

Нам нужно явно построить дерево модулей в Rust, здесь нет неявного сопоставления с файловой системой

Чтобы добавить файл в дерево модулей, нам нужно объявить этот файл как подмодуль, используя ключевое слово mod. Следующее, что смущает людей, это то, что вы предполагаете, что мы объявляем файл как модуль в том же файле. Но нам нужно объявить это в другом файле! Поскольку в дереве модулей есть только main.rs, давайте объявим config.rs как подмодуль в main.rs.

Ключевое слово mod объявляет подмодуль

Ключевое слово mod имеет следующий синтаксис:

mod my_module;

Здесь компилятор ищет my_module.rs или my_module / mod.rs в том же каталоге.

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  └── my_module.rs

or

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  └─┬ my_module
    └── mod.rs

Поскольку main.rs и config.rs находятся в одном каталоге, давайте объявим модуль config следующим образом:

// main.rs
+ mod config;

fn main() {
+ config::print_config();
  println!("main");
}

// config.rs
fn print_config() {
  println!("config");
}

Мы обращаемся к функции print_config с помощью синтаксиса ::.

Вот как выглядит дерево модулей:

Мы успешно объявили модуль конфигурации! Но этого недостаточно для вызова функции print_config внутри config.rs. Почти все в Rust по умолчанию является закрытым, нам нужно сделать функцию общедоступной с помощью ключевого слова pub:

Ключевое слово pub делает информацию общедоступной

// main.rs
mod config;

fn main() {
  config::print_config();
  println!("main");
}

// config.rs
- fn print_config() {
+ pub fn print_config() {
  println!("config");
}

Теперь это работает. Мы успешно вызвали функцию, определенную в другом файле!

Пример 2

Давайте попробуем вызвать функцию print_health_route, определенную в routes / health_route.rs из main.rs.

// main.rs
mod config;

fn main() {
  config::print_config();
  println!("main");
}

// routes/health_route.rs
fn print_health_route() {
  println!("health_route");
}

Как мы обсуждали ранее, мы можем использовать ключевое слово mod только для my_module.rs или my_module / mod.rs в том же каталоге.

Итак, чтобы вызывать функции внутри routes / health_route.rs из main.rs, нам нужно сделать следующее:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
+ │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs
// main.rs
mod config;
+ mod routes;

fn main() {
+ routes::health_route::print_health_route();
  config::print_config();
  println!("main");
}

// routes/mod.rs
+ pub mod health_route;

// routes/health_route.rs
- fn print_health_route() {
+ pub fn print_health_route() {
  println!("health_route");
}

Вот как выглядит дерево модулей:

Теперь мы можем вызвать функцию, определенную в файле внутри папки.

Пример 3

Давайте попробуем позвонить из main.rs => routes / user_route.rs => models / user_model.rs

// main.rs
mod config;
mod routes;

fn main() {
  routes::health_route::print_health_route();
  config::print_config();
  println!("main");
}

// routes/user_route.rs
fn print_user_route() {
  println!("user_route");
}

// models/user_model.rs
fn print_user_model() {
  println!("user_model");
}

Мы хотим вызвать функцию print_user_model из print_user_route из main.

Давайте внесем те же изменения, что и раньше - объявим подмодули, сделаем функции общедоступными и добавим файл mod.rs.

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
+   ├── mod.rs
    └── user_model.rs
// main.rs
mod config;
mod routes;
+ mod models;

fn main() {
  routes::health_route::print_health_route();
+ routes::user_route::print_user_route();
  config::print_config();
  println!("main");
}

// routes/mod.rs
pub mod health_route;
+ pub mod user_route;

// routes/user_route.rs
- fn print_user_route() {
+ pub fn print_user_route() {
  println!("user_route");
}

// models/mod.rs
+ pub mod user_model;

// models/user_model.rs
- fn print_user_model() {
+ pub fn print_user_model() {
  println!("user_model");
}

Вот как выглядит дерево модулей:

Подождите, на самом деле мы не вызывали print_user_model из print_user_route! До сих пор мы вызывали только функции, определенные в других модулях из main.rs, как это сделать из других файлов?

Если мы посмотрим на наше дерево модулей, функция print_user_model находится в пути crate::models::user_model. Итак, чтобы использовать модуль в файлах, которые не являются main.rs, мы должны думать о пути, необходимом для достижения этого модуля в дереве модулей.

// routes/user_route.rs
pub fn print_user_route() {
+ crate::models::user_model::print_user_model();
  println!("user_route");
}

Мы успешно вызвали функцию, определенную в файле, из файла, который не является main.rs.

супер

Полное имя становится слишком длинным, если наша файловая организация состоит из нескольких каталогов. Допустим, по какой-то причине мы хотим вызвать print_health_route из print_user_route. Они находятся по путям crate::routes::health_route и crate::routes::user_route соответственно.

Мы можем вызвать его, используя полное имя crate::routes::health_route::print_health_route(), но мы также можем использовать относительный путь super::health_route::print_health_route() ;. Обратите внимание, что мы использовали super для обозначения родительской области.

Ключевое слово super в пути к модулю относится к родительской области

pub fn print_user_route() {
  crate::routes::health_route::print_health_route();
  // can also be called using
  super::health_route::print_health_route();

  println!("user_route");
}

use

Было бы утомительно использовать полное имя или даже относительное имя в приведенных выше примерах. Чтобы сократить имена, мы можем использовать ключевое слово use для привязки пути к новому имени или псевдониму.

Ключевое слово use используется для сокращения пути к модулю

pub fn print_user_route() {
  crate::models::user_model::print_user_model();
  println!("user_route");
}

Приведенный выше код можно реорганизовать как:

use crate::models::user_model::print_user_model;

pub fn print_user_route() {
  print_user_model();
  println!("user_route");
}

Вместо использования имени print_user_model мы также можем присвоить ему псевдоним другого:

use crate::models::user_model::print_user_model as log_user_model;

pub fn print_user_route() {
  log_user_model();
  println!("user_route");
}

Внешние модули

Зависимости, добавленные в Cargo.toml, доступны глобально для всех модулей внутри проекта. Нам не нужно явно импортировать или объявлять что-либо, чтобы использовать зависимость.

Внешние зависимости глобально доступны для всех модулей внутри проекта.

Например, предположим, что мы добавили крэйт rand в наш проект. Мы можем использовать его в нашем коде напрямую как:

pub fn print_health_route() {
  let random_number: u8 = rand::random();
  println!("{}", random_number);
  println!("health_route");
}

Мы также можем использовать use для сокращения пути:

use rand::random;

pub fn print_health_route() {
  let random_number: u8 = random();
  println!("{}", random_number);
  println!("health_route");
}

Резюме

Спасибо за чтение! Не стесняйтесь подписываться на меня в Twitter, чтобы увидеть больше подобных сообщений :)