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

Может показаться, что пути, которые мы писали для вызова функций неудобные, длинные и повторяющиеся. Например, в листинге 7-7, где мы выбирали абсолютный или относительный путь к функции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();
}

fn main() {}

Листинг 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();
}

fn main() {}

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

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

В листинге 7-11 вы могли бы задаться вопросом, почему мы указали use crate::front_of_house::hosting, а затем вызвали use crate::front_of_house::hosting внутри 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();
}

fn main() {}

Листинг 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();
}

fn main() {}

Листинг 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.5.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!("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 в текущую область видимости. Будьте осторожны при использовании оператора glob! Он может усложнить понимание, какие имена находятся в области видимости и где были определены имена, используемые в вашей программе.

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