Подключение путей в область видимости с помощью ключевого слова 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.

Файл: src/lib.rs

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();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Листинг 7-11. Подключение модуля в область видимости с помощью use

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

Также можно подключить элемент в область видимости с помощью use и относительного пути. Листинг 7-12 показывает как указать относительный путь, чтобы получить то же поведение, что и в листинге 7-11.

Файл: src/lib.rs

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

use self::front_of_house::hosting;

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

Листинг 7-12. Подключение модуля в область видимости с помощью use и относительного пути

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

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

Файл: src/lib.rs

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();
    add_to_waitlist();
    add_to_waitlist();
}

Листинг 7-13. Подключение функции add_to_waitlist в область видимости с помощью use не идиоматическим способом

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

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

Файл: src/main.rs

use std::collections::HashMap;

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

Листинг 7-14. Подключение HashMap в область видимости идиоматическим способом

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

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

Файл: src/lib.rs


#![allow(unused)]
fn main() {
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}
}

Листинг 7-15. Подключение двух типов с одинаковыми именами в одну область видимости требует использования их родительских модулей.

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

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

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

Файл: src/lib.rs


#![allow(unused)]
fn main() {
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}
}

Листинг 7-16. Переименование типа с помощью ключевого слова as при его подключении в область видимости

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

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

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

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

Файл: src/lib.rs

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();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Листинг 7-17. Делаем при помощи pub use имя доступным для любого кода из новой области видимости

Благодаря использованию pub use, внешний код теперь может вызывать функцию add_to_waitlist используя hosting::add_to_waitlist. Если бы мы не указали pub use, то только функция eat_at_restaurant могла бы вызывать hosting::add_to_waitlist в своей области видимости, но внешний код не смог бы так сделать.

Реэкспорт полезен, когда внутренняя структура вашего кода отличается от того, как другие программисты вызывающие ваш код, будут думать о предметной области. Например, в метафоре про ресторан, люди работающие в ресторане, думают о «фронтальной части дома» и «задней части дома». Но вероятно что клиенты посещающие ресторан, не буду думать о частях ресторана в таких терминах. С помощью pub use, можно структурировать код по одному принципу, но наружу публиковать другие варианты структуризации кода (подходящие под разные предметные области). Благодаря этому мы можем сделать нашу библиотеку удобно организованной как для программистов, работающих над библиотекой так и для программистов вызывающих нашу библиотеку.

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

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

Файл: Cargo.toml

[dependencies]
rand = "0.8.3"

Добавление 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!("Guess the number!");

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

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", 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 в область видимости:

Файл: src/main.rs

use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    println!("Guess the number!");

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

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

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

Файл: src/main.rs

use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

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

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

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

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

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

Файл: src/lib.rs


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

Листинг 7-19. Два оператора use где один содержит часть пути другого

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

Файл: src/lib.rs


#![allow(unused)]
fn main() {
use std::io::{self, Write};
}

Листинг 7-20. Объединение путей из листинга 7-19 в один оператор use

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

Оператор * (Glob)

Если хотим подключить в область видимости все общие элементы, определённые в пути, можно указать путь за которым следует оператор * (звёздочка, glob):


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

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

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