Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Conversioni di tipo con Rust

Rust: analizziamo le modalità di conversione dei dati (o cast) offerte da questo linguaggio di programmazione
Rust: analizziamo le modalità di conversione dei dati (o cast) offerte da questo linguaggio di programmazione
Link copiato negli appunti

Considerando che Rust è un linguaggio fortemente tipizzato, la conversione tra tipi di dato appare come una pratica molto importante, comune e da gestire correttamente. In questa lezione, ci occuperemo proprio di questo argomento. Verificando come anche in questo ambito Rust porti sempre il programmatore su strade sicure, permettendo solo compilazioni che non rischino di dare adito a bug.

Cast in Rust

La conversione di tipo - non solo in Rust ma in tutta l'informatica - prende il nome di cast e permette al programmatore di indicare il tipo di dato di destinazione per un valore incluso in una variabile di tipo diverso. Con il cast si hanno conversioni esplicite dove il programmatore si assume la "responsabilità" dell'operazione.

Cominciamo a ragionare sui numeri. Se provassimo a convertire un intero in un numero in virgola:

let intero:u8 = 20;
let virgola:f32 = intero;

otterremmo il seguente errore:

error[E0308]: mismatched types
    ...
expected `f32`, found `u8`

Rust quindi non ha permesso la conversione nonostante questa non rischiasse di condurre ad un errore in quanto saremmo passati da un numero intero ad uno in virgola (nessun rischio di perdere decimali) con un valore totalmente incluso nel range possibile per il tipo di destinazione. Rust non permette conversioni automatiche. Per poter agire con una conversione da intero a numero in virgola possiamo invece:

  • utilizzare l'operatore as per il cast:

    let virgola:f32 = intero as f32;

  • invocare il metodo into sul valore sorgente:

    let virgola:f32 = intero.into();

Ad esempio, il seguente codice funziona perfettamente e ce lo dimostra anche il fatto che viene richiesto di usare un valore non intero per effettuare la seconda divisione:

let intero:u8 = 20;
let virgola:f32 = intero.into();
println!("intero = {}", intero/3);
println!("in virgola = {}", virgola/3.0);
ed il tutto offre il seguente output:

intero = 6
in virgola = 6.6666665

Rispettare i limiti

Il cast consente di ottenere conversioni e risulta comodo e funzionale un po' in tutti i casi. E' tuttavia importante ricordare che il programmatore autorizza la conversione esplicitamente, quindi alcuni fattori vanno comunque considerati. Come ad esempio i limiti di range. Un u8 è un intero senza segno a 8 bit il cui range si estende pertanto da 0 a 255 (8 bit consentono al massimo 256 combinazioni).

Quindi cosa succederebbe se provassimo a "inserire" un float troppo grande o negativo in un u8? Capiamolo con questo esempio:

let virgola_grande:f32 = 1500.0;
let virgola_negativo:f32 = -1500.0;
let intero_troppo_grande:u8 = virgola_grande as u8;
let intero_negativo:u8 = virgola_negativo as u8;
println!("convertito da {} a {}", virgola_grande, intero_troppo_grande);
println!("convertito da {} a {}", virgola_negativo, intero_negativo);

e questo il risultato in output:

convertito da 1500 a 255
convertito da -1500 a 0

Come si vede, tutto ciò che eccedeva i limiti - o verso l'alto o verso il basso - è stato "tagliato" fermandosi ai valori limite del range consentito dal tipo di dato destinazione: 0 per i numeri negativi, 255 per i valori troppo grandi.

Per effettuare un esperimento, possiamo provare ad usare altre tipologie di interi come destinazione e vedere i risultati. Ad esempio, fossero state le variabili intere di tipo u16 avremmo avuto il valore negativo limitato a comunque 0. Mentre, verso l'alto, potendo 16 bit indirizzare 65536 combinazioni il valore 1500 sarebbe stato accettato.

In questo ambito pertanto si ricordi che la sicurezza non sta nel meccanismo bensì nei valori che permettiamo di transitare durante il cast.

Inoltre, alcune operazioni di cast possono essere svolte in un senso ma non nell'altro come quella tra booleani e interi. Con questo semplice esperimento possiamo, ad esempio, dimostrare che un booleano può diventare un intero:

println!("convertito da {} a {}", true, true as u8);
println!("convertito da {} a {}", false, false as u8);

ottenendo

convertito da true a 1
convertito da false a 0

mentre l'operazione opposta:

let intero:u8 = 10;
let booleano:bool =intero as bool;
println!("convertito da {} a {}", intero, booleano);

restituisce un errore:

error[E0054]: cannot cast `u8` as `bool`

Ciò avviene al contrario di altri linguaggi di programmazione che, in alcuni casi, permettono addirittura l'uso di un valore numerico al posto di un booleano con conversione automatica.

Parsing da stringhe a numeri

Sebbene non si tratti di pure conversioni di tipi di dato, introduciamo qualcosa riguardo al parsing di numeri da stringa ovvero quando abbiamo una stringa contenente un formato tale che può essere convertito in numero. Attività assai comune quando si tratta di analizzare dati.

Le stringhe offrono il metodo parse per svolgere tutto ciò ma essendo il risultato incerto (qualora il numero rappresentato non fosse nel formato corretto o non fosse affatto un numero) viene restituito un elemento di tipo Result sul quale potremo indagare l'eventuale presenza di errori.

Ad esempio, il seguente codice fa il parsing di una stringa contenente un numero cercando di convertirla in un tipo u8:

let numero_in_stringa = "12";
    let risultato: Result<u8, _> = numero_in_stringa.parse();
    match risultato {
        Ok(numero) => println!("Conversione riuscita: {}", numero),
        Err(errore) => println!("Errore: {}", errore),
    }

Trattandosi di un numero positivo e compreso tra 0 e 255 otterremo una conversione di successo. Ma cosa succederebbe se ad esempio impostassimo il valore di numero_in_stringa a -12 o 1500? In entrambi i casi otterremmo errore: nel primo, il messaggio in output

Errore: invalid digit found in string

in quanto il segno meno non sarebbe riconosciuto come numero. Nel secondo, la stampa di

Errore: number too large to fit in target type

quindi qualcosa legato non ai caratteri in senso stretto ma all'informazione convertita.

In definitiva, è interessante vedere che qualsiasi errore collegato al parsing viene, in un modo o nell'altro, convertito in un apposito segnale di risposta. Come abbiamo imparato sugli errori in Rust, per concludere, potremmo evitare di usare il costrutto match sfruttando i metodi unwrap e expect. Questo dipende però dalla situazione che stiamo gestendo.

Ti consigliamo anche