ปรับปรุงโปรเจกต์ I/O ของเรา
ด้วยความรู้ใหม่นี้เกี่ยวกับ iterator เราปรับปรุงโปรเจกต์ I/O ในบทที่
12 ได้โดยใช้ iterator เพื่อทำให้โค้ดชัดเจนและกระชับมากขึ้น มาดู
ว่า iterator ปรับปรุง implementation ของเราของฟังก์ชัน Config::build
และฟังก์ชัน search ได้อย่างไร
ลบ clone โดยใช้ Iterator
ใน Listing 12-6 เราเพิ่มโค้ดที่รับ slice ของค่า String และสร้าง
instance ของ struct Config โดย index เข้า slice และ clone ค่า ทำให้
struct Config own ค่าเหล่านั้นได้ ใน Listing 13-17 เราได้
reproduce implementation ของฟังก์ชัน Config::build ในแบบที่มันเป็น
ใน Listing 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 จาก Listing 12-23ในเวลานั้น เราบอกไม่ให้กังวลเกี่ยวกับการเรียก clone ที่ไม่มี
ประสิทธิภาพเพราะเราจะลบพวกมันในอนาคต ดี เวลานั้นคือตอนนี้!
เราต้องการ clone ที่นี่เพราะเรามี slice ที่มี element String ใน
parameter args แต่ฟังก์ชัน build ไม่ own args เพื่อ return
ownership ของ instance Config เราต้อง clone ค่าจาก field query
และ file_path ของ Config เพื่อให้ instance Config own ค่าของ
มันได้
ด้วยความรู้ใหม่ของเราเกี่ยวกับ iterator เราเปลี่ยนฟังก์ชัน build
ให้รับ ownership ของ iterator เป็นอาร์กิวเมนต์แทน borrow slice ได้
เราจะใช้ functionality iterator แทนโค้ดที่ตรวจสอบความยาวของ slice
และ index เข้าที่เฉพาะ นี่จะทำให้ชัดเจนว่าฟังก์ชัน Config::build
กำลังทำอะไรเพราะ iterator จะเข้าถึงค่า
เมื่อ Config::build รับ ownership ของ iterator และหยุดใช้
operation indexing ที่ borrow เราย้ายค่า String จาก iterator เข้า
Config แทนการเรียก clone และทำ allocation ใหม่ได้
ใช้ Iterator ที่ Return โดยตรง
เปิดไฟล์ src/main.rs ของโปรเจกต์ I/O ของคุณ ซึ่งควรดูแบบนี้:
Filename: 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 ที่เรามีใน Listing 12-24
ก่อน ให้เป็นโค้ดใน Listing 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 return iterator! ไม่ใช่ collect ค่า iterator
เข้า vector แล้วส่ง slice ให้ Config::build ตอนนี้เรากำลังส่ง
ownership ของ iterator ที่ return จาก env::args ให้
Config::build โดยตรง
ถัดไป เราต้องอัพเดทนิยามของ Config::build มาเปลี่ยน signature ของ
Config::build ให้ดูเหมือน Listing 13-19 นี่ยังจะไม่คอมไพล์ เพราะ
เราต้องอัพเดท body ของฟังก์ชัน
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 ให้คาดหวัง iteratordocumentation ของ standard library สำหรับฟังก์ชัน env::args แสดง
ว่า type ของ iterator ที่มัน return คือ std::env::Args และ type
นั้น implement trait Iterator และ return ค่า String
เราได้อัพเดท signature ของฟังก์ชัน Config::build เพื่อให้ parameter
args มี generic type ที่มี trait bound impl Iterator<Item = String>
แทน &[String] การใช้ syntax impl Trait ที่เราพูดถึงในส่วน
“ใช้ Trait เป็น Parameter” ของบทที่ 10
แปลว่า args เป็น type ใดก็ได้ที่ implement trait Iterator และ
return item String
เพราะเรากำลังรับ ownership ของ args และเราจะ mutate args โดย
iterate ผ่านมัน เราเพิ่ม keyword mut เข้า specification ของ
parameter args เพื่อทำให้มัน mutable ได้
ใช้เมธอด Trait Iterator
ถัดไป เราจะแก้ body ของ Config::build เพราะ args implement
trait Iterator เรารู้ว่าเราเรียกเมธอด next บนมันได้! Listing
13-20 อัพเดทโค้ดจาก Listing 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จำได้ว่าค่าแรกในค่า return ของ env::args คือชื่อของโปรแกรม เรา
ต้องการ ignore นั้นและไปยังค่าถัดไป ดังนั้นก่อนอื่นเราเรียก next
และไม่ทำอะไรกับค่า return จากนั้น เราเรียก next เพื่อรับค่าที่เรา
ต้องการใส่ใน field query ของ Config ถ้า next return Some
เราใช้ match เพื่อดึงค่า ถ้ามัน return None หมายความว่า
อาร์กิวเมนต์ไม่พอที่ถูกให้ และเรา return เร็วด้วยค่า Err เราทำสิ่ง
เดียวกันสำหรับค่า file_path
ทำให้โค้ดชัดเจนด้วย Iterator Adapter
เรายังใช้ประโยชน์ของ iterator ในฟังก์ชัน search ในโปรเจกต์ I/O
ของเราได้ ซึ่ง reproduce ที่นี่ใน Listing 13-21 ในแบบที่มันเป็นใน
Listing 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 จาก Listing 12-19เราเขียนโค้ดนี้ในแบบที่กระชับมากขึ้นโดยใช้เมธอด iterator adapter ได้
การทำเช่นนั้นยังให้เราหลีกเลี่ยงการมี vector results ระหว่างกลาง
ที่ mutable สไตล์ functional programming ชอบลดจำนวน mutable state
เพื่อทำให้โค้ดชัดเจน การลบ mutable state อาจช่วยให้การปรับปรุงใน
อนาคตทำให้การค้นหาเกิดขึ้นแบบขนานได้ เพราะเราไม่ต้องจัดการการเข้าถึง
แบบ concurrent ของ vector results Listing 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 คือ return บรรทัดทั้งหมดใน
contents ที่มี query คล้ายกับตัวอย่าง filter ใน Listing 13-16
โค้ดนี้ใช้ adapter filter เพื่อเก็บเฉพาะบรรทัดที่
line.contains(query) return true เราจากนั้น collect บรรทัดที่
ตรงกันเข้า vector อื่นด้วย collect ง่ายขึ้นมาก! รู้สึกอิสระที่จะ
ทำการเปลี่ยนแปลงเดียวกันเพื่อใช้เมธอด iterator ในฟังก์ชัน
search_case_insensitive ด้วย
สำหรับการปรับปรุงเพิ่มเติม return iterator จากฟังก์ชัน search โดย
ลบการเรียก collect และเปลี่ยน return type เป็น
impl Iterator<Item = &'a str> เพื่อให้ฟังก์ชันกลายเป็น iterator
adapter สังเกตว่าคุณจะต้องอัพเดทเทสด้วย! ค้นหาผ่านไฟล์ขนาดใหญ่โดยใช้
เครื่องมือ minigrep ของคุณก่อนและหลังการเปลี่ยนแปลงนี้ เพื่อสังเกต
ความแตกต่างในพฤติกรรม ก่อนการเปลี่ยนแปลงนี้ โปรแกรมจะไม่ print ผลใด
จนกว่ามันจะ collect ผลทั้งหมด แต่หลังการเปลี่ยนแปลง ผลจะถูก print
เมื่อแต่ละบรรทัดที่ตรงกันถูกพบ เพราะ loop for ในฟังก์ชัน run
สามารถใช้ประโยชน์จากความเป็น lazy ของ iterator
เลือกระหว่าง Loop และ Iterator
คำถามที่มีตรรกะถัดไปคือสไตล์ไหนคุณควรเลือกในโค้ดของคุณเองและทำไม — implementation เดิมใน Listing 13-21 หรือเวอร์ชันที่ใช้ iterator ใน Listing 13-22 (สมมติเรากำลัง collect ผลทั้งหมดก่อน return พวกมัน ไม่ใช่ return iterator) programmer Rust ส่วนใหญ่ชอบใช้สไตล์ iterator มันยากขึ้นหน่อยที่จะเข้าใจในตอนแรก แต่เมื่อคุณคุ้นกับ iterator adapter ต่าง ๆ และสิ่งที่พวกมันทำ iterator เข้าใจง่ายขึ้นได้ แทนที่ จะ fiddle กับชิ้นส่วนต่าง ๆ ของการ loop และสร้าง vector ใหม่ โค้ด โฟกัสที่วัตถุประสงค์ระดับสูงของ loop นี่ทำให้โค้ดที่พบทั่วไปเป็นนามธรรม เพื่อให้ง่ายขึ้นในการเห็นแนวคิดที่เป็นเอกลักษณ์ของโค้ดนี้ เช่น เงื่อนไข filter ที่แต่ละ element ใน iterator ต้องผ่าน
แต่ implementation สองแบบเทียบเท่ากันจริงไหม? สมมุติฐานโดยสัญชาตญาณ อาจเป็นว่า loop ระดับต่ำกว่าจะเร็วกว่า มาพูดถึง performance