Добавление путей в область видимости ключевым словом use

Необходимость записывать пути к функциям вызова может показаться неудобной и многословной. В Листинге 7-7 независимо от того, выбирали ли мы абсолютный или относительный путь к функции add_to_waitlist, каждый раз, когда мы хотели вызвать add_to_waitlist, нам приходилось также указывать front_of_house и hosting. К счастью, есть способ упростить этот процесс: мы можем один раз создать псевдоним на путь при помощи ключевого слова use, а затем использовать более короткое имя везде в области видимости.

В Листинге 7-11 мы подключили модуль crate::front_of_house::hosting в область видимости функции eat_at_restaurant, поэтому нам достаточно только указать hosting::add_to_waitlist для вызова функции add_to_waitlist внутри eat_at_restaurant.

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

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Добавление use и пути в область видимости аналогично созданию символической ссылки в файловой системе. С добавлением use crate::front_of_house::hosting в корневой модуль крейта, hosting становится допустимым именем в этой области, как если бы модуль hosting был определён в корне крейта. Пути, подключённые в область видимости с помощью use, также проверяются на приватность, как и любые другие пути.

Обратите внимание, что use создаёт псевдоним только для той конкретной области, в которой это объявление use и находится. В Листинге 7-12 функция eat_at_restaurant перемещается в новый дочерний модуль с именем customer, область видимости которого отличается от области видимости инструкции use, поэтому тело функции не будет компилироваться.

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

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}

Ошибка компилятора показывает, что данный псевдоним не может использоваться в модуле customer:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`
   |
help: consider importing this module through its public re-export
   |
10 +     use crate::hosting;
   |

warning: unused import: `crate::front_of_house::hosting`
 --> src/lib.rs:7:5
  |
7 | use crate::front_of_house::hosting;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted

Обратите внимание, что есть также предупреждение о том, что use не спользуется в своей области видимости! Чтобы решить эту проблему, можно переместить use в модуль customer, или же можно сослаться на псевдоним в родительском модуле с помощью super::hosting в дочернем модуле customer.

Создание идиоматических путей с use

В Листинге 7-11 вы могли задаться вопросом, почему мы указали use crate::front_of_house::hosting, а затем вызвали hosting::add_to_waitlist внутри eat_at_restaurant вместо указания в use полного пути прямо до функции add_to_waitlist для получения того же результата, что в Листинге 7-13.

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

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}

Хотя Листинги 7-11 и 7-13 выполняют одну и ту же задачу, Листинг 7-11 является идиоматическим способом подключения функции в область видимости с помощью use. Подключение родительского модуля функции в область видимости при помощи use означает, что мы должны указывать родительский модуль при вызове функции. Указание родительского модуля при вызове функции даёт понять, что функция не определена локально, но в то же время сводя к минимуму повторение полного пути. В коде Листинга 7-13 не ясно, где именно определена add_to_waitlist.

С другой стороны, при подключении структур, перечислений и других элементов использованием use, идиоматически правильным будет указывать полный путь. Листинг 7-14 показывает идиоматический способ подключения структуры стандартной библиотеки HashMap в область видимости бинарного крейта.

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

За этой идиомой нет веской причины: это просто соглашение, которое появилось само собой. Люди привыкли читать и писать код на Rust таким образом.

Исключением из этой идиомы является случай, когда мы подключаем два элемента с одинаковыми именами в область видимости используя инструкцию use — Rust просто не позволяет этого сделать. Листинг 7-15 показывает, как подключить в область действия два типа с одинаковыми именами Result, но из разных родительских модулей, и как на них ссылаться.

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --код сокращён--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --код сокращён--
    Ok(())
}

Как видите, использование имени родительских модулей позволяет различать два типа Result. Если бы вместо этого мы указали use std::fmt::Result и use std::io::Result, мы бы имели два типа Result в одной области видимости, и Rust не смог бы понять какой из двух Result мы имели в виду, когда нашёл бы их употребление в коде.

Предоставление новых имён с помощью ключевого слова as

Есть другое решение проблемы добавления словом use двух типов с одинаковыми именами в одну и ту же область видимости: после пути можно указать as и новое локальное имя (или псевдоним) для типа. Листинг 7-16 показывает, как по-другому написать код из Листинга 7-15, путём переименования одного из двух типов Result словом as.

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --код сокращён--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --код сокращён--
    Ok(())
}

Во второй инструкции use мы выбрали новое имя IoResult для типа std::io::Result, которое теперь не будет конфликтовать с типом Result из std::fmt, который также подключён в область видимости. Оба Листинга 7-15 и 7-16 считаются идиоматичными, так что выбор за вами!

Реэкспорт имён с pub use

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

Листинг 7-17 показывает код из Листинга 7-11, где use в корневом модуле заменено на pub use.

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

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

До этого изменения внешний код должен был вызывать функцию add_to_waitlist, используя путь restaurant::front_of_house::hosting::add_to_waitlist(). Теперь, поскольку это объявление pub use реэкспортировало модуль hosting из корневого модуля, внешний код теперь может использовать вместо него путь restaurant::hosting::add_to_waitlist().

Реэкспорт полезен, когда внутренняя структура вашего кода отличается от того, как программисты, вызывающие ваш код, думают о предметной области. Например, по аналогии с рестораном люди, управляющие им, думают о "front of house" и "back of house". Но клиенты, посещающие ресторан, вероятно, не будут думать о частях ресторана в таких терминах. Используя pub use, мы можем написать наш код с одной структурой, но сделать общедоступной другую структуру. Благодаря этому наша библиотека хорошо организована и для программистов, работающих над библиотекой, и для программистов, вызывающих библиотеку. Мы рассмотрим ещё один пример pub use и его влияние на документацию вашего крейта в разделе "Экспорт удобного общедоступного API с pub use" Главы 14.

Использование внешних пакетов

В Главе 2 мы запрограммировали игру в угадайку, где использовался внешний пакет с именем rand для генерации случайного числа. Чтобы использовать rand в нашем проекте, мы добавили эту строку в Cargo.toml.

rand = "0.8.5"

Добавление rand в качестве зависимости в Cargo.toml указывает Cargo загрузить пакет rand и все его зависимости с crates.io и сделать rand доступным для нашего проекта.

Затем, чтобы подключить определения rand в область видимости нашего пакета, мы добавили строку use, начинающуюся с названия пакета rand и списка элементов, которые мы хотим подключить в область видимости. Напомним, что в разделе "Генерация секретного числа"Главы 2 мы подключили трейт Rng в область видимости и вызвали функцию rand::thread_rng:

use std::io;
use rand::Rng;

fn main() {
    println!("Угадайте число!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("Загаданное число: {secret_number}");

    println!("Введите свою догадку.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Не удалось прочесть ввод.");

    println!("Вы предположили: {guess}");
}

Участники Сообщества Rust создали множество пакетов, доступных на crates.io, и добавление любого из них в ваш пакет включает в себя одни и те же шаги: нужно перечислить их в файле Cargo.toml вашего пакета и использовать use для подключения элементов внешних пакетов в область видимости.

Обратите внимание, что стандартная библиотека std также является крейтом, внешним по отношению к нашему пакету. Поскольку стандартная библиотека поставляется с языком Rust, нам не нужно изменять Cargo.toml для подключения std. Но нам нужно ссылаться на неё при помощи use, чтобы добавить элементы оттуда в область видимости нашего пакета. Например, с HashMap мы использовали бы эту строку:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

Это абсолютный путь, начинающийся с std, имени крейта стандартной библиотеки.

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

Если мы используем несколько элементов, определённых в одном крейте или в том же модуле, то перечисление каждого элемента в отдельной строке может занимать много вертикального пространства в файле. Например, эти две инструкции use используются в нашей игре в угадайку (Листинг 2-4) для подключения элементов из std в область видимости:

use rand::Rng;
// --код сокращён--
use std::cmp::Ordering;
use std::io;
// --код сокращён--

fn main() {
    println!("Угадайте число!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("Загаданное число: {secret_number}");

    println!("Введите свою догадку.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Не удалось прочесть ввод.");

    println!("Вы предположили: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Слишком маленькое!"),
        Ordering::Greater => println!("Слишком большое!"),
        Ordering::Equal => println!("Вы победили!"),
    }
}

Вместо этого, мы можем использовать перечисление путей, чтобы добавить эти элементы в область видимости одной строкой. Это делается, как показано в Листинге 7-18: указывается общая часть пути, за которой следуют два двоеточия, а затем фигурные скобки вокруг списка тех частей пути, которые отличаются.

use rand::Rng;
// --код сокращён--
use std::{cmp::Ordering, io};
// --код сокращён--

fn main() {
    println!("Угадайте число!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("Загаданное число: {secret_number}");

    println!("Введите свою догадку.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Не удалось прочесть ввод.");

    let guess: u32 = guess.trim().parse().expect("Пожалуйста, введите число!");

    println!("Вы предположили: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Слишком маленькое!"),
        Ordering::Greater => println!("Слишком большое!"),
        Ordering::Equal => println!("Вы победили!"),
    }
}

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

Использовать перечисление путей можно на любом уровне, что полезно при объединении двух инструкций use, которые имеют общую часть пути. Например, в Листинге 7-19 показаны две инструкции use: одна в область видимости подключает std::io, а другая — std::io::Write.

use std::io;
use std::io::Write;

Общей частью этих двух путей является std::io, и это полный первый путь. Чтобы объединить эти два пути в одной инструкции use, мы можем использовать ключевое слово self в перечислении путей, как показано в Листинге 7-20.

use std::io::{self, Write};

Эта строка подключает std::io и std::io::Write в область видимости.

Оператор *

If we want to bring all public items defined in a path into scope, we can specify that path followed by the * glob operator:

#![allow(unused)]
fn main() {
use std::collections::*;
}

Эта инструкция use подключает все открытые элементы из модуля std::collections в текущую область видимости. Будьте осторожны при использовании оператора *! Он может усложнить понимание, какие имена находятся в области видимости и где были определены имена, используемые в вашей программе.

Оператор * часто используется при тестировании для подключения всего, что есть в модуле tests; мы поговорим об этом в разделе "Как писать тесты" Главы 11. Оператор * также иногда используется как часть шаблона prelude: обратитесь к документации стандартной библиотеки для получения дополнительной информации.