Unser E/A-Projekt verbessern
Mit diesem Wissen über Iteratoren können wir unser E/A-Projekt in Kapitel 12
verbessern. Wir werden Bereiche im Code klarer und prägnanter gestalten. Lass
uns herausfinden wie Iteratoren unsere Implementierung der
Funktion Config::build
und der Funktion search
optimieren können.
Ein clone
durch Verwendung eines Iterators entfernen
Im Codeblock 12-6 haben wir Programmcode hinzugefügt, der einen Anteilstyp
(slice) von Zeichenketten
-Werten (String values) nimmt, und erzeugten eine
Config
-Struktur indem wir den Anteilstyp indexierten und die Werte klonten
und der Config
-Struktur die Eigentümerschaft dieser Werte gaben. Im Codeblock
13-17 haben wir die Implementierung der Funktion Config::build
so reproduziert
wie sie im Codeblock 12-23 aussah:
Dateiname: src/lib.rs
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("Nicht genügend Argumente");
}
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 = "dukt";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
PRODUKTION.";
assert_eq!(vec!["sicher, schnell, produktiv."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Zu diesem Zeitpunkt sagten wir, dass man sich keine Gedanken wegen der
ineffizienten clone
-Aufrufe machen soll, da sie zu einem späteren Zeitpunkt
entfernt werden. Jetzt ist es an der Zeit, dass wir uns darum kümmern!
Wir haben clone
benutzt, da wir einen Anteilstyp mit String
-Elementen im
Parameter args
haben, aber die Funktion build
besitzt args
nicht. Um die
Eigentümerschaft einer Config
-Instanz zurückzugeben, mussten wir die Werte
aus den Feldern query
und file_path
von Config
klonen, damit die
Config
-Instanz ihre Werte besitzen kann.
Mithilfe unserer neuen Kenntnisse über Iteratoren können wir die Funktion
build
so ändern, dass sie die Eigentümerschaft eines Iterators als Argument
nimmt anstatt sich einen Anteilstyp auszuleihen. Wir werden die
Iterator
-Funktionalität benutzen und nicht mehr den Programmcode der die
Länge des Anteilstyps überprüft und an bestimmte Stellen indiziert. Dadurch
wird deutlich, was die Funktion Config::build
bewirkt, da der Iterator auf
Werte zugreift.
Sobald Config::build
die Eigentümerschaft des Iterators hat und keine
ausleihenden Indexierungsoperationen mehr verwendet, können wir die
String
-Werte vom Iterator
in Config
verschieben anstatt clone
aufzurufen und eine neue Zuweisung vorzunehmen.
Direktes Verwenden des zurückgegebenen Iterators
Öffne die Datei src/main.rs deines E/A-Projekts, sie sollte so aussehen:
Dateiname: 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 beim Parsen der Argumente: {err}");
process::exit(1);
});
// --abschneiden--
if let Err(e) = minigrep::run(config) {
eprintln!("Anwendungsfehler: {e}");
process::exit(1);
}
}
Wir werden zuerst den Anfang der Funktion main
von Codeblock 12-24 in den
Programmcode im Codeblock 13-18 ändern, der dieses Mal einen Iterator
verwendet. Dieser Code wird erst kompilieren, wenn wir auch Config::build
abgeändert haben.
Dateiname: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem beim Parsen der Argumente: {err}");
process::exit(1);
});
// --abschneiden--
if let Err(e) = minigrep::run(config) {
eprintln!("Anwendungsfehler: {e}");
process::exit(1);
}
}
Die env::arg
-Funktion gibt einen Iterator zurück! Anstatt die Werte des Iterators
in einem Vektor zu sammeln und dann einen Anteilstyp an Config::build
zu
übergeben, geben wir nun die Eigentümerschaft des Iterators, der von env::args
zurückgegeben wird, direkt an Config::build
.
Als Nächstes müssen wir die Definition von Config::build
aktualisieren.
Ändere in der Datei src/lib.rs deines E/A-Projekts die Signatur von
Config::build
um, damit sie so wie im Codeblock 13-26 aussieht. Dies wird
noch immer nicht kompilieren, da der Funktionsrumpf aktualisiert werden muss.
Dateiname src/lib.rs
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> {
// --abschneiden--
if args.len() < 3 {
return Err("Nicht genügend Argumente");
}
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 = "dukt";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
PRODUKTION.";
assert_eq!(vec!["sicher, schnell, produktiv."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Laut Dokumentation der Standardbibliothek für die Funktion env::args
ist der
Typ des zurückgegebenen Iterators std::env::Args
, und dieser Typ
implementiert das Merkmal Iterator
und gibt String
-Werte zurück.
Wir haben die Signatur der Funktion Config::build
aktualisiert, sodass der
Parameter args
einen generischen Typ mit den Merkmalsabgrenzungen impl Iterator<Item = String>
anstelle von &[String]
hat. Diese Verwendung der
Syntax impl Trait
, die wir im Abschnitt „Merkmale als
Parameter“ in Kapitel 10 besprochen haben, bedeutet, dass args
jeder Typ sein kann, der das Merkmal Iterator
implementiert und
String
-Elemente zurückgibt.
Da wir die Eigentümerschaft von args
übernehmen und args
beim Iterieren
verändern werden, können wir das Schlüsselwort mut
in die Spezifikation des
Parameters args
eintragen, um ihn veränderbar (mutable) zu machen.
Verwenden von Iterator
-Merkmalen anstelle von Indizierung
Als Nächstes werden wir den Rumpf von Config::build
in Ordnung bringen. Da
args
das Merkmal Iterator
implementiert, wissen wir, dass wir die Methode
next
darauf aufrufen können! Codeblock 13-20 aktualisiert den Code aus
Codeblock 12-23, um die next
-Methode zu verwenden:
Dateiname: src/lib.rs
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("Keine Abfragezeichenkette erhalten"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Keinen Dateinamen erhalten"),
};
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 = "dukt";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
PRODUKTION.";
assert_eq!(vec!["sicher, schnell, produktiv."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Beachte, dass der erste Wert des Rückgabewerts von env::args
der Name des
Programms ist, wir wollen das ignorieren und rufen daher gleich next
auf um
zum nächsten Wert zu gelangen und den ersten Rückgabewert zu überspringen. Als
Nächstes rufen wir next
auf, um den Wert zu erhalten, den wir in das Feld query
von Config
einfügen möchten. Falls next
ein Some
zurückgibt, benutzen wir
match
, um den Wert zu extrahieren, wenn es jedoch None
zurückgibt,
bedeutet dies, das nicht genügend Argumente eingegeben wurden und wir kehren
vorzeitig mit einem Err
zurück. Dasselbe machen wir für den Wert file_path
.
Programmcode mit Iteratorenadapter klarer gestalten
Wir können die Vorteile der Iteratoren auch in der Funktion search
unseres
E/A-Projekts nutzen, die hier im Codeblock 13-21 wiedergegeben ist, wie im
Codeblock 12-19:
Dateiname: src/lib.rs
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("Nicht genügend Argumente");
}
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 = "dukt";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.";
assert_eq!(vec!["sicher, schnell, produktiv."], search(query, contents));
}
}
Wir können diesen Programmcode durch die Verwendung von Iteratoradaptern
prägnanter gestalten und vermeiden, einen veränderbaren Vektor results
für
die Zwischenergebnisse zu haben. Bevorzugt wird im funktionalen Programmierstil
die Menge der veränderbaren Werte reduziert, um den Code übersichtlicher zu
machen. Das Entfernen des veränderbar-Status kann uns eventuell zukünftige
Verbesserungen ermöglichen, um die Suche parallel auszuführen, da wir uns nicht
um die Verwaltung des simultanen Zugriffs auf den Vektor results
kümmern
müssen. Codeblock 13-22 zeigt diese Änderung:
Dateiname: src/lib.rs
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("Keine Abfragezeichenkette erhalten"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Keinen Dateinamen erhalten"),
};
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 = "dukt";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
PRODUKTION.";
assert_eq!(vec!["sicher, schnell, produktiv."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
sicher, schnell, produktiv.
Nimm drei.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Denke daran, der Zweck der Funktion search
besteht darin, alle Zeilen in
contents
zurückzugeben, die die query
enthalten. So ähnlich wie im Beispiel
filter
im Codeblock 13-16 verwendet dieser Programmcode den filter
-Adapter,
um nur die Zeilen beizubehalten, für die line.contains(query)
den Wert true
zurückgibt.
Wir sammeln dann die passenden Zeilen mit collect
in einen anderen Vektor.
Viel einfacher! Nimm die gleiche Änderung vor, um Iteratormethoden auch in der
Funktion search_case_insensitive
zu nutzen.
Zwischen Schleifen und Iteratoren wählen
Die nächste logische Frage wäre, welchen Stil du in deinem eigenen Programmcode wählen solltest und warum. Die ursprüngliche Implementierung im Codeblock 13-21 oder die Version die Iteratoren verwendet im Codeblock 13-22. Die meisten Rust-Programmierer bevorzugen den Iterator-Stil. Zunächst ist es zwar schwieriger, den Überblick zu behalten, aber sobald du ein Gefühl für die verschiedenen Iteratoradapter und deren Funktionsweise hast, können Iteratoren einfacher zu verstehen sein. Statt mit verschiedensten Schleifen herumzuspielen und Vektoren zu erstellen, konzentriert sich der Programmcode auf das höhere Ziel der Schleife. Dadurch wird ein Teil des gewöhnlichen Programmcodes abstrahiert und die einzigartigen Konzepte, z.B. die Filterbedingung die jedes Element bestehen muss um durch den Iterator zu kommen, werden leichter erkennbar.
Aber sind beide Implementierungen wirklich gleichwertig? Die intuitive Annahme könnte sein, dass die weniger abstrakte Schleife schneller ist. Lass uns über Performanz sprechen.