|
La gestione dell'input è abbastanza laboriosa, detto sinceramente. Rispetto ad altri linguaggi ci sono un po' di particolarità, d'altronde, lo dico e lo ripeto, Rust è Rust. In questo paragrafo analizzeremo più a fondo qualcosa che abbiamo già incontrato nei nostri precedenti esempi, anche in questo caso preferendo, come nel paragrafo precedente, un taglio pratico. Iniziamo dalla gestione dell'input, ovvero, caso base, quando volete inserire input dalla tastiera: Il primo step è importare il modulo per la gestione dell'input / output ovvero std::io. use std::io questo modulo contiene tutto quanto vi può servire per gestire l'input e l'output. il secondo passo è definire la variabile in cui memorizzare l'input: let mut input = String::new(); il terzo passo è leggere l'input e questo viene fatto tramite read_line(), Quest'ultimo è un metodo definito come segue: pub fn read_line(&self, buf: &mut String) -> Result<usize> Vediamo cosa significa questa definizione:
A questo punto abbiamo tutto quello che serve e l'istruzione è la seguente io::stdin().read_line(&mut input).expect("Errore nella lettura dell'input"); Avendo un Result come output peeferiamo usare Expect() come metodo per gestire meglio eventuali errori, in ossequio a quanto visto nel capitolo dedicato Result stesso. Questo modo di gestire l'input può essere un po' pesante rispetto alle semplici istruzioni che si incontrano in altri linguaggi. Tuttavia la finalità di questo sistema è di garantire sicurezza e coerente gestione dell'input. Possiamo quindi vedere un piccolo programma di prova con i commenti che riprendono in parte quanto detto:
La riga 11 presenta il metodo trim() che applichiamo sulle stringhe e che non abbiamo visto nel capitolo dedicato alle stringhe stesse. Serve ad eliminare eventuali caratteri bianchi o newline agli estremi della stringa. Questo è utile in particolare quando elaborate stringhe che dovrete convertire in numeri, questo vale in generale. Aggiungo, già che ci siamo, che esistono anche trim_start() e trim_end() che lavorano solo all'inizio ed alla fine della stringa rispettivamente. Il seguente programma recupera in maniera dettagliata le informazioni lette in input, ovvero la stringa, il numero di byte letti e ogni singolo codice ASCII dell'input:
Ad esempio, se a questo programma date da gestire la stringa "ciao" l'output sarà:
in cui gli ultimi due sono i classici CR+LF. A questo punto affrontiamo schematicamente un problema pratico molto comune ovvero il passaggio da stringa in input al formato numerico. Propongo il seguente semplice programma per comodità, commentandolo all'interno così che sia immediatamente chiaro il funzionamento.
Facciamo uso del metodo parse che restituisce, come immaginerete dalla lettura del codice, un Result. Sempre a titolo di esempio pratico, ripetendo concetti già espressi, propongo il programma successivo che assegna il numero letto ad una variabile, sono un fanatico del "repetita juvant":
Altro aspetto interessante riguarda le macro di stampa print! e println!. La prima stampa (o meglio dovrebbe...) il contenuto della stringa ad essa associata mentre println! fa la stessa cosa ma aggiunge un carattere di "a capo". Sembra che tutto sia uguale, a parte la questione del "a capo" ma non è proprio così. Provate infatti ad eseguire il seguente codice:
fn main() {
println!("Inserisci il tuo nome: ");
let mut input = String::new();
std::io::stdin().read_line(&mut input)
.expect("Errore nella lettura dell'input");
let input = input.trim();
print!("ciao, {}", input);
}
usando prima println! e poi print!. Noterete che nel primo caso println! scrive "Inserisci il tuo nome: " e va a capo, nel secondo print! non stampa nulla fino a che non viene dato un input e un invio (o comunque un invio). Il punto è che println! avendo il suo carattere di "a capo" diciamo in bundle, vede il proprio buffer svuotato automaticamente e quindi esposto a video (autoflush). Al contrario print! non ha questa aggiunta e quindi il buffer deve essere svuotato forzatamente in modo esplicito oppure quando il programma termina. Se si è in attesa di un qualche input pertanto print! da solo non stampa nulla. Per ovviare a questo problema si può fare come segue:
use std::io::{self, Write};
fn main() {
print!("Inserisci il tuo nome: ");
io::stdout().flush().expect("Errore nel flush del
buffer");
let mut input = String::new();
std::io::stdin().read_line(&mut input)
.expect("Errore nella lettura dell'input");
let input = input.trim();
print!("ciao, {}", input);
}
E vedrete che tutto funziona. Il cuore di tutto è evidentemente nella quarta riga. io::stdout().flush(): Questo metodo, flush(), forza lo svuotamento del buffer di stdout, assicurando che tutto l'output venga visualizzato immediatamente. Piùi n generale si può dire è utilizzato per forzare l'invio di qualsiasi dato ancora in sospeso nel buffer di scrittura di un oggetto Write (come un file o lo standard output) al suo destinatario finale. flush() restituisce un Result, quindi è buona pratica gestire eventuali errori con expect, come sempre. Per quanto risulti utile è bene non abusarne in quanto appesantisce le prestazioni. Di seguito mostro un breve esempio di uso di flush nella scrittura su file, integrando un po' quanto visto nel precedente paragrafo (crea un file esempio.txt senza guardare se esiste già. Attenzione):
BufWriter serve come buffer interno per scrivere blocchi grandi... in linea di massima qui non servirebbe. Un altro trait che potrà essere utile è Seek. Questo permette di spostare il cursore di lettura/scrittura in un file (o in altri tipi di stream supportati) a una posizione specifica ed è utile quindi per accedere o modificare parti specifiche di un file senza doverlo leggere o scrivere sequenzialmente. Il metodo principale di questo trait è: fn seek(&mut self, pos: SeekFrom) -> Result<u64>; Vediamo un esempio che apre in lettura un file che si aaa.txt che ha il seguente contenuto: 1234567890abcdefg
La riga 7 imposta il cursore sul di 10 byte dall'inizio e la riga 9 crea un array di 5 elementi di cui verrà stampato in sostanza il codice ASCII che li rappresenta. Il metodo read_exact legge esattamente il numero di byte specificato nel suo argomento. L'output quindi è:
che rappresenta i caratteri da 'a' ad 'e' Seek_from è un enumerativo che è così definito: pub enum SeekFrom { Start(u64), End(i64), Current(i64), } nell'ambito del quale le 3 varianti indicano rispettivamente - la partenza in byte - la posizione finale + un dato numero di byte - la posizione corrente + un dato numero di byte Vediamo un altro esempio che scrive alla fine del file, metodo alternativo a quanto già visto:
Questo programma aggiunge in coda al file aaa.txt la stringa "nuovo contenuto". Per completezza operativa ecco un esempio che scrive la stringa "Hello" in un punto preciso del file:
Chiudiamo questo paragrafo vedendo come gestire gli argomenti dalla linea di comando. Il modo più immediato è richiamare la std::env (dove env ovviamente richiama la parola environment) modulo che peraltro integra numerose funzioni per quanto riguarda l'ambiente operativo. Nel nostro caso lo usiamo, come detto, per gestire gli argomenti che possono affiancare il richiamo del programma da linea di comando. Esempio classico:
Gli argomenti a riga di comando vengono memorizzati in un Vec alla riga 4 e da qui possono essere recuperati tramite indice. Da notare che all'indice 0 c'è il nome del programma stesso. La trattazione in questo caso non è diversa da quella che incontrate in altri linguaggi. Il discorso potrebbe essere allargato alle operazioni di I/O asincrone, problematica gestita egregiamente tramite le librerie tokio e async_std e che permette di superare le limitazioni, il collo di bottiglia, costituito a volte dalle operazioni sincrone ma è un discorso avanzato che tratteremo più avanti. |