Rust - Модули и структура проекта

Изучение структуры проекта Rust, ящиков, модулей, видимости и что, черт возьми, такое прелюдия !?

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

Если у вас еще нет, иди и установи Rust и убедись, что сможешь создать новый проект -

$ cargo new hello_rust

Напоминаем, что при этом будет создано новое двоичное приложение, поэтому вы можете запустить его на терминале с помощью -

$ cargo run

Вы должны увидеть, что Cargo сначала скомпилирован, а затем запустится ваше приложение, при этом на консоль будет записано следующее:

$ cargo run
“Hello, World!”

Большой! В оставшейся части этой статьи я собираюсь обсудить -

  • Структура проекта Rust по умолчанию
  • Файл main.rs
  • модули Rust (на основе файлов)
  • Модули Rust и видимость
  • Модули Rust (по папкам)
  • Что такое прелюдия?

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

Проект Rust по умолчанию

Консольное приложение Rust по умолчанию довольно простое, но структура папок преднамеренная и не должна изменяться -

hello_rust
  - src
    - main.rs
  - .gitignore
  - Cargo.toml

Обратите внимание, что вы можете использовать команду cargo check для проверки структуры папок и файла Cargo.toml в любое время. Если вы сделаете ошибку (в данном случае я переименовал src в src1), Cargo услужливо сообщит вам, что вам нужно сделать -

error: failed to parse manifest at `/Users/gian/_working/scratch/hello_rust/Cargo.toml`
Caused by:
 no targets specified in the manifest
 either src/lib.rs, src/main.rs, a [lib] section, or [[bin]] section must be present

В нашем случае у нас должен быть src/main.rs, поскольку мы создали двоичное приложение. Если бы мы создали новую библиотеку (передав --lib команде cargo new), вместо этого Cargo создал бы для нас src/lib.rs.

Файл Cargo.lock создается автоматически и не подлежит редактированию. Поскольку Cargo по умолчанию инициализирует для вас репозиторий Git, он также включает. gitignore с одной записью -

/target

Папка target автоматически создается cargo build и содержит артефакты сборки в папке debug или release (в зависимости от конфигурации сборки, помните, что по умолчанию - отладка).

Если вы выполняете кросс-компиляцию на другую платформу, вы увидите дополнительный уровень, определяющий целевую платформу, а затем конфигурацию сборки.

Наконец, есть файл main.rs, который является точкой входа для нашего приложения. Давайте внимательно посмотрим на его содержимое.

Файл main.rs

Файл main.rs по умолчанию довольно прост -

fn main() {
  println!("Hello, world!");
}

У нас есть функция main(), основная точка входа для нашего приложения, которая просто печатает «Helo, World!» на стандартный вывод.

Возможно, вы заметили ! в println! - это означает, что функция println является макросом Rust (расширенная функция синтаксиса Rust), которую вы можете игнорировать по большей части, кроме как помнить, что это не обычная функция.

Хотя теперь вы можете с радостью написать весь свой код на Rust в файле main.rs, это, как правило, не идеально;) Вот тут-то и пригодятся модули!

Модули

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

struct MyStruct {}
fn main() {
  let _ms = MyStruct {};    <-- Note the '_'
}

Это почти самая простая программа, которую вы могли бы написать, но она прекрасно подходит для иллюстрации модулей Rust. Обратите внимание на префикс _ перед именем переменной - Rust не любит неиспользуемые переменные (это правильно!), Но с помощью префикса _ мы сообщаем компилятору, что это было намеренно, и это предотвратит выдачу компилятором предупреждения. Это не хороший пример того, когда использовать эту функцию («игнорируемое» сопоставление с шаблоном), но в других случаях у нее есть законное применение.

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

hello_rust
  - src
    - main.rs
    - my_struct.rs

Обратите внимание, что мы должны добавить файл под папку src/, чтобы компилятор нашел его. Хотя имя файла на самом деле не имеет значения, в Rust идиоматично использовать snake_case, поэтому мы и будем этим заниматься.

Переместите объявление структуры из main.rs и поместите его в my_struct.rs -

// Contents of my_struct.rs
struct MyStruct {}

Попробуйте построить проект -

$ cargo build

Если вы удалили объявление структуры из main.rs, вы увидите такую ​​ошибку:

Compiling hello_rust v0.1.0 (/scratch/hello_rust)
error[E0422]: cannot find struct, variant or union type `MyStruct` in this scope
 → src/main.rs:2:15
  |
2 | let _ms = MyStruct {};
  |           ^^^^^^^^ not found in this scope
error: aborting due to previous error
For more information about this error, try `rustc — explain E0422`. error: could not compile `hello_rust`

Rust сообщает нам, что больше не может найти определение нашей структуры. Здесь на помощь приходят модули - в отличие от некоторых других языков, вы должны явно включать код в свое приложение. Rust не просто найдет файл и скомпилирует / включит его за вас.

Чтобы включить объявление структуры, нам нужно обновить наш main.rs, чтобы добавить ссылку на модуль, например:

mod my_struct;
fn main() {
  let _ms = MyStruct {};
}

В Rust все файлы и папки являются модулями. Чтобы использовать код в модуле, вам необходимо сначала импортировать его с синтаксисом mod. По сути, это вставка кода из модуля в точку, где находится оператор mod my_struct;. Подробнее о модулях папок чуть позже.

Попробуйте построить еще раз. Подожди, что это ?! По-прежнему не работает ... хм. Давайте посмотрим на сообщение об ошибке -

Compiling hello_rust v0.1.0 (/scratch/hello_rust)
error[E0422]: cannot find struct, variant or union type `MyStruct` in this scope
 → src/main.rs:4:15
  |
4 | let _ms = MyStruct {};
  |           ^^^^^^^^ not found in this scope
  |
help: consider importing this struct
  |
1 | use crate::my_struct::MyStruct;
  |

Хотя ошибка та же, теперь есть полезный совет о добавлении -

use crate::my_struct::MyStruct;

Давайте попробуем - измените main.rs так, чтобы он выглядел так (но не собирать! Спойлер, у нас есть еще одна проблема, о которой я скоро расскажу) -

mod my_struct;
use crate::my_struct::MyStruct;
fn main() {
  let _ms = MyStruct {};
}

Здесь есть кое-что, что нужно распаковать. Когда вы импортируете модуль с помощью оператора mod, Rust автоматически создает для него пространство имен модуля (во избежание конфликтов), и поэтому мы не можем напрямую получить доступ к нашему типу структуры. Пространство имен модуля автоматически берется из имени файла (так как модуль в данном случае является файлом), следовательно, my_struct::MyStruct; часть оператора use - оно происходит непосредственно из имени файла my_struct.rs (без расширение файла).

Причина crate:: части выражения use заключается в том, что все проекты Rust являются ящиками. Как вы теперь видели, проекты Rust могут состоять из нескольких файлов (которые являются модулями), которые могут быть вложены в папки (которые также являются модулями). Чтобы получить доступ к корню этого дерева модулей, вы всегда можете использовать префикс crate::.

Итак, снова посмотрев на наш main.rs, у нас есть -

mod my_struct;                  <-- Import the module code, placing
                                    it into the 'my_struct'
                                    namespace
use crate::my_struct::MyStruct; <-- Map the fully qualified (from 
                                    the crate root) struct 
                                    declaration to just 'MyStruct'
fn main() {
  let _ms = MyStruct {};        <-- Yay, we found it! .. or did we?
}

Если это кажется запутанным (и я должен сказать, что нашел это немного запутанным из-за C #), просто запомните это -

  • Вы должны использовать mod для включения модуля (файла или папки) в ваше приложение.
  • Ключевое слово use позволяет сопоставить полное имя типа только с именем типа (вы даже можете переименовывать типы, но это касается другого сообщения).

Модули - видимость

Если бы вы были нетерпеливы (продолжайте, признайте это!), То вы бы попытались построить предыдущую версию main.rs и получили бы еще одну ошибку -

Compiling hello_rust v0.1.0 (/scratch/hello_rust)
error[E0603]: struct `MyStruct` is private
 → src/main.rs:2:23
  |
2 | use crate::my_struct::MyStruct;
  |                       ^^^^^^^^ private struct
  |

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

Видимость в Rust немного отличается от таких языков, как C #, но стоит запомнить пару правил:

  • Все внутри модуля (т. Е. Файл или подпапка в /src папке) может обращаться к всему остальному в этом модуле.
  • Все, что вне модуля, может только получить доступ к открытым членам этого модуля.

Сначала это может показаться странным, но у него есть очень привлекательные побочные эффекты - частные функции внутри модуля по-прежнему доступны для тестов в этом модуле (идиоматический Rust сохраняет модульные тесты внутри модуля). Во-вторых, каждый модуль вынужден объявлять открытый интерфейс, определяя, какие члены доступны вне модуля.

Чтобы сделать член модуля общедоступным, мы должны добавить ключевое слово pub. Давайте еще раз вернемся к нашему файлу my_struct.rs и заменим его содержимое на -

pub struct MyStruct {}         <-- Add the 'pub' keyword

Вот и все! Теперь вы можете успешно создать наше удивительно сложное приложение :) Обратите внимание, что вы можете помещать pub в большинство объявлений, включая структуры, поля структур, функции (связанные и другие), константы и т. Д.

Модули - Папки

Теперь предположим, что наша MyStruct структура выходит из-под контроля, и мы хотим разбить ее на несколько файлов. Мы хотим собрать их в папку, чтобы, конечно, все было в порядке и порядке.

Как упоминалось выше, Rust обрабатывает файлы и папки одинаково (как модули) с одним ключевым отличием.

Давайте начнем с создания папки с именем foo/, потому что мы поняли, что наш MyStruct действительно является частью функции foo нашего приложения. Затем переместите файл my_struct.rs в /src/foo. Т.е. новая структура папок должна выглядеть так -

- src/
  - main.rs
  - foo/
    - my_struct.rs

Теперь отредактируйте main.rs, чтобы включить наш новый модуль foo вместо my_struct -

mod foo;                   <-- Change the module to match the folder
use crate::foo::MyStruct;  <-- Update the namespace to 'foo'
fn main() {
  let _ms = MyStruct {};
}

Мы можем построить это сейчас (cargo build), но получим ошибку. Как всегда, сообщения об ошибках Rust поучительны -

Compiling hello_rust v0.1.0 (/scratch/hello_rust)
error[E0583]: file not found for module `foo`
 → src/main.rs:1:1
  |
1 | mod foo;
  | ^^^^^^^^
  |
  = help: to create the module `foo`, create file “src/foo.rs” or “src/foo/mod.rs”

При попытке импортировать модуль, определенный как папка, мы используем имя папки (как мы делали ранее для модуля на основе файлов), но Rust ожидает, что в папке существует файл с именем mod.rs.

В этом случае мы можем просто переименовать наш my_struct.rs в mod.rs и вуаля! Наше приложение снова строится.

Для полноты картины добавим в папку foo/ файл с другим определением структуры (образно названным Другой) -

// Contents of src/foo/another.rs
pub struct Another {}   <-- We're going to expose this as public
                            from the 'foo' module so that we can
                            use it in main.rs

Мы импортируем наш новый модуль в файл mod.rs -

// Contents of src/foo/mod.rs
pub mod another;        <-- Add the module import for 'another'
                            Note the use of 'pub' to expose the 
                            module 'another' as public from the 
                            module 'foo'
pub struct MyStruct {}

И, наконец, попробуйте использовать нашу новую структуру Another в main.rs

mod foo;
use crate::foo::MyStruct;
use crate::foo::another::Another; <-- Note that 'another' is a
                                      module within 'foo'
fn main() {
  let _ms = MyStruct {};
  let _a = Another {};            <-- Using prefix '_' as before
}

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

Прелюдии

Вернемся к нашему файлу mod.rs в папке foo/. Измените содержимое на следующее -

mod another;              <-- Remove the 'pub' modifier
pub use another::Another; <-- Add a use'ing to map Another directly
                              into 'foo' and make it public
pub struct MyStruct {}

Здесь мы больше не хотим, чтобы другой модуль был общедоступным, поэтому мы удаляем ключевое слово pub. Затем оператор use отобразит полный тип Another в пространство имен foo (потому что мы находимся в модуле foo).

Наконец, давайте обновим наш main.rs -

mod foo;
use crate::foo::{MyStruct,Another};
fn main() {
  let _ms = MyStruct {};
  let _a = Another {};
}

Намного лучше! Обратите внимание: поскольку мы сопоставили имя типа Another с модулем foo, мы можем использовать расширенный синтаксис use для одновременного импорта нескольких имен.

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

Хорошо, это здорово ... ну что за хрень прелюдия, я слышал, вы спросите! Что ж, прелюдия - это просто шаблон для идиоматического обнародования всех типов, которые вы хотите сделать общедоступными. Не все ящики определяют прелюдию (хотя многие так и делают), и она не всегда нужна, но давайте все равно определим ее для нашего небольшого проекта hello_rust.

Вернемся к нашему main.rs мы идем -

mod foo;
mod prelude {                             <-- Create module inline
  pub use crate::foo::{MyStruct,Another}; <-- Note the 'pub' here!
}
use crate::prelude::*;                    <-- Make the types exposed
                                              in the prelude
                                              available
fn main() {
  let _ms = MyStruct {};
  let _a = Another {};
}

Мы определяем прелюдию как еще один модуль (используя mod), только на этот раз мы указываем модуль напрямую, вместо того, чтобы позволить Rust искать соответствующий файл или папку.

Теперь мы также можем использовать модуль prelude, как и любой другой, например, в файле mod.rs -

mod another;
pub use another::Another;
use crate::prelude::*;
pub struct MyStruct {}

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

Он также выделяет пару других интересных частей использования модуля -

  • Вы можете импортировать все общедоступные имена из модуля с подстановочным знаком ::*
  • Вы можете получить доступ к корню дерева модулей (то есть к основному модулю в данном случае), используя crate::, и вы можете сделать это из любого места в вашем приложении.

Резюме

Модульная система в Rust определенно была одним из наиболее загадочных аспектов языка. Исходя из фона C ++ / C # в сочетании с правилами видимости модуля (и прелюдиями), это было совершенно запутанным! Но как только вы осознаете, что такое модуль (файл, папка) и как вы их импортируете (mod), а затем сопоставляете имена с разными модулями (use), это начинает обретать смысл.

Также важно помнить, что структура проекта Rust очень специфична (application vs library = main.rs vs lib.rs), поэтому определенные файлы должны существовать в разных контекстах. (mod.rs).

Надеюсь, это было полезно (это было для меня, написавшего это!).

Далее структуры, связанные функции и методы.