Refactorizarea pentru îmbunătățirea modularității și gestionarea erorilor
Pentru a îmbunătăți programul nostru, ne vom axa pe rezolvarea a patru probleme care sunt legate de structura programului și de modul cum gestionează acesta erorile potențiale. În primul rând, funcția noastră main
îndeplinește în prezent două sarcini: parsează argumentele și citește fișiere. Odată cu creșterea programului nostru, numărul de sarcini gestionate de funcția main
de asemenea va crește. Pe măsură ce o funcție acumulează mai multe responsabilități, devine tot mai dificil de analizat, mai complicat de testat și mai greoi de modificat fără a afecta una dintre funcționalitățile sale. Este preferabil să separăm funcționalitățile astfel încât fiecare funcție să fie responsabilă doar de o anumită sarcină.
Al doilea aspect ce necesită atenție este faptul că, deși query
și file_path
sunt variabile de configurare pentru program, avem și variabile cum ar fi contents
ce sunt folosite pentru implementarea logicii programului. Cu cât blocul main
se extinde, cu atât va fi necesar să aducem în scop mai multe variabile; iar odată cu creșterea numărului de variabile în domeniul de vizibilitate, devine tot mai complicat să urmărim funcția fiecăreia. Ar fi mai adecvat să consolidăm variabilele de configurare într-o singură structură pentru astfel a clarifica rolul lor.
A treia problemă constă în utilizarea instrucțiunii expect
pentru generarea unui mesaj de eroare când citirea fișierului eșuează, care acum se rezumă doar la Should have been able to read the file
. Eșecul citirii unui fișier poate surveni din diverse motive - cum ar fi absența fișierului sau lipsa permisiunii de acces. Momentan, am afișa același mesaj indiferent de situație, fără a furniza ceva informativ utilizatorului.
A patra problemă este legată de utilizarea repetată a lui expect
în tratamentul diferitelor erori și faptul că, dacă un utilizator execută programul fără a oferi suficiente argumente, se va întâlni cu o eroare de tip index out of bounds
din partea Rust, o eroare care nu descrie clar problema. Ideal ar fi ca întreg codul de gestionare a erorilor să fie centralizat, astfel încât viitorii dezvoltatori să aibă un singur punct de referință la care să se raporteze dacă logica de gestionare a erorilor necesită ajustări. Concentrând codul destinat erorilor într-un loc unic ne garantăm că mesajele generate sunt pertinente și utile pentru utilizatorii finali.
Să ne ocupăm de aceste patru probleme printr-o atentă refactorizare a proiectului.
Separarea responsabilităților în proiectele binare
Problema organizațională de atribuire a responsabilităților pentru diverse sarcini funcției main
este frecventă în multe proiecte binare. Drept consecință, comunitatea Rust a elaborat ghiduri pentru descompunerea preocupărilor separate ale unui program binar când funcția main
începe să crească prea mult în dimensiune. Procesul cuprinde următorii pași:
- Împarte programul într-un main.rs și un lib.rs și transferă logica programului în lib.rs.
- Dacă logica de procesare a liniei de comandă este simplă, aceasta poate să rămână în main.rs.
- Când logica de procesare a liniei de comandă devine complexă, extrage-o din main.rs și mut-o în lib.rs.
Responsabilitățile ce ar trebui să rămână în funcția main
în urma acestui proces se limitează la:
- Apelarea logicii de procesare a liniei de comandă cu valorile argumentelor
- Configurarea oricăror setări suplimentare
- Invocarea unei funcții
run
din lib.rs - Tratarea erorii dacă
run
returnează o eroare
Această metodologie se axează pe separarea responsabilităților: main.rs se ocupă de execuția programului, iar lib.rs gestionează integral logica specifică sarcinii. Deoarece funcția main
nu poate fi testată în mod direct, această structură îți oferă posibilitatea să testezi toată logica programului prin includerea ei în funcții din lib.rs. Codul rezidual din main.rs va fi atât de concis încât corectitudinea lui se poate constata prin simpla lectură. Să procedăm la reconfigurarea programului nostru aplicând acești pași.
Extragerea parserului de argumente
Vom separa funcționalitatea de parsare a argumentelor într-o funcție pe care main
o va chema, pregătind astfel mutarea logicii de parsare a argumentelor din linia de comandă în src/lib.rs. Listarea 12-5 ilustrează noul început al funcției main
, unde aceasta apelează o nouă funcție parse_config
, definită momentan în src/main.rs.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {}", query);
println!("In file {}", file_path);
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
Listarea 12-5: Extragerea unei funcții parse_config
din main
Continuăm să adunăm argumentele liniei de comandă într-un vector, dar în loc de a atribui direct valoarea argumentului de la indexul 1 variabilei query
și valoarea argumentului de la indexul 2 variabilei file_path
în funcția main
, acum transmitem întreg vectorul către funcția parse_config
. Funcția parse_config
are acum rolul de a determina care argument corespunde căreia dintre variabile și trimite valorile înapoi în main
. În main
continuăm să creăm variabilele query
și file_path
, însă main
nu mai are rolul de a face corelația dintre argumentele de comandă și variabile.
Această restructurare ar putea să pară inutilă pentru un program de dimensiuni atît de reduse ca al nostru, însă anume așa și refactorizăm: în pași mici și consecutivi. După ce efectezi această modificare, rulează iar programul pentru a te asigura că procesul de parsare a argumentelor funcționează în continuare. E recomandat să verifici progresul frecvent, pentru a putea identifica mai ușor sursa problemelor atunci când apar.
Gruparea valorilor de configurare
Putem face încă un mic pas pentru a îmbunătăți funcția parse_config
.
În prezent, returnăm o tuplă, dar apoi o descompunem imediat în componentele ei individuale. Acest lucru poate semnala faptul că nu am ajuns încă la abstracția corectă.
Alt semnal care indică posibilitatea îmbunătățirii este partea config
din parse_config
, ce sugerează că cele două valori returnate sunt interconectate și constituie împreună o configurație unitară. În momentul de față, nu comunicăm acest sens în structura datelor, decât prin gruparea valorilor într-o tuplă; în loc de aceasta, vom încorpora cele două valori într-o structură și le vom atribui câmpurilor nume sugestive. Aceasta va simplifica munca viitorilor dezvoltatori care vor interacționa cu codul, facilitând înțelegerea interdependenței valorilor și scopurilor pe care le servesc.
Listarea 12-6 prezintă îmbunătățirile aduse funcției parse_config
.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
Listarea 12-6: Refactorizarea funcției parse_config
pentru returnarea unei instanțe a structurii Config
Am introdus o structură denumită Config
, concepută cu câmpurile query
și file_path
. Semnătura funcției parse_config
reflectă acum faptul că se returnează o valoare de tip Config
. În conținutul parse_config
, acolo unde înainte returnam secțiuni care făceau referire la valori de tip String
din args
, configurăm acum Config
astfel încât să conțină în directă posesiune valorile sale String
. Variabila args
din funcția main
este proprietara valorilor argumente și permite funcției parse_config
doar să le împrumute, iar dacă Config
ar încerca să preia controlul asupra acestor valori atunci situația ar contraveni regulilor Rust privind împrumutul.
Există multiple metode prin care am putea controla datele de tip String
; cea mai simplă, deși posibil ineficientă, este să apelăm metoda clone
pe respectivele valori. Acest lucru ar genera o copie integrală a datelor, pe care instanța Config
le-ar deține, consumând mai mult timp și memorie decât dacă am stoca o referință la datele string. Totuși, clonarea datelor are avantajul de a ne simplifica codul, nefiind necesar să gestionăm duratele de viață ale referințelor. În această situație, renunțarea la o cantitate mică de performanță în schimbul simplificării este un compromis justificat.
Dezavantajele utilizării funcției
clone
Mulți dintre cei care programează în Rust au tendința de a evita utilizarea funcției
clone
pentru a soluționa problemele de posesiune, din cauza impactului pe care îl are asupra timpului de execuție. În Capitolul 13, vei învăța metode mai eficiente pentru aceste tipuri de situații. Totuși, în acest moment, nu este o problemă să copiezi câteva string-uri pentru a avansa în progresul tău, deoarece aceste copieri se vor face doar o singură dată și atât string-ul de interogare, cât și calea directoriului tău sunt destul de reduse ca dimensiune. Este preferabil să ai un program funcțional care nu este optimizat la maximum decât să încerci să optimizezi excesiv codul din prima încercare. Pe măsură ce vei deveni mai versat în Rust, va fi mai simplu să pornești direct cu soluția cea mai eficientă, dar pentru moment, este complet acceptabil să apelezi laclone
.
Am modificat funcția main
astfel încât să atribuie instanța de Config
întoarsă de parse_config
unei variabile denumite config
, și am actualizat codul care anterior utiliza variabilele query
și file_path
separat, astfel încât acum să acceseze câmpurile structurii Config
.
În acest fel, codul nostru comunică mai eficient faptul că query
și file_path
sunt corelate și că funcția lor este de a seta configurarea pentru comportamentul programului. Orice porțiune de cod care le folosește va ști că trebuie să le caute în instanța config
, în câmpurile cu numele corespunzător scopului lor.
Crearea unui constructor pentru Config
Până acum, am separat logica de parsare a argumentelor liniei de comandă din main
și am inclus-o în funcția parse_config
. Acest demers ne-a ajutat să observăm că valorile pentru query
și file_path
sunt interconectate și această conexiune trebuie evidențiată în codul nostru. În consecință, am introdus structura Config
pentru a numi scopul conex al query
și file_path
și pentru a putea returna numele acestor valori sub forma unor nume de câmpuri ale structurii din cadrul funcției parse_config
.
Așadar, fiindcă rolul funcției parse_config
este de a inițializa o instanță Config
, putem transforma parse_config
dintr-o funcție obișnuită într-o funcție numită new
, asociată acum structurii Config
. Prin această modificare, codul nostru va deveni mai idiomatic. Putem crea instanțe ale tipurilor din biblioteca standard, cum ar fi String
, invocând String::new
. În mod similar, prin schimbarea funcției parse_config
în new
, asociată cu Config
, vom putea crea instanțe de Config
invocând Config::new
. Listarea 12-7 ilustrează schimbările pe care trebuie să le efectuăm.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
Listarea 12-7: Transformarea parse_config
în Config::new
Am actualizat locul din main
unde parse_config
era apelată, pentru a folosi în schimb Config::new
. Am redenumit parse_config
în new
și am transferat-o într-un bloc impl
, asociind astfel funcția new
cu structura Config
. Încearcă să compilezi din nou codul pentru a te asigura că funcționează cum trebuie.
Remediind problemele de tratare a erorilor
Acum ne vom concentra pe îmbunătățirea gestionării erorilor. Reamintim că încercarea de a accesa valorile din vectorul args
la indexul 1 sau 2 poate provoca panică în program dacă vectorul are mai puțin de trei elemente. Încercând rularea programului fără niciun argument; ieșirea va arăta în felul următor:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Îmbunătățind mesajul de eroare
În Listarea 12-8, includem o verificare în funcția new
ce se asigură că secțiunea este destul de lungă înainte de a accesa indecșii 1 și 2. Dacă secțiunea nu este suficientă, programul va intra în panică și va afișa un mesaj de eroare îmbunătățit.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
Listarea 12-8: Adăugând o verificare pentru numărul de argumente
Acest cod este similar cu funcția Guess::new
pe care am creat-o în Listarea 9-13]ch9-custom-types, unde am folosit macro-ul panic!
când valoarea value
era în afara limitei valorilor valide. Aici, în loc de verificarea unui interval de valori, ne asigurăm că lungimea args
este cel puțin 3, iar restul funcției poate opera sub presupunerea că această condiție este îndeplinită. Dacă args
are mai puțin de trei elemente, această condiție este verificată și se apelează macro-ul panic!
pentru a opri imediat programul.
Cu aceste câteva linii de cod suplimentare în new
, să consultăm din nou rularea programului fără argumente pentru a vedea cum arată eroarea acum:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Rezultatul este mai bun: în sfîrșit avem un mesaj de eroare adecvat. Totuși, rămânem cu informații suplimentare care nu sunt necesare utilizatorilor noștri. Se pare că metoda utilizată în Listarea 9-13 nu este cea mai potrivită aici: un apel la panic!
e mai adecvat pentru o problemă de programare decât una de utilizare, așa cum am discutat în Capitolul 9. În schimb, vom aplica o altă metodă pe care ai învățat-o în Capitolul 9—returnarea unui Result
care indică fie succesul, fie o eroare.
Returnarea unui Result
în loc de apelarea panic!
Putem opta pentru returnarea unei valori Result
care va include o instanță Config
în cazul unui succes și va detalia problema în situația unei erori. Intenționăm să schimbăm numele funcției din new
în build
, deoarece mulți programatori presupun că funcțiile new
nu ar trebui să eșueze niciodată. În comunicarea dintre Config::build
și main
, folosim tipul Result
pentru a semnala potențiala apariție a unei probleme. Astfel, putem ajusta funcția main
să convertească o variantă Err
într-o eroare mai prietenoasă pentru utilizatorii noștri, fără textul suplimentar legat de thread 'main'
și RUST_BACKTRACE
care însoțește apelul la panic!
.
Listarea 12-9 prezintă modificările necesare valorii de retur a funcției denumite acum Config::build
și ale corpului acesteia necesare pentru a returna un Result
. Notăm că aceste schimbări nu vor compila decât după ce actualizăm și funcția main
, lucru pe care îl vom face în lista următoare.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Listarea 12-9: Returnarea unui Result
din Config::build
Funcția build
returnează un Result
care conține o instanță Config
în caz de succes și un &'static str
în caz de eroare, valorile de eroare fiind întotdeauna literali de string cu durata de viață 'static
.
Am efectuat două schimbări în corpul funcției: în loc să folosim panic!
când utilizatorul nu furnizează suficiente argumente, acum returnăm o valoare Err
și am ambalat valoarea de retur Config
într-un Ok
. Aceste ajustări asigură conformitatea funcției cu noua sa semnătură de tip.
Returnând o valoare Err
în cadrul Config::build
, funcția main
poate gestiona valoarea Result
returnată din build
și poate încheia procesul într-o manieră mai curată în situația unei erori.
Apelarea Config::build
și gestionarea erorilor
Pentru a gestiona cazul de eroare și a afișa un mesaj prietenos pentru utilizatori, trebuie să actualizăm funcția main
pentru a prelucra Result
returnat de Config::build
, așa cum e demonstrat în Listarea 12-10. De asemenea, ne asumăm responsabilitatea de a încheia execuția instrumentului de linie de comandă cu un cod de eroare non-zero, rol care anterior îl avea panic!
, și implementăm această funcție manual. Un cod de ieșire non-zero este o convenție care semnalează procesului ce a invocat programul nostru că acesta s-a terminat cu o stare de eroare.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
use std::process;
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);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Listarea 12-10: Terminarea execuției cu un cod de eroare dacă inițializarea unui Config
nu reușește
În această listare am folosit metoda unwrap_or_else
, care nu a fost încă detaliată: unwrap_or_else
este definită pentru Result<T, E>
de către biblioteca standard. Prin utilizarea unwrap_or_else
, definim un comportament personalizat pentru gestionarea erorilor, care nu recurge la panic!
. Dacă Result
este o valoare Ok
, metoda funcționează similar cu unwrap
, returnând valoarea din interiorul Ok
. În schimb, dacă avem o valoare de tip Err
, metoda invocă codul specificat într-o închidere (closure), adică o funcție anonimă definită de noi și pasată ca argument la unwrap_or_else
. Vom aborda închiderile în detaliu în Capitolul 13. Deocamdată, este suficient să înțelegeți că unwrap_or_else
va trimite valoarea internă a erorii Err
, în acest caz șirul static "not enough arguments"
, către închiderea noastră prin intermediul argumentului err
, care este plasat între barele verticale. Astfel, închiderea poate folosi variabila err
atunci când se execută.
Am adăugat un nou rând use
pentru a include process
din biblioteca standard în domeniul de vizibilitate. Codul din închidere care va fi executat în caz de eroare conține doar două linii: afișăm valoarea err
și invocăm process::exit
. Funcția process::exit
oprește programul imediat și returnează numărul care a fost specificat ca cod de ieșire. Acesta este un comportament similar cu cel din gestionarea bazată pe panic!
prezentată în Listarea 12-8, însă de data aceasta fără output-ul adițional. Acum să testăm:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
Excelent! Acest mesaj este mult mai accesibil utilizatorilor noștri.
Extragerea logicii din main
Odată finalizată refactorizarea analizei configurației, ne îndreptăm acum spre logica programului. Conform celor stabilite în „Separarea problemelor pentru proiectele binare”, extragem o funcție denumită run
care va cuprinde toată logica existentă în funcția main
, cu excepția părților care se ocupă de configurarea inițială sau de gestionarea erorilor. La finalizare, main
va fi concisă și ușor de verificat printr-o simplă inspecție, iar noi vom fi capabili să scriem teste pentru întreaga logică restantă.
Listarea 12-11 ilustrează procesul de extracție a funcției run
. Pentru moment, aceasta reprezintă un pas mic și gradual în îmbunătățirea codului. Funcția continuă să fie definită în src/main.rs.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Listarea 12-11: Extragerea funcției run
, care conține restul logicii din program
Funcția run
încorporează acum toată logica rămasă din main
, începând de la etapa de citire a fișierului. Aceasta primește o instanță de Config
ca parametru.
Returnarea erorilor de către funcția run
Cu partea rămasă de logică a programului separată acum în funcția run
, avem oportunitatea de a îmbunătăți gestionarea erorilor, așa cum am procedat pentru Config::build
în Listarea 12-9. În loc să lăsăm programul să genereze panică prin apelarea expect
, funcția run
va returna un Result<T, E>
când ceva nu funcționează corect. Aceasta ne va da posibilitatea de a centraliza mai eficient gestionarea erorilor în main
, într-un mod accesibil utilizatorului. Listarea 12-12 prezintă modificările necesare la semnătura și corpul funcției run
.
Numele fișierului: src/main.rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
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);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Listarea 12-12: Modificarea funcției run
pentru a returna Result
Am realizat trei schimbări importante aici. Prima, schimbarea tipului de retur pentru funcția run
în Result<(), Box<dyn Error>>
. Inițial, această funcție returna tipul unit ()
, pe care acum îl păstrăm ca valoare returnată în cazul de succes Ok
.
Ca tip de eroare, am utilizat obiectul-trăsătură Box<dyn Error>
(și am importat std::error::Error
în context cu o directivă use
la începutul fișierului). Vom discuta despre obiecte-trăsătură în Capitolul 17. Deocamdată, e de ajuns să știm că Box<dyn Error>
înseamnă că funcția va returna un tip care implementează trăsătura Error
, fără a specifica tipul exact al valorii de retur. Aceasta oferă flexibilitatea de a returna diferite valori ale erorilor în diferite scenarii de eroare. Cuvântul dyn
este o abreviere pentru „dinamic” (dynamic).
În al doilea rând, am înlocuit apelul la expect
cu operatorul ?
, despre care am discutat în Capitolul 9. În loc să provoace panic!
atunci când întâlnește o eroare, ?
va returna valoarea erorii din funcția actuală pentru ca apelantul să o poată gestiona.
În al treilea rând, funcția run
returnează acum o valoare Ok
în caz de reușită. În semnătura ei, am declarat tipul de succes al funcției run
ca fiind ()
, ceea ce ne obligă să închidem valoarea tipului unit într-o valoare Ok
. Sintagma Ok(())
poate părea inițial ciudată, dar utilizarea ()
în acest fel este modul convențional de a semnala că apelăm run
doar pentru efectele sale secundare; nu are o valoare de retur relevantă pentru noi.
La rularea acestui cod, compilarea se va efectua, dar va afișa un avertisment:
$ cargo run the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Rust ne avertizează că am ignorat valoarea Result
și că această valoare Result
ar putea semnala că s-a produs o eroare. Nu am verificat însă dacă există sau nu o astfel de eroare, iar compilatorul ne atenționează că, probabil, intenționăm să adăugăm ceva cod pentru gestionarea erorilor! Să corectăm acum acest aspect.
Tratarea erorilor returnate de run
în funcția main
Detectăm erorile și le gestionăm printr-o metodă similară cu cea utilizată anterior pentru Config::build
, în Listarea 12-10, dar cu o subtilă deosebire:
Numele fișierului: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Optăm pentru if let
în defavoarea lui unwrap_or_else
atunci când verificăm dacă run
dă o valoare de tip Err
și invocăm process::exit(1)
în acest caz. Funcția run
nu returnează o valoare pe care am vrea să-i facem unwrap
, spre deosebire de ceea ce se întâmplă cu Config::build
, care ne oferă instanța Config
. Deoarece run
returnează ()
când operează cu succes, suntem interesați exclusiv de depistarea unei erori și, prin urmare, nu avem nevoie de unwrap_or_else
pentru a extrage valoarea neîmpachetată, care ar fi de altfel doar ()
.
Procedura funcțiilor if let
și unwrap_or_else
este la fel în ambele contexte: afișăm mesajul de eroare și terminăm execuția.
Divizarea codului într-un crate de tip bibliotecă
Proiectul nostru minigrep
se prezintă excelent până acum! Urmează să împărțim conținutul fișierului src/main.rs și să transferăm unele porțiuni de cod în fișierul src/lib.rs. Astfel, vom putea testa codul și vom avea un fișier src/main.rs cu responsabilități reduse.
Să transferăm toate părțile de cod care nu sunt funcția main
din src/main.rs în src/lib.rs:
- Definiția funcției
run
- Declarațiile
use
aplicabile - Definiția structurii
Config
- Definiția metodei
Config::build
Fișierul src/lib.rs ar trebui să conțină semnăturile ilustrate în Listarea 12-13 (am omis corpurile funcțiilor pentru rezum). Atenție, acest cod nu va compila până nu facem modificările necesare în src/main.rs, așa cum este indicat în Listarea 12-14.
Numele fișierului: 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> {
// --snip--
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>> {
// --snip--
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
Listarea 12-13: Transferul lui Config
și run
în src/lib.rs
Am folosit generos cuvântul pub
: la structura Config
, la câmpurile și metoda sa build
, precum și la funcția run
. De acum, dispunem de un crate de tip bibliotecă cu un API public ce poate fi testat!
Trebuie acum să aducem în domeniul de vizibilitate al crate-ului binar din src/main.rs codul pe care l-am mutat în src/lib.rs, după cum este ilustrat în Listarea 12-14.
Numele fișierului: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = minigrep::run(config) {
// --snip--
println!("Application error: {e}");
process::exit(1);
}
}
Listarea 12-14: Folosirea crate-ului de tip bibliotecă minigrep
în src/main.rs
Introducem linia use minigrep::Config
pentru a aduce tipul Config
din crate-ul de tip bibliotecă în sfera de accesibilitate a crate-ului binar și prefixăm funcția run
cu numele crate-ului nostru. Tot ansamblul de funcționalități ar trebui să fie acum interconectat și să funcționeze corect. Execută programul cu cargo run
pentru a te asigura că totul merge bine.
Ce muncă intensivă! Dar prin aceasta ne-am pregătit pentru succes pe termen lung. Acum este considerabil mai simplu să gestionăm erorile și am modularizat codul. De acum înainte, majoritatea activității noastre se va concentra în src/lib.rs.
Să profităm de această modularitate nou dobândită prin executarea unei sarcini care ar fi fost complicată cu codul vechi dar este simplă cu noul cod: să scriem niște teste!