Meningkatkan Project I/O Kita
Dengan pengetahuan baru tentang iterators ini, kita bisa meningkatkan project I/O
di Bab 12 dengan memakai iterators buat bikin beberapa bagian kodenya jadi lebih
jelas dan lebih ringkas. Mari kita lihat gimana iterators bisa meningkatkan
implementasi dari fungsi Config::build dan fungsi search kita.
Menghilangkan clone Menggunakan Iterator
Di Listing 12-6, kita menambahkan kode yang mengambil slice berisi nilai
String lalu membuat instance dari struct Config dengan mengindeks ke dalam
slice tersebut dan meng-clone (menyalin) nilai-nilainya, yang memungkinkan
struct Config buat memiliki (own) nilai-nilai itu. Di Listing 13-17, kita
menampilkan ulang implementasi dari fungsi Config::build persis seperti yang
ada di 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 dari Listing 12-23Waktu itu, kita bilang buat tidak mengkhawatirkan panggilan clone yang kurang
efisien karena kita bakal menghapusnya di masa depan. Nah, sekaranglah saatnya!
Kita membutuhkan clone di sini karena kita punya sebuah slice berisi
elemen-elemen String di parameter args, tapi fungsi build tidak mengambil
kepemilikan (ownership) atas args. Buat mengembalikan kepemilikan dari instance
Config, kita harus meng-clone nilai-nilai dari field query dan file_path
milik Config supaya instance Config tersebut bisa memiliki nilai-nilainya.
Dengan pengetahuan baru kita tentang iterators, kita bisa mengubah fungsi build
agar mengambil kepemilikan dari sebuah iterator sebagai argumennya, ketimbang
meminjam (borrow) sebuah slice. Kita bakal memakai fungsionalitas iterator
alih-alih kode yang mengecek panjang dari slice dan mengindeks ke lokasi
spesifik. Ini bakal memperjelas apa yang sedang dilakukan oleh fungsi
Config::build karena iterator-lah yang bakal mengakses nilai-nilainya.
Begitu Config::build mengambil kepemilikan atas iterator dan berhenti memakai
operasi indexing yang sifatnya meminjam, kita bisa memindahkan (move)
nilai-nilai String dari iterator tersebut ke dalam Config alih-alih memanggil
clone dan membuat alokasi baru.
Memakai Iterator yang Dikembalikan secara Langsung
Buka file src/main.rs dari project I/O kita, yang seharusnya kelihatan seperti ini:
Nama file: 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(())
}
Pertama-tama kita bakal mengubah bagian awal dari fungsi main yang kita punya
di Listing 12-24 jadi kode yang ada di Listing 13-18, yang mana kali ini memakai
iterator. Ini belum bisa di-compile sampai kita meng-update Config::build juga.
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 ke Config::buildFungsi env::args mengembalikan sebuah iterator! Alih-alih mengumpulkan nilai-nilai
iterator itu ke dalam sebuah vector terus meneruskan sebuah slice ke
Config::build, sekarang kita meneruskan kepemilikan dari iterator yang
dikembalikan dari env::args ke Config::build secara langsung.
Berikutnya, kita harus meng-update definisi dari Config::build. Mari kita ubah
signature (tanda tangan) dari Config::build agar kelihatan seperti Listing
13-19. Ini masih belum bisa di-compile, karena kita harus meng-update isi (body)
fungsinya.
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 buat mengharapkan sebuah iteratorDokumentasi standard library untuk fungsi env::args menunjukkan bahwa tipe
dari iterator yang dikembalikannya adalah std::env::Args, dan tipe tersebut
mengimplementasikan trait Iterator serta mengembalikan nilai String.
Kita sudah meng-update signature dari fungsi Config::build jadi parameter
args punya tipe generik dengan trait bounds impl Iterator<Item = String>
bukannya &[String]. Penggunaan sintaks impl Trait yang kita bahas di
bagian “Traits sebagai Parameter” di Bab 10 ini berarti args
bisa berupa tipe apa pun yang mengimplementasikan trait Iterator dan
mengembalikan item berupa String.
Karena kita mengambil kepemilikan atas args dan kita bakal memutasi (mengubah)
args saat kita beriterasi melewatinya, kita bisa menambahkan keyword mut
ke dalam spesifikasi parameter args buat membikinnya jadi mutable.
Memakai Method Trait Iterator Alih-Alih Indexing
Berikutnya, kita bakal memperbaiki isi dari Config::build. Karena args
mengimplementasikan trait Iterator, kita tahu kalau kita bisa memanggil method
next padanya! Listing 13-20 meng-update kode dari Listing 12-23 buat memakai
method 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 buat memakai method iteratorIngat kembali bahwa nilai pertama di dalam nilai yang dikembalikan oleh
env::args adalah nama dari programnya. Kita mau mengabaikan itu dan lanjut ke
nilai berikutnya, jadi pertama kita memanggil next dan tidak ngelakuin apa-apa
dengan nilai yang dikembalikannya. Terus kita panggil next lagi buat dapet
nilai yang mau kita masukin ke field query dari Config. Kalau next
mengembalikan Some, kita pakai match buat mengekstrak nilainya. Kalau dia
mengembalikan None, itu berarti argumen yang diberikan tidak cukup dan kita
bisa keluar lebih awal dengan nilai Err. Kita ngelakuin hal yang sama buat
nilai file_path.
Membikin Kode Lebih Jelas dengan Iterator Adapters
Kita juga bisa memanfaatkan iterators di fungsi search dari project I/O kita,
yang ditampilkan ulang di Listing 13-21 persis seperti yang ada di 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 dari Listing 12-19Kita bisa nulis kode ini dengan cara yang lebih ringkas memakai method iterator
adapter. Dengan begitu, kita juga terhindar dari kewajiban punya vector
results menengah (intermediate) yang mutable. Gaya pemrograman fungsional
lebih suka meminimalisir mutable state (keadaan yang bisa berubah) demi
bikin kodenya jadi lebih jelas. Membuang mutable state ini mungkin bisa
memungkinkan adanya peningkatan di masa depan yang bakal bikin pencarian terjadi
secara paralel karena kita tidak perlu pusing mengelola akses konruen
(concurrent access) ke vector results tersebut. Listing 13-22 menunjukkan
perubahan ini.
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)
);
}
}
searchIngat kembali bahwa tujuan dari fungsi search adalah mengembalikan semua baris
di dalam contents yang mengandung query. Mirip seperti contoh filter di
Listing 13-16, kode ini memakai adapter filter buat menyimpan cuma
baris-baris di mana line.contains(query) mengembalikan true. Kita kemudian
mengumpulkan baris-baris yang cocok itu ke dalam sebuah vector lain menggunakan
collect. Jauh lebih simpel kan! Jangan ragu buat ngelakuin perubahan yang sama
buat memakai method iterator di fungsi search_case_insensitive juga.
Sebagai peningkatan lanjutan, coba kembalikan sebuah iterator dari fungsi
search dengan menghapus panggilan ke collect dan mengubah tipe kembaliannya
jadi impl Iterator<Item = &'a str> supaya fungsi ini menjadi sebuah iterator
adapter. Perhatikan bahwa kita juga bakal harus meng-update pengujiannya! Coba
cari di dalam sebuah file berukuran besar menggunakan alat minigrep kita sebelum
dan sesudah membikin perubahan ini untuk melihat perbedaan perilakunya. Sebelum
perubahan ini, program tidak bakal mencetak hasil apa pun sampai ia selesai
mengumpulkan semua hasilnya, tapi setelah perubahan itu, hasil bakal dicetak
satu per satu setiap kali ada baris yang cocok karena for loop di fungsi run
bisa memanfaatkan sifat lazy (malas) dari iterator-nya.
Memilih antara Loops dan Iterators
Pertanyaan masuk akal berikutnya adalah gaya mana yang sebaiknya kita pilih di kode kita sendiri dan kenapa: implementasi awal di Listing 13-21 atau versi yang memakai iterators di Listing 13-22 (dengan asumsi kita mengumpulkan semua hasilnya sebelum mengembalikannya bukannya mengembalikan iteratornya). Sebagian besar programmer Rust lebih suka memakai gaya iterator. Mungkin agak sedikit susah buat memahaminya di awal, tapi begitu kita sudah mulai merasakan (get a feel for) berbagai iterator adapters dan apa yang mereka lakukan, iterators bisa jadi lebih gampang buat dipahami. Ketimbang ribet ngurusin detail soal looping (perulangan) dan membikin vector baru, kode kita jadi bisa lebih fokus ke tujuan tingkat tinggi (high-level objective) dari loop tersebut. Ini mengabstraksi kode-kode umum (commonplace code) sehingga konsep-konsep yang unik buat kode ini jadi lebih mudah dilihat, seperti contohnya kondisi penyaringan (filtering condition) yang harus dilewati sama setiap elemen di dalam iterator.
Tapi apakah kedua implementasi ini benar-benar ekuivalen (sama)? Asumsi yang mungkin muncul secara intuitif adalah loop tingkat rendah (lower-level loop) bakal lebih cepat. Mari kita bahas soal performa.