وارد کردن مسیرها به محدوده با کلمه کلیدی 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 نیز بهکار میرود؛
برای اطلاعات بیشتر دربارهی این الگو، به مستندات کتابخانهی استاندارد مراجعه کنید.