وارد کردن مسیرها به محدوده با کلمه کلیدی 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 داشته باشیم.
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 و یک مسیر در یک محدوده مشابه ایجاد یک لینک نمادین در فایلسیستم است. با اضافه کردن use crate::front_of_house::hosting در ریشه جعبه (crate)، hosting اکنون یک نام معتبر در آن محدوده است، درست مانند اینکه ماژول hosting در ریشه جعبه (crate) تعریف شده باشد. مسیرهایی که با 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();
}
}
use تنها در همان حوزهای (scope) اعمال میشود که در آن قرار داردخطای کامپایلر نشان میدهد که میانبر دیگر در ماژول 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();
}
add_to_waitlist به محدوده با use که غیر ایدیوماتیک استاگرچه هم لیستینگ 7-11 و هم لیستینگ 7-13 کار مشابهی انجام میدهند، لیستینگ 7-11 روش ایدیوماتیک برای وارد کردن یک تابع به محدوده با use است. وارد کردن ماژول والد تابع با use به این معنا است که باید ماژول والد را هنگام فراخوانی تابع مشخص کنیم. مشخص کردن ماژول والد هنگام فراخوانی تابع نشان میدهد که تابع به صورت محلی تعریف نشده است، در حالی که همچنان تکرار مسیر کامل را به حداقل میرساند. کد موجود در لیستینگ 7-13 مشخص نمیکند که add_to_waitlist کجا تعریف شده است.
از طرف دیگر، وقتی ساختارها، enumها، و سایر آیتمها را با use وارد میکنیم، ایدیوماتیک است که مسیر کامل را مشخص کنیم. لیستینگ 7-14 روش ایدیوماتیک برای وارد کردن ساختار HashMap از کتابخانه استاندارد به محدوده جعبه (crate) باینری را نشان میدهد.
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
HashMap به محدوده به روش ایدیوماتیکهیچ دلیل قوی پشت این عرف نیست: این فقط کنوانسیونی است که در جامعه Rust به وجود آمده و افراد به خواندن و نوشتن کد 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(())
}
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 نشان میدهد.
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();
}
pub useقبل از این تغییر، کد خارجی باید تابع add_to_waitlist را با استفاده از مسیر restaurant::front_of_house::hosting::add_to_waitlist() فراخوانی میکرد، که همچنین نیاز داشت ماژول front_of_house به عنوان pub علامتگذاری شود. حالا که این pub use ماژول hosting را از ماژول ریشه دوباره صادر کرده است، کد خارجی میتواند از مسیر restaurant::hosting::add_to_waitlist() استفاده کند.
Re-exporting زمانی مفید است که ساختار داخلی کد شما با نحوهی تفکر برنامهنویسانی که از کد شما استفاده میکنند دربارهی دامنه، متفاوت باشد.
برای مثال، در این تمثیل رستوران، کسانی که رستوران را اداره میکنند دربارهی «بخش جلویی» (front of house) و «بخش پشتی» (back of house) فکر میکنند.
اما مشتریانی که به رستوران میآیند احتمالاً دربارهی قسمتهای رستوران با چنین اصطلاحاتی فکر نمیکنند.
با استفاده از pub use میتوانیم کد خود را با یک ساختار بنویسیم ولی ساختاری متفاوت را در معرض استفاده قرار دهیم.
این کار باعث میشود کتابخانهی ما هم برای برنامهنویسانی که روی کتابخانه کار میکنند و هم برای برنامهنویسانی که از آن استفاده میکنند، بهخوبی سازماندهی شده باشد.
در فصل ۱۴، در بخش “صادرات یک API عمومی راحت با استفاده از pub use”،
مثال دیگری از pub use و تأثیر آن بر مستندات crate شما را بررسی خواهیم کرد.
استفاده از بستههای خارجی
در فصل ۲، ما یک پروژه بازی حدسزنی برنامهریزی کردیم که از یک بسته خارجی به نام rand برای تولید اعداد تصادفی استفاده میکرد. برای استفاده از rand در پروژه خود، این خط را به Cargo.toml اضافه کردیم:
rand = "0.8.5"
اضافه کردن rand به عنوان یک وابستگی در Cargo.toml به Cargo میگوید که بسته rand و هرگونه وابستگی را از crates.io دانلود کرده و rand را در پروژه ما در دسترس قرار دهد.
سپس، برای وارد کردن تعاریف crate rand به حوزهی پکیج خود،
یک خط use اضافه کردیم که با نام crate، یعنی rand، آغاز شد
و آیتمهایی را که میخواستیم وارد حوزه کنیم، فهرست کردیم.
به یاد داشته باشید که در بخش “تولید یک عدد تصادفی” در فصل ۲،
trait مربوط به 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 برای وارد کردن آیتمها از جعبه (crate) آنها به محدوده.
توجه داشته باشید که کتابخانه استاندارد std نیز یک جعبه (crate) خارجی برای بسته ما است. از آنجا که کتابخانه استاندارد همراه با زبان Rust ارائه میشود، نیازی به تغییر Cargo.toml برای گنجاندن std نداریم. اما برای وارد کردن آیتمها از آن به محدوده بسته خود، باید به آن با use ارجاع دهیم. برای مثال، با HashMap از این خط استفاده میکردیم:
#![allow(unused)] fn main() { use std::collections::HashMap; }
این یک مسیر مطلق است که با std، نام جعبه (crate) کتابخانه استاندارد، شروع میشود.
استفاده از مسیرهای تو در تو برای سادهسازی لیستهای بزرگ use
اگر از چندین آیتم تعریفشده در یک جعبه (crate) یا ماژول استفاده کنیم، فهرست کردن هر آیتم در خط خود میتواند فضای عمودی زیادی در فایلهای ما اشغال کند. برای مثال، این دو دستور use که در بازی حدسزنی در لیستینگ ۲-۴ استفاده کردیم آیتمهایی از 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!"),
}
}
در برنامههای بزرگتر، وارد کردن بسیاری از آیتمها از یک جعبه (crate) یا ماژول مشابه با استفاده از مسیرهای تو در تو میتواند تعداد دستورات use جداگانه مورد نیاز را به طور قابلتوجهی کاهش دهد.
ما میتوانیم در هر سطحی از یک مسیر، از یک مسیر تو در تو استفاده کنیم، که این کار در مواقعی که دو دستور use دارای یک زیرمسیر مشترک هستند، مفید است. برای مثال، لیستینگ 7-19 دو دستور use را نشان میدهد: یکی که std::io را به محدوده وارد میکند و دیگری که std::io::Write را به محدوده وارد میکند.
use std::io;
use std::io::Write;
use که یکی زیرمسیر دیگری استبخش مشترک این دو مسیر، std::io است که مسیر کامل اولین دستور use را تشکیل میدهد. برای ترکیب این دو مسیر به یک دستور use، میتوانیم از self در مسیر تو در تو استفاده کنیم، همانطور که در لیستینگ 7-20 نشان داده شده است.
use std::io::{self, Write};
useاین خط، std::io و std::io::Write را به محدوده وارد میکند.
عملگر Glob
اگر بخواهیم تمام آیتمهای عمومی تعریفشده در یک مسیر را به محدوده وارد کنیم، میتوانیم آن مسیر را به همراه عملگر * مشخص کنیم:
#![allow(unused)] fn main() { use std::collections::*; }
این دستور use تمام آیتمهای عمومی تعریفشده در std::collections را وارد حوزهی فعلی میکند.
در استفاده از عملگر glob دقت کنید!
استفاده از glob میتواند باعث شود تشخیص اینکه چه نامهایی در حوزه هستند
و یک نام استفادهشده در برنامه از کجا آمده، دشوارتر شود.
علاوه بر این، اگر وابستگی تغییراتی در تعاریف خود ایجاد کند، آنچه شما وارد کردهاید نیز تغییر میکند،
که ممکن است هنگام بهروزرسانی وابستگی، باعث بروز خطای کامپایلر شود—
برای مثال، اگر وابستگی تعریفی با همان نامی اضافه کند که شما نیز در همان حوزه تعریف کردهاید.
عملگر glob اغلب هنگام تست برای وارد کردن تمام آیتمهای تحت تست به ماژول tests استفاده میشود؛
در فصل ۱۱ در بخش “چگونه تست بنویسیم” دربارهی آن صحبت خواهیم کرد.
همچنین، عملگر glob گاهی در قالب الگوی prelude نیز بهکار میرود؛
برای اطلاعات بیشتر دربارهی این الگو، به مستندات کتابخانهی استاندارد مراجعه کنید.