بهبود پروژه 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;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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,
})
}
}
pub 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(())
}
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
}
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)
);
}
}
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::process;
use minigrep::Config;
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) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
ابتدا شروع تابع main
که در لیست 12-24 داشتیم را به کدی که در لیست 13-18 است تغییر میدهیم، که این بار از یک iterator استفاده میکند. این کد تا زمانی که Config::build
را نیز بهروزرسانی کنیم، کامپایل نخواهد شد.
use std::env;
use std::process;
use minigrep::Config;
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) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
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;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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,
})
}
}
pub 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(())
}
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
}
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)
);
}
}
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;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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,
})
}
}
pub 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(())
}
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
}
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)
);
}
}
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 بود بازتولید شده است:
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub 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();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
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 این تغییر را نشان میدهد:
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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,
})
}
}
pub 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(())
}
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
در یک وکتور دیگر جمعآوری میکنیم. بسیار سادهتر! اگر تمایل دارید، میتوانید همین تغییر را برای استفاده از متدهای iterator در تابع search_case_insensitive
نیز انجام دهید.
انتخاب بین حلقهها یا Iteratorها
سؤال منطقی بعدی این است که کدام سبک را باید در کد خود انتخاب کنید و چرا: پیادهسازی اصلی در لیست 13-21 یا نسخهای که از iteratorها در لیست 13-22 استفاده میکند. اکثر برنامهنویسان Rust ترجیح میدهند از سبک iterator استفاده کنند. یادگیری آن در ابتدا کمی سختتر است، اما وقتی با تطبیقدهندههای مختلف iterator و کارهایی که انجام میدهند آشنا شوید، استفاده از iteratorها میتواند آسانتر شود. به جای دستوپنجه نرم کردن با بخشهای مختلف حلقهها و ساخت وکتورهای جدید، کد بر هدف سطح بالا حلقه تمرکز میکند. این کار برخی از کدهای عمومی را پنهان میکند، بنابراین مفاهیمی که مختص این کد هستند، مانند شرط فیلتر کردن که هر عنصر در iterator باید پاس کند، واضحتر دیده میشوند.
اما آیا این دو پیادهسازی واقعاً معادل هستند؟ فرضیه شهودی این است که حلقه سطح پایینتر سریعتر خواهد بود. بیایید درباره عملکرد صحبت کنیم.