Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Модули

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

Tip

Модули в какой-то мере являются аналогами пространств имен из C++ или пакетов из Java.

Имеется несколько способов создать модуль.

Способ 1: блок mod { … }

Модуль можно объявить прямо в main.rs при помощи ключевого слова mod.

mod имя_модуля {
    содержимое
}

Далее, чтобы обратиться к функции или структуре из модуля, используется синтаксис:

модуль::имя

Рассмотрим пример: решения конфликта имён при помощи модулей.

Файл main.rs:

mod a {
    pub fn get_num() -> i32 {
        1
    }
}

mod b {
    pub fn get_num() -> i32 {
        2
    }
}

fn main() {
    println!("{}", a::get_num()); // 1
    println!("{}", b::get_num()); // 2
}

Обратите внимание, что перед объявлением функции стоит модификатор видимости pub (public). Он указывает, что функция может быть использована “внешним” кодом. По умолчанию всё содержимое модуля является приватным и может быть использовано только внутри этого модуля.

mod my_module {
    pub fn get_num() -> i32 { // видима за пределами модуля
        get_5()
    }
    fn get_5() -> i32 { // видима только внутри модуля
        5
    }
}

fn main() {
    println!("{}", my_module::get_num()); // 5
}

Как правило, блок mod { ... } используют:

  • для решения конфликта имён
  • для вынесения части кода в блок (модуль), который компилируется только при выполнении некого условия

Способ 2: другой *.rs файл

Если в той же директории, где находится файл main.rs, создать другой файл с расширением *.rs, то этот файл можно будет подключить как модуль при помощи ключевого слова mod.

mod имя_файла_без_rs;

Например, допустим, у нас имеются такие файлы:

  • main.rs:
    mod my_module;
    
    fn main() {
        println!("{}", my_module::get_num());
    }
  • my_module.rs:
    #![allow(unused)]
    fn main() {
    pub fn get_num() -> i32 {
        5
    }
    }

Скомпилируем и запустим

$ rustc main.rs
$ ./main
5

Note

Сильно упрощая, можно сказать, что при компиляции файла main.rs вместо выражения mod my_module помещается содержимое файла my_module.rs так, как если бы оно изначально находилось там и было просто заключено в блок mod my_module { ... }.

mod my_module {
    pub fn get_num() -> i32 {
        5
    }
}

fn main() {
    println!("{}", my_module::get_num());
}

Чтобы не писать полное имя функции с указанием модуля, её можно импортировать при помощи директивы use.

mod my_module;
use my_module::get_num;

fn main() {
    println!("{}", get_num());
}

Способ создания модуля в виде отдельного *.rs файла подходит для случаев, когда весь код модуля можно поместить в один файл.

Способ 3: директория и mod.rs

Если функциональность модуля хорошо гранулирована и может быть разделена на несколько подмодулей, то модуль создают не как отдельный файл, а как отдельный каталог, в котором есть корневой файл модуля — mod.rs и другие *.rs файлы, которые служат в качестве под-модулей.

Для примера рассмотрим тривиальный модуль с арифметическими операциями.

src/
├── main.rs           <- главный файл программы
└── my_math/          <- папка модуля
    └── mod.rs        <- корневой файл модуля
        ├── add.rs    <- здесь функция сложения
        └── mul.rs    <- здесь функция умножения

Файлы программы:

  • Файл my_math/mod.rs:

    pub mod add; // подключаем add.rs в модуль my_math
    pub mod mul; // подключаем mul.rs в модуль my_math
  • Файл my_math/add.rs:

    pub fn sum(a: i32, b: i32) -> i32 {
        a + b
    }
  • Файл my_math/add.rs:

    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }
  • Файл main.rs:

    mod my_math;
    use my_math::add::sum;
    use my_math::mul::multiply;
    
    fn main() {
        println!("2 + 3 = {}", sum(2, 3));
        println!("2 * 3 = {}", multiply(2, 3));
    }

Скомпилируем и запустим программу:

$ rustc main.rs
$ ./main
7

Note

Упрощая, можно сказать, что при компиляции main.rs, компилятор также соберёт все файлы модулей воедино, словно изначально это был файл:

mod my_math {
    pub mod add {
        pub fn sum(a: i32, b: i32) -> i32 {
            a + b
        }
    }
    pub mod mul {
        pub fn multiply(a: i32, b: i32) -> i32 {
            a + b
        }
    }
}
use my_math::add::sum;
use my_math::mul::multiply;

fn main() {
    println!("2 + 3 = {}", sum(2, 3));
    println!("2 * 3 = {}", multiply(2, 3));
}

Трансляция и модули

Как мы уже сказали, когда мы создаём модули в виде отдельных файлов, нам не приходится компилировать их отдельно. Мы вызываем rustc только для файла main.rs, и все модули компилируются автоматически.

Сборку бинарного исполняемого файла можно проиллюстрировать так:

Для сравнения: в C++ каждый *.cpp файл компилируется в отдельный объектный файл, и только потом все объектные файлы линкуются в исполняемый бинарный файл.

Из-за того, что компилятор Rust “склеивает” main.rs и все входящие в него модули в один большой файл, который далее компилирует целиком, этот самый main.rs и все входящие в него модули называют крэйтом (crate — ящик). Представьте, словно все файлы “свалили” в один ящик (единицу трансляции) и подали на вход компилятору.

Понятие крэйта очень важно в экосистеме Rust. Мы подробнее поговорим про крэйты, и другие составляющие программ на Rust в главе Несколько исполняемых файлов.