Перейти к содержимому

Модули в Rust

До сих пор каждый пример жил в одном файле main.rs. Для маленьких программ это нормально, но настоящие проекты делят код на логические части. Для этого в Rust есть система модулей — mod, pub и use.

Модуль — это именованный блок, который собирает связанный код вместе:

mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}

Вывод:

5
▶ Попробуй сам

Обрати внимание на две вещи. Функция вызывается как math::add(...) — имя модуля служит путём. И она отмечена pub: без этого до неё нельзя было бы добраться снаружи модуля.

Об этом стоит сказать отдельно, потому что многих это удивляет: содержимое модуля по умолчанию приватное (private). Если убрать pub из примера выше, код не скомпилируется:

mod math {
fn add(a: i32, b: i32) -> i32 { // нет `pub`
a + b
}
}
fn main() {
let sum = math::add(2, 3); // ❌ ошибка: функция `add` приватная
println!("{sum}");
}

Всё, чем должен пользоваться код снаружи модуля, нужно явно отметить pub. Так задумано: модуль может прятать свои внутренние вспомогательные функции и открывать только то, что задумано как публичный API.

Модули могут содержать другие модули. Так большие проекты выстраивают древовидную структуру:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
fn main() {
let area = shapes::circle::area(2.0);
println!("{area}");
}
▶ Попробуй сам

Каждый раз писать shapes::circle::area(...) утомительно. use вводит путь в область видимости, и к нему можно обращаться напрямую:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
use shapes::circle;
fn main() {
let area = circle::area(2.0); // короче, чем shapes::circle::area
println!("{area}");
}
▶ Попробуй сам

На самом деле ты уже пользовался use — это то же ключевое слово, что и на странице про HashMap (use std::collections::HashMap;).

В примерах выше всё лежит в одном файле — этого требует Rust Playground. В настоящем проекте на твоём компьютере у каждого модуля обычно свой файл. Проект с модулем math обычно выглядит так:

src/
├── main.rs
└── math.rs
src/main.rs
mod math; // говорит Rust: код этого модуля ищи в math.rs
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}
src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

mod math; (с точкой с запятой и без тела) говорит Rust загрузить содержимое модуля из файла math.rs в той же папке. Этим приёмом ты будешь пользоваться, как только проект перерастёт один файл.

Создай модуль convert с двумя публичными функциями: c_to_f (из Цельсия в Фаренгейт) и f_to_c (обратно). Из main выведи 100°C — это 212°F и 212°F — это 100°C.

Подсказка: формулы — c * 9.0 / 5.0 + 32.0 и (f - 32.0) * 5.0 / 9.0. Не забудь pub и вызывай функции через имя модуля, например convert::c_to_f(100.0).

Показать решение
mod convert {
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
pub fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
}
fn main() {
println!("100°C — это {}°F", convert::c_to_f(100.0));
println!("212°F — это {}°C", convert::f_to_c(212.0));
}

Вывод:

100°C — это 212°F
212°F — это 100°C
▶ Попробуй сам

Обе функции pub, поэтому main (который живёт снаружи модуля) может их вызвать. Убери один pub — и компилятор скажет, что функция приватная.

Создай вложенный модуль school::grades с публичной функцией letter(score: u32) -> char, которая возвращает 'A' для 90 и выше, 'B' для 80 и выше и 'C' в остальных случаях. Внутри grades напиши ещё и приватную вспомогательную функцию is_at_least(score: u32, min: u32) -> bool, которой пользуется letter. Введи school::grades в область видимости через use и выведи оценку для 95, 85 и 70.

Подсказка: pub нужен и внутреннему mod grades в school, и fn letter, а у is_at_least его быть не должно. Код внутри того же модуля всегда может вызывать приватные функции.

Показать решение
mod school {
pub mod grades {
pub fn letter(score: u32) -> char {
if is_at_least(score, 90) {
'A'
} else if is_at_least(score, 80) {
'B'
} else {
'C'
}
}
// приватная: вызывать её может только код внутри `grades`
fn is_at_least(score: u32, min: u32) -> bool {
score >= min
}
}
}
use school::grades;
fn main() {
for score in [95, 85, 70] {
println!("{score} -> {}", grades::letter(score));
}
}

Вывод:

95 -> A
85 -> B
70 -> C
▶ Попробуй сам

letter может вызвать is_at_least, потому что они живут в одном модуле. А из main вызов grades::is_at_least(95, 90) не сработает: компилятор скажет «function is_at_least is private» (функция is_at_least приватная). В этом и смысл: модуль показывает маленький публичный API и держит свои помощники при себе.

  • mod name { ... } собирает связанный код в именованный модуль.
  • Всё внутри модуля по умолчанию приватное — добавь pub, чтобы открыть это снаружи.
  • use вводит путь в область видимости, чтобы не писать его целиком каждый раз.
  • В проекте из нескольких файлов mod name; (без тела) загружает код модуля из файла с тем же именем.
  • Код внутри модуля всегда может пользоваться его приватными элементами, так что держи помощники приватными и открывай только то, что нужно вызывающему коду.

Проверь себя

1. Элементы внутри mod по умолчанию публичные или приватные?

2. Как сказать Rust загрузить код модуля из math.rs, а не писать его прямо на месте?

Результат: 0 / 2