Додавання шляхів до області видимості за допомогою ключового слова use (Bringing Paths into Scope with the use Keyword)
Необхідність виписувати шляхи для виклику функцій може здаватися незручною і
повторюваною. У Лістингу 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 unresolved module or unlinked crate `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of unresolved module or unlinked crate `hosting`
|
= help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
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 {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
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 {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
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(),
що також вимагало б, щоб модуль front_of_house було позначено як pub.
Тепер, коли цей pub use повторно вивів модуль hosting з кореневого модуля,
зовнішній код може натомість використовувати шлях
restaurant::hosting::add_to_waitlist().
Повторне виведення корисне, коли внутрішня структура вашого коду відрізняється
від того, як програмісти, що викликають ваш код, сприймали б цю предметну
область. Наприклад, у цій метафорі з рестораном люди, які керують
ресторанами, думають про “front of house” і “back of house”. Але відвідувачі
ресторану, ймовірно, не думатимуть про частини ресторану в таких термінах. За
допомогою pub use ми можемо писати наш код з однією структурою, але
експонувати іншу структуру. Це робить нашу бібліотеку добре організованою
як для програмістів, що працюють над бібліотекою, так і для програмістів, що
викликають бібліотеку. Ми розглянемо ще один приклад pub use і те, як це
впливає на документацію вашого крейту в “Експортування зручного публічного
API” у розділі 14.
Використання зовнішніх пакетів
У розділі 2 ми створили проєкт гри в вгадування, який використовував зовнішній
пакет під назвою rand, щоб отримувати випадкові числа. Щоб використати
rand у нашому проєкті, ми додали цей рядок до Cargo.toml:
rand = "0.8.5"
Додавання rand як залежності в Cargo.toml каже Cargo завантажити пакет
rand і будь-які залежності з crates.io та зробити
rand доступним для нашого проєкту.
Потім, щоб додати визначення rand до області видимості нашого пакета, ми
додали рядок use, що починається з назви крейту, rand, і перелічили
елементи, які хотіли додати до області видимості. Згадайте, що в
“Generating a Random Number” у розділі 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..=100);
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
до області видимості:
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..=100);
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.
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..=100);
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!"),
}
}
У більших програмах додавання багатьох елементів до області видимості з того
самого крейту або модуля за допомогою вкладених шляхів може значно зменшити
кількість окремих операторів 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 до області видимості.
Імпортування елементів за допомогою оператора glob
Якщо ми хочемо додати до області видимості всі публічні елементи, визначені
в шляху, ми можемо вказати цей шлях, за яким іде оператор glob *:
#![allow(unused)]
fn main() {
use std::collections::*;
}
Цей оператор use додає всі публічні елементи, визначені в
std::collections, до поточної області видимості. Будьте обережні, коли
використовуєте оператор glob! Glob може ускладнити визначення того, які назви
є в області видимості і де було визначено назву, використану у вашій програмі.
Крім того, якщо залежність змінює свої визначення, імпортоване вами теж
змінюється, що може призвести до помилок компілятора під час оновлення
залежності, якщо залежність додасть визначення з такою самою назвою, як і
визначення вашого коду в тій самій області видимості, наприклад.
Оператор glob часто використовується під час тестування, щоб додати все, що
тестується, до модуля tests; ми поговоримо про це в “Як писати
тести” у розділі 11. Оператор glob також
іноді використовується як частина шаблону prelude: див. документацію
стандартної бібліотеки
для отримання додаткової інформації про цей шаблон.