Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 37
Input - output

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:
  • pub sta ad indicare che la funzione è pubblica cioè può essere utilizzata fuori dal modulo in cui è definita.
  • fn va beh, lo sappiamo definisce una funzione
  • read_line è il nome della funzione
  • Seguono poi i parametri della funzione.
    &self: Indica che questa funzione è un metodo e deve essere chiamato su un’istanza. In questo caso, self si riferisce tipicamente a un’istanza di std::io::Stdin, che rappresenta lo standard input.
    buf: È il nome del secondo parametro.
    &mut String: Indica che buf è un riferimento mutabile a una String. Questo significa che read_line può modificare il contenuto della stringa passata.
  • La funzione restituisce un Result che può contenere o un valore di tipo usize ovvero la lunghezza della stringa in caso di successo oppure un errore, come noto.

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:

  Esempio 37.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::io;

fn main() {
// Crea una nuova stringa per memorizzare l'input
    let mut input = String::new();
// Stampa un messaggio all'utente
    println!("Inserisci qualcosa:");
// Legge l'input dell'utente
    io::stdin().read_line(&mut input).expect("Errore input");
// Rimuovere eventuali caratteri di newline
    let input = input.trim();
// Stampare l'input ricevuto
    println!("Hai inserito: {}", input);
}

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:

  Esempio 37.2
1
2
3
4
5
6
7
8
9
10
11
use std::io;
fn main() {
    let mut input = String::new();
    let bytes_read = io::stdin().read_line(&mut input)
        .expect("Errore nella lettura dell'input");
    println!("Ho letto {} byte", bytes_read);
    println!("Hai inserito: {}", input);
    for byte in input.bytes() {
        println!("Byte: {}, ASCII: {}", byte, byte as char);
    }
}

Ad esempio, se a questo programma date da gestire la stringa "ciao" l'output sarà:

Ho letto 6 byte
Hai inserito: ciao

Byte: 99, ASCII: c
Byte: 105, ASCII: i
Byte: 97, ASCII: a
Byte: 111, ASCII: o
Byte: 13, ASCII:
Byte: 10, ASCII:

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.

  Esempio 37.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io;
fn main() {
    // Crea una nuova stringa mutabile per memorizzare l'input
    let mut input = String::new();
    // Leggi una linea dall'input standard e gestisci eventuali errori
    io::stdin().read_line(&mut input)
        .expect("Errore nella lettura dell'input");
    // Rimuove eventuali caratteri di newline o spazi bianchi
    let input = input.trim();
    // Tenta di convertire la stringa in un numero intero
    match input.parse::<i32>() {
        Ok(num) => println!("Numero intero: {}", num),
        Err(_) => println!("Non è un numero intero valido"),
    }
    // Tenta di convertire la stringa in un numero con virgola mobile
    match input.parse::<f64>() {
        Ok(num) => println!("Numero con virgola mobile: {}", num),
        Err(_) => println!("Non è un numero con virgola mobile valido"),
    }
}

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":

  Esempio 37.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io;
fn main() {
    // Crea una nuova stringa mutabile per memorizzare l'input
    let mut input = String::new();
    // Leggi una linea dall'input standard e gestisci eventuali errori
    io::stdin().read_line(&mut input)
        .expect("Errore nella lettura dell'input");
    // Rimuovi eventuali caratteri di newline o spazi bianchi
    let input = input.trim();
    // Tenta di convertire la stringa in un numero intero
    let x1: i32 = match input.parse() {
        Ok(num) => num,
        Err(_) => {
            println!("Non è un numero intero valido");
            return;
        }
    };
    // Stampa il valore di x1
    println!("Numero intero assegnato a x1: {}", x1);
}

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):

  Esempio 37.5
1
2
3
4
5
6
7
8
9
10
11
use std::fs::File;
use std::io::{Write, BufWriter};

fn main() {
    let file = File::create("esempio.txt").expect("Impossibile creare il file");
    let mut writer = BufWriter::new(file); // Scrittura bufferizzata
   
    writer.write_all(b"Hello, world!").expect("Errore nella scrittura");
    writer.flush().expect("Errore nel flush"); // Forza la scrittura sul disco
}

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

  Esempio 37.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs::File;
use std::io::{Seek, SeekFrom, Read};
fn main() -> std::io::Result<()> {
    let mut file = File::open("aaa.txt")?;
   
    // Sposta il cursore a 10 byte dall'inizio
    file.seek(SeekFrom::Start(10))?;
   
    let mut buffer = [0; 5]; // Buffer di 5 byte
    file.read_exact(&mut buffer)?;
    println!("Dati letti: {:?}", buffer);
   
    Ok(())
}

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 è:

Dati letti: [97, 98, 99, 100, 101]

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:

  Esempio 37.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
fn main() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .write(true)
        .open("aaa.txt")?;
   
    // Sposta il cursore alla fine del file
    file.seek(SeekFrom::End(0))?;
   
    // Aggiunge "Nuovo contenuto" alla fine del file
    file.write_all(b"Nuovo contenuto")?;
 
    Ok(())
}

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:

  Esempio 37.8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
fn main() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .write(true)
        .open("aaa.txt")?;
   
    // Sposta il cursore a 5 byte dall'inizio
    file.seek(SeekFrom::Start(5))?;
   
    // Scrive "Hello" a partire dalla posizione 5
    file.write_all(b"Hello")?;
   
    Ok(())
}

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:

  Esempio 37.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::env;
fn main() {
    // Argomenti della linea di comando visti un vettore di stringhe
    let args: Vec<String> = env::args().collect();
    // Stampa tutti gli argomenti
    println!("Argomenti della linea di comando: {:?}", args);
    // Controlla se sono stati passati abbastanza argomenti
    if args.len() < 2 {
        println!("Utilizzo: {} <argomento>", args[0]);
        return;
    }
    // Usa il primo argomento (dopo il nome del programma)
    let first_arg = &args[1];
    println!("Primo argomento: {}", first_arg);
}

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.