Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 8
Confronti , conversioni, overflow

Prima di proseguire con lo studio dei tipi ritengo utile inserire il presente paragrafo. Un capitolo come questo, in generale, non si trova su tutti i testi di programmazione. Ma come ho già detto, questo è il mio percorso di apprendimento del linguaggio e l'argomento mi incuriosisce, per quanto riconosca che forse qui è trattato in modo un po' confuso.
Per quanto riguarda gli interi credo non sia il caso di perdere troppo tempo. I classici operatori di confronto fanno pienamente il loro lavoro, vediamo il banale esempio:


fn main() {
    let x = 7;
    let y = 5;
    println!("{}", x > y);
    println!("{}", x >= y);
    println!("{}", x == y + 2);
    println!("{}", y == x - 2);
}

credo non ci sia bisogno di altro, il risultato sarà sempre "true" perchè, anche nel caso di calcoli, non ci sono problemi sotto questo aspettocon gli interi, laddove siano rispettati i range che conosciamo.
Diverso è il discorso quando ci sono di mezzo i numeri con virgola, questo proprio per come vengono rappresentati internamente.
La prima cosa che si ha la tentazione di provare è tramite il classico == ma è una strada molto pericolosa.

let x = 0.3;    
let y = 0.2;    
println!("{}", x == y + 0.01);

questo codice ad esempio vi darà false con ogni probabilità nonostante 0.03 sia uguale a 0.02 + 0.01 nella nostra realtà. Ma per un computer è diverso. Quindi usare l'operatore di uguaglianza non va bene.
Un metodo classico, valido in tutti i linguaggi è quello del confronto con una entità, un numero, piccolo a piacere, purchè sia sufficiente per i nostri scopi. L'esempio seguente segue questa semplice strada:

  Esempio 8.1
1
2
3
4
5
6
7
8
9
const EPSILON: f64 = 1e-10;
fn confronta(a: f64, b: f64, epsilon: f64) -> bool {
    (a - b).abs() < epsilon
}
fn main() {
    let a = 0.1 + 0.2;
    let b = 0.3;
    println!("{}", confronta(a, b, EPSILON)); // Questo stamperà true
}

Abbiamo definito alla riga 1 una costante che rappresenta un valore sufficientemente piccolo per i nostri scopi, e l'abbiamo usata come temine di confronto alla riga 3. Essendo la differenza tra e b minore di quel valore per i nostri scopi sono uguali. Quel "per i nostri scopi" è in grassetto perchè, in qualche caso, non in questo, dovrete valutare voi quale sia il valore adatto a fungere da sentinella. Questo è il modo più classico. Rust ci offre anche un altro sistema, diciamo built-in ma che segue lo stesso principio. Lo illustro step by step e fa uso del tool Cargo. Questa la sequenza di istruzioni:

    1) cargo new floatn (floatn è il nome del mio esempio)
    2) nella directory floatn all'interno del file cargo.toml bisogna aggiungere nella sezione Dependencies la riga:
       
float-cmp = "0.8"
    3) costruiamo l'eseguibile con l'istruzione cargo build dalla directory principale del progetto.

il fulcro ovviamente è il punto 2 mentre a quel punto il codice è il seguente:

use float_cmp::approx_eq;

fn main() {
    let a = 0.1 + 0.2;
    let b = 0.3;
    println!("{}", approx_eq!(f64, a, b, epsilon = 1e-10)); // true
}


CONVERSIONI

il capitolo delle conversioni invece è un po' più complesso perchè espone a dei rischi. Abbiamo già visto nel paragrafo dedicato agli interi il caso in cui un valore fosse riversato il una variabile di un tipo non in grado di accoglierlo, ricordiamo quel codice:

fn main() {
  let x: i32 = 400;
  let y = x as i8;
  println!("{}", y);
}

e qui il nostro 400 diventava -112. Questo è banale:
400 in binario in un i32 è 00000000000000000000000110010000
ma se lo riversiamo su 8 bit diventa: 10010000 che è -112 dato che il primo bit è quello del segno. Chiaramente è un errore pericoloso. Quando si fa una conversione di questo tipo la cosa migliore è utilizzare try_from oppure try_into. Vediamo gli esempi:

  Esempio 8.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::convert::TryInto;
fn main() {
    let x: i32 = 10; // Prova con 10 invece di 400
    let y: Result<i8, _> = x.try_into();
   
    let mut converted_value: i8 = 0; // Dichiaro una variabile mutabile
    match y {
        Ok(value) => {
            converted_value = value;
            println!("Conversione riuscita: {}", converted_value);
        },
        Err(e) => println!("Errore nella conversione: {}", e),
    }
    // Ora possiamo usare converted_value al di fuori del match
    println!("Valore convertito {}", converted_value);
}

oppure usando try_from:

  Esempio 8.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::convert::TryFrom;
fn main() {
    let x: i32 = 10; // Prova con 10 invece di 400
    let y = i8::try_from(x);
   
    let mut converted_value: i8 = 0; // Dichiaro una variabile mutabile
    match y {
        Ok(value) => {
            converted_value = value;
            println!("Conversione riuscita: {}", converted_value);
        },
        Err(e) => println!("Errore nella conversione: {}", e),
    }
    // Ora possiamo usare converted_value al di fuori del match
    println!("Valore convertito {}", converted_value);
}

Si fa uso del modulo convert che a cui appartengono i trait try_into e try_from. Il risultato dell'operazione è un result che espone il risultato della conversione oppure un errore.
Anche il passaggio da intero a float non è esente da problemi:

  Esempio 8.4
1
2
3
4
5
6
7
8
9
10
fn main() {
    let large_int: i64 = 9_007_199_254_740_993; // 2^53 + 1
    let large_float: f64 = large_int as f64;
    let back_to_int: i64 = large_float as i64;
    if large_int != back_to_int {
        println!("Errore! Intero: {}, Dopo conversione: {}", large_int, back_to_int);
    } else {
        println!("Nessuna perdita di precisione. Intero: {}", large_int);
    }
}

una "andata e ritorno" che ci restituisce:

Errore! Intero: 9007199254740993, Dopo conversione: 9007199254740992

Il che può non essere carino....

Avevamo accennato nel capitolo dedicato ai caratteri che la conversione da interno a char potrebbe dare adito a problemi, se usiamo as. Il problema sorge ovviamente a causa dei range differenti e quindi bisogna fare attenzione, usare as è molto comodo ma non è la strada più sicura, Vale la pena ripetere che In Rust, il tipo char rappresenta un singolo carattere Unicode, che può essere qualsiasi valore nel range da 0x0000 a 0x10FFFF. Convertire un intero a char può quindi fallire se il valore intero non rientra in questo range valido. Questo problema sorge ovviamente nella direzione intero -> char perchè viceversa ogni char ha un valore certamente valido nell'ambito degli interi. Per procedere in maniera sicura possiamo usare il metodo char::from_u32 che restituisce un Option. Questo metodo tenta di convertire un u32 a char e restituisce None se il valore non è un carattere Unicode valido. Vediamo l'esempio:

  Esempio 8.5
1
2
3
4
5
6
7
fn main() {
  let code_point: u32 = 65; // Codice ASCII per 'A'
  match char::from_u32(code_point) {
  Some(c) => println!("Il carattere è: {}", c),
  None => println!("Valore fuori dal range valido per un char"),
  }
}

ANALISI DEGLI OVERFLOW

Abbiamo finora analizzato questo problema da un punto di vista generale, al fine di presentare le insidie insite nei calcoli anche più banali. Poi abbiamo visto qualche metodo di base, le operazione checked, per mitigare il problema. Vediamo ora qualche meccansimo un po' più avanzato che Rust ci propone, tenendo presente che, come purtroppo mi tocca spesso dire, sarà bene tornare su questa sezione quando avrete più chiari altri concetti avanzati. Cercherò comunque di spiegare le cose in modo molto basilare.
Innanzitutto il compilatore Rust ci propone due modalità per portare a termine il proprio lavoro:

--- la modalità debug
--- la modalità release

La prima è più pesante in quanto aggiunge codice di verifica interno all'eseguibile generato, la seconda dovrebbe essere utilizzata quando si rilascia il programma nelle versioni finali. Almeno di norma. In realtà questo opzioni sono disponibil via Cargo ma possiamo anche settare vari livelli di controllo agendo direttamente sulle opzioni disponibili per il compilatore. Vediamo il seguente esempio:

  Esempio 8.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::io;
use std::io::prelude::*;
fn main()
{
    let mut x01: i8 = 30;
    print!( "Inserisci un numero: ");
    io::stdout().flush().ok().expect("");
    let mut input01 = String::new();
    io::stdin().read_line(&mut input01);
    let x02: i8 = input01.trim().parse()
    .expect("Ho chiesto un numero: ");
    x01 = x01 * x02;
    println!("{}", x01);
}

e proviamo a compilarlo in due modalità diverse con questi comandi:

1) rustc -C debuginfo=2 -C opt-level=0
2) rustc -C debuginfo=0 -C opt-level=3

vediamo subito che gli eseguibili avranno dimensioni diverse con la seconda modalità che genera un file più pesante in quanto aggiunge informazioni (siamo in debug mode). Ma anche in esecuzione vi sono delle differenze. Poniamo ad esempio che il numero che digitiamo quando ci viene richiesto sia 10 il che genera certamente un overflow (30 x 10) rispetto al tipo i8 e vediamo come rispondono i due programmi:

1)
Inserisci un numero: 10
thread 'main' panicked at 'attempt to multiply with overflow', rs068.rs:12:11
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

2)
Inserisci un numero: 10
44

Come vedete, nel primo caso, con informazioni di debug attivate, il programma è andato in panico, esponendo un errore dovuto all'overflow. Nel secondo caso è uscito fuori un risultato in apparenza assurdo dovuto dal fatto che il numero 300 è stato "wrappato" nell'ambito degli i8. e se vogliamo andare a fondo di questo secondo caso, rammentiamo che il numero 300 in formato binario è:
1 0010 1100
che nel caso di interi a 8 bit esso viene troncato come:
0010 1100
il quale, riportato in ambito decimale, vale 44.

Questo comportamento è decisamente grave evidentemente (vedasi il già citato caso del disastro  dell'Ariane, causato da un problema del genere) e quindi, vale come consiglio generale, prima di andare in fase release bisogna fare molti test in questo senso.

Molto utile il metodo incontrato in precedenza che fa uso delle operazioni checked in grado di intercettare gli overflow. Questo, almeno nella fase iniziale dello studio è, a mio avviso, il metodo più semplice per un neofita del linguaggio per porre una "sentinella", in quanto concettualmente molto lineare. Il vostro codice risulterà un po' più pesante forse ma avrete la certezza che tutto funzioni a dovere. Almeno, io mi sono trovato bene così.

Particolare è il l'uso di overflowing che si può adoperare anche in questo caso per tutte le operazioni di base. Questa funzione espone una ennupla composta da due elementi il risultato wrappato (di nuovo... attenzione) e un valore booleano che esprime true se si è verificato un overflow, false diversamente. Vediamo l'esempio prendendo come input il solito valore 10:

  Esempio 8.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::num::Wrapping;
use std::io;
use std::io::prelude::*;
fn main()
{
    let mut x01: i8 = 30;
    print!( "Inserisci un numero: ");
    io::stdout().flush().ok().expect("");
    let mut input01 = String::new();
    io::stdin().read_line(&mut input01);
    let x02: i8 = input01.trim().parse()
    .expect("Ho chiesto un numero: ");
    x01 = x01.overflowing_mul(x02).0;
    println!("{}", x01.overflowing_mul(x02).1);
    println!("{}", x01);
}

l'ouput è il seguente:

Inserisci un numero: 10
true
44

come si vede l'output numerico è il solito (sballato) 44 ma avete un true che vi suggerisce che si è verificato un overflow. Come detto il nostro overflowing si può applicare a varie tipologie di operazioni ad esempio overflowing_add (somma), oveflowing_div (divisione), ecc...

Anche wrapping "inscatola" i valori originando il risultato scorretto.

  Esempio 8.8
1
2
3
4
5
6
7
fn main()
{
    let x1: i8 = 100;
    let x2: i8 = 100;
    let x3: i8 = x1.wrapping_add(x2);
    println!("{}", x3);
}

E il risultato è 44 anche qui evitando l'overflow..

L'ultimo metodo possibile fa uso del prefisso saturating (quindi saturating_add, saturating_sub, saturating_mul, saturating_div e altre ancora). La finalità, che forse potrete già  intuire è quello di non permettere l'overflow ma di "saturare" la variabile che va oltre i limiti del tipo attribuendogli il valore massimo permesso. Vediamo l'esempio:

  Esempio 8.9
1
2
3
4
5
6
7
8
9
10
11
fn main()
{
    let x: i8 = 120;
    let y: i8 = 110;
    let z1 = x.saturating_add(y);
    println!("{}", z1);
    let t: i32 = 100000;
    let z: i32 = 100000;
    let r1 = t.saturating_mul(z);
    println!("{}", r1);
}

L'output è il seguente:

127
2147483647

ovvero il massimo previsto per i tipi i8 ed i32, invece del previsto overflow. Vi può essere utile questo metodo? Certo è particolare....

Questa presentazione è ovviamente indicativa una vera e propria panoramica che ha valenza in linea generale come presentazione dei metodi disponibili. La scelta dipende dalla impostazione del vostro programma oltre che, naturalmente, dalla conoscenza di altre tecniche di gestione delle situazioni di errore che apprenderemo in seguito.

Una ulteriore possibilità è offerta dalle operazioni checked, ovvero controllate come dice il loro stesso nome. Questo è, a mio avviso, il metodo migliore, quanto meno il più intuitivo, per gestire potenziali situazioni di errore. Vediamo subito un esempio che fa uso di una addizione controllata:

Esempio 8.10
1
2
3
4
5
6
7
8
fn main() {
    let x: i8 = 100;
    let y: i8 = 28;
    match x.checked_add(y) {
        Some(result) => println!("Result: {}", result),
        None => println!("Overflow detected!"),
    }
}

Discorso del tutto analogo per le moltiplicazioni:

  Esempio 8.11
1
2
3
4
5
6
7
8
fn main() {
    let x: i8 = 100;
    let y: i8 = 28;
    match x.checked_mul(y) {
        Some(result) => println!("Result: {}", result),
        None => println!("Overflow detected!"),
    }
}

In entrambi i casi abbiamo un output che ci indica l'overflow.
Questo sistema vale ovviamente anche per altre operazioni, abbiamo anche
 
  -- checked_sub (sottrazione)
  -- checked_div (divisione)
  -- checked_rem (resto della divisione, utile ad esempio in caso divisioni per 0)
  -- checked_neg (inversione del numero)
  -- checked_shr e checked_shl (right e left shift)


Il capitolo non è esaustivo ma dovrebbe aver fornito delle basi, almeno dal punto di vista logico,  per rendere più sicuri i vostri programmi.

to_string()

questo particolare metodo che converte verso il tipo stringa lo conosciamo già e permette di convertire a stringa tutti gli elementi che implementano il trait ToString che è automaticamente implementato da tutti gli elementi che implementano il trait Display. Lo abbiamo visto all'opera con tanti tipi, interi, float ecc... Mostriamo un esempio che funziona su un tipo custom. Consiglio fortemente di tornare su di esso quando avremo visto le struct e l'uso di impl:

  Esempio 8.12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fmt;
struct Point {
    x: i32,
    y: i32,
}
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
fn main() {
    let point = Point { x: 10, y: 20 };
    let point_string = point.to_string();
    println!("{}", point_string); // "(10, 20)"
}