بهبود پروژه I/O
با این دانش جدید درباره iteratorها، میتوانیم پروژه I/O در فصل ۱۲ را با استفاده از iteratorها بهبود بخشیم تا بخشهایی از کد واضحتر و مختصرتر شوند. بیایید ببینیم چگونه iteratorها میتوانند پیادهسازی تابع Config::build
و تابع search
را بهبود دهند.
حذف یک clone
با استفاده از یک Iterator
در لیست 12-6، کدی اضافه کردیم که یک برش از مقادیر String
را گرفته و یک نمونه از ساختار Config
ایجاد میکرد. این کار با شاخصگذاری در برش و کلون کردن مقادیر انجام شد تا ساختار Config
مالک آن مقادیر شود. در لیست 13-17، پیادهسازی تابع Config::build
را همانطور که در لیست 12-23 بود بازتولید کردهایم:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build
از لیست 12-23در آن زمان گفتیم که نگران تماسهای ناکارآمد clone
نباشید زیرا در آینده آنها را حذف خواهیم کرد. خب، اکنون زمان آن فرا رسیده است!
ما در اینجا به clone
نیاز داشتیم زیرا در پارامتر args
یک برش با عناصر String
داریم، اما تابع build
مالک args
نیست. برای بازگرداندن مالکیت یک نمونه Config
، مجبور بودیم مقادیر فیلدهای query
و file_path
را از Config
کلون کنیم تا نمونه Config
بتواند مالک مقادیرش باشد.
با دانش جدیدمان درباره iteratorها، میتوانیم تابع build
را تغییر دهیم تا مالکیت یک iterator را به عنوان آرگومان خود بگیرد، به جای اینکه یک برش را قرض بگیرد. ما از قابلیتهای iterator به جای کدی که طول برش را بررسی میکند و به مکانهای خاص شاخص میزند، استفاده خواهیم کرد. این کار مشخص میکند که تابع Config::build
چه کاری انجام میدهد زیرا iterator به مقادیر دسترسی پیدا خواهد کرد.
زمانی که Config::build
مالکیت iterator را به دست آورد و استفاده از عملیات شاخصگذاری که قرض میگیرند را متوقف کرد، میتوانیم مقادیر String
را از iterator به Config
منتقل کنیم به جای اینکه clone
را فراخوانی کنیم و تخصیص جدیدی ایجاد کنیم.
استفاده مستقیم از Iterator بازگرداندهشده
فایل src/main.rs پروژه I/O خود را باز کنید، که باید به این شکل باشد:
نام فایل: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
ابتدا شروع تابع main
که در لیست 12-24 داشتیم را به کدی که در لیست 13-18 است تغییر میدهیم، که این بار از یک iterator استفاده میکند. این کد تا زمانی که Config::build
را نیز بهروزرسانی کنیم، کامپایل نخواهد شد.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args
به Config::build
تابع env::args
یک iterator بازمیگرداند! به جای جمعآوری مقادیر iterator در یک وکتور و سپس ارسال یک برش به Config::build
، اکنون ما مالکیت iterator بازگرداندهشده از env::args
را مستقیماً به Config::build
ارسال میکنیم.
سپس باید تعریف تابع Config::build
را بهروزرسانی کنیم. در فایل src/lib.rs پروژه I/O خود، امضای تابع Config::build
را به شکلی که در لیست 13-19 نشان داده شده تغییر دهید. این کد هنوز کامپایل نخواهد شد زیرا باید بدنه تابع را نیز بهروزرسانی کنیم.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build
برای انتظار یک iteratorمستندات کتابخانه استاندارد برای تابع env::args
نشان میدهد که نوع iterator بازگرداندهشده std::env::Args
است، و این نوع صفت Iterator
را پیادهسازی کرده و مقادیر String
بازمیگرداند.
ما امضای تابع Config::build
را بهروزرسانی کردهایم تا پارامتر args
یک نوع جنریک با محدودیتهای صفت impl Iterator<Item = String>
باشد به جای &[String]
. این استفاده از نحو impl Trait
که در بخش “Traits به عنوان پارامترها” فصل 10 بحث شد، به این معناست که args
میتواند هر نوعی باشد که صفت Iterator
را پیادهسازی کرده و آیتمهای String
بازمیگرداند.
از آنجا که مالکیت args
را به دست میآوریم و با پیمایش در آن، args
را تغییر خواهیم داد، میتوانیم کلمه کلیدی mut
را به مشخصات پارامتر args
اضافه کنیم تا آن را قابل تغییر کنیم.
استفاده از متدهای صفت Iterator
به جای شاخصگذاری
سپس بدنه تابع Config::build
را اصلاح میکنیم. از آنجا که args
صفت Iterator
را پیادهسازی کرده است، میدانیم که میتوانیم متد next
را روی آن فراخوانی کنیم! لیست 13-20 کد لیست 12-23 را برای استفاده از متد next
بهروزرسانی میکند:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build
برای استفاده از متدهای iteratorبه یاد داشته باشید که اولین مقدار در مقدار بازگرداندهشده از env::args
نام برنامه است. ما میخواهیم آن را نادیده بگیریم و به مقدار بعدی برسیم، بنابراین ابتدا next
را فراخوانی میکنیم و هیچ کاری با مقدار بازگشتی انجام نمیدهیم. سپس، next
را فراخوانی میکنیم تا مقداری که میخواهیم در فیلد query
از Config
قرار دهیم را دریافت کنیم. اگر next
یک Some
بازگرداند، از یک match
برای استخراج مقدار استفاده میکنیم. اگر None
بازگرداند، به این معنی است که آرگومانهای کافی ارائه نشدهاند و با مقدار Err
زودتر بازمیگردیم. همین کار را برای مقدار file_path
انجام میدهیم.
واضحتر کردن کد با تطبیقدهندههای Iterator
ما همچنین میتوانیم از iteratorها در تابع search
پروژه I/O خود بهره ببریم. این تابع در لیست 13-21 به همان شکلی که در لیست 12-19 بود بازتولید شده است:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search
از لیست 12-19ما میتوانیم این کد را با استفاده از متدهای تطبیقدهنده iterator به شکلی مختصرتر بنویسیم. این کار همچنین به ما اجازه میدهد که از داشتن یک وکتور میانی قابل تغییر به نام results
اجتناب کنیم. سبک برنامهنویسی تابعی ترجیح میدهد مقدار حالتهای قابل تغییر را به حداقل برساند تا کد واضحتر شود. حذف حالت قابل تغییر ممکن است امکان ارتقاء آینده را فراهم کند تا جستجو به صورت موازی انجام شود، زیرا نیازی به مدیریت دسترسی همزمان به وکتور results
نخواهیم داشت. لیست 13-22 این تغییر را نشان میدهد:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
search
به یاد دارید که هدف تابع search
این است که تمام خطوط موجود در contents
را که شامل query
هستند برگرداند. مشابه با مثال filter
در لیستینگ 13-16، این کد از آداپتور filter
استفاده میکند تا فقط خطوطی را نگه دارد که در آنها line.contains(query)
مقدار true
را بازمیگرداند. سپس خطوط مطابق را با استفاده از collect
در یک وکتور جدید جمعآوری میکنیم. خیلی سادهتر! شما میتوانید همین تغییر را در تابع search_case_insensitive
نیز اعمال کرده و از متدهای پیمایشگر استفاده کنید.
برای بهبود بیشتر، مقدار بازگشتی تابع search
را بهجای وکتور، یک پیمایشگر قرار دهید؛ با حذف فراخوانی collect
و تغییر نوع بازگشتی به impl Iterator<Item = &'a str>
، این تابع به یک آداپتور پیمایشگر تبدیل میشود. توجه داشته باشید که باید تستها را نیز مطابق این تغییر بهروزرسانی کنید! یک فایل بزرگ را با ابزار minigrep
خود، قبل و بعد از این تغییر جستوجو کنید تا تفاوت رفتار را مشاهده نمایید. قبل از این تغییر، برنامه تا زمانی که تمام نتایج جمعآوری نشدهاند چیزی چاپ نمیکند، اما پس از این تغییر، نتایج بهمحض یافتن هر خط مطابق چاپ میشوند، زیرا حلقه for
در تابع run
میتواند از ویژگی تنبلی پیمایشگر استفاده کند.
انتخاب بین حلقهها و پیمایشگرها
سؤال منطقی بعدی این است که کدام سبک را در کد خود انتخاب کنیم و چرا: پیادهسازی اولیه در لیستینگ 13-21 یا نسخهای که از پیمایشگرها استفاده میکند در لیستینگ 13-22 (با فرض اینکه تمام نتایج را پیش از بازگرداندن جمعآوری میکنیم و نه اینکه خود پیمایشگر را بازگردانیم). بیشتر برنامهنویسان Rust ترجیح میدهند از سبک پیمایشگر استفاده کنند. در ابتدا ممکن است درک آن کمی دشوارتر باشد، اما زمانی که با آداپتورهای مختلف پیمایشگر و عملکرد آنها آشنا شدید، کار با آنها آسانتر خواهد بود. به جای کلنجار رفتن با بخشهای مختلف حلقه و ساخت وکتورهای جدید، کد روی هدف سطح بالای حلقه تمرکز میکند. این امر باعث پنهان شدن بخشی از کدهای تکراری شده و فهم مفاهیم خاص این کد (مانند شرط فیلتر شدن هر عنصر پیمایشگر) را آسانتر میکند.
اما آیا این دو پیادهسازی واقعاً معادل هم هستند؟ فرض شهودی ممکن است این باشد که حلقه سطح پایینتر سریعتر است. بیایید درباره عملکرد صحبت کنیم.