Conversione fra tipi

Rust, concentrandosi sulla sicurezza, fornisce due diversi modi di convertire un valore in un valore di tipo diverso. Il primo, as, è per conversioni sicure. Invece, transmute consente conversioni arbitrarie, ed è una delle caratteristiche più pericolose di Rust!

La forzatura

La forzatura tra tipi è implicita e non ha una sua sintassi, ma può essere esplicitata usando as.

La forzatura può avvenire nelle istruzioni let, const, e static; negli argomenti delle chiamate di funzione; nei valori dei campi nell'inizializzazione di struct; e nei risultati di funzioni.

Il caso più tipico di forzatura è la rimozione della mutabilità da un riferimento:

Un'analoga conversione si ha per rimuovere la mutabilità da un puntatore grezzo:

I riferimenti possono anche essere forzati in puntatori grezzi:

Si possono definire delle forzature personalizzate usando Deref.

Le forzature sono transitive.

as

La parola-chiave as esegue una conversione sicura:

fn main() { let x: i32 = 5; let y = x as i64; }
let x: i32 = 5;

let y = x as i64;

Ci sono tre categorie principali di conversioni sicure: le forzature esplicite, le conversioni tra tipi numerici, e le conversioni tra puntatori.

Le conversioni non sono transitive: anche se e as U1 as U2 è un'espressione valida, e as U2 non lo è necessariamente (di fatto è valida solamente se U1 può essere forzata a U2).

Forzature esplicite

Una conversione e as U è valida se e ha tipo T e T può essere forzato a U.

Conversioni numeriche

Una conversione e as U è pure valida in ognuno dei seguenti casi:

Per esempio

fn main() { let one = true as u8; let at_sign = 64 as char; let two_hundred = -56i8 as u8; }
let one = true as u8;
let at_sign = 64 as char;
let two_hundred = -56i8 as u8;

La semantica delle conversioni numeriche è la seguente:

Conversione di puntatori

Forse sorprenderà qualcuno, ma è sicuro convertire puntatori grezzi in interi e interi in puntatori grezzi, e convertire fra puntatori che puntano a tipi diversi pur di rispettare alcuni vincoli. È insicuro solamente dereferenziare il puntatore:

fn main() { let a = 300 as *const char; // un puntatore alla posizione 300 let b = a as u32; }
let a = 300 as *const char; // un puntatore alla posizione 300
let b = a as u32;

e as U è una valida conversione di puntatore in ognuno dei seguenti casi:

La funzione transmute

as consente solamente conversioni sicure, e per esempio respngerà un tentativo di convertire quattro bye in un u32:

fn main() { let a = [0u8, 0u8, 0u8, 0u8]; let b = a as u32; // quattro u8 fanno un u32 }
let a = [0u8, 0u8, 0u8, 0u8];

let b = a as u32; // quattro u8 fanno un u32

Ciò produrrà questo errore:

error: non-scalar cast: `[u8; 4]` as `u32`
let b = a as u32; // quattro u8 fanno un u32
        ^~~~~~~~

Questa è una conversione ’non scalare’, perché qui abbiamo più valori: i quattro elementi dell'array. Questi generi di conversioni sono molto pericolose, perché fanno assunzioni sul modo in cui più strutture soggiacenti sono implementate. Per questo, ci serve qualcosa di più pericoloso.

La funzione transmute è fornita da un intrinseco del compilatore, e quello che fa è molto semplice, ma molto pauroso. Dice a Rust di trattare un valore di un tipo come se fosse di un altro tipo. Lo fa senza riguardo per il sistema di verifica dei tipi, e si fida completamente del programmatore.

Nell'esempio precedente, sappiamo che un array di quattro u8 rappresenta appropriatamente un u32, e quindi vogliamo fare la conversione. Usando transmute invece di as, Rust ce lo lascia fare:

use std::mem; fn main() { unsafe { let a = [0u8, 1u8, 0u8, 0u8]; let b = mem::transmute::<[u8; 4], u32>(a); println!("{}", b); // 256 // o, più concisamente: let c: u32 = mem::transmute(a); println!("{}", c); // 256 } }
use std::mem;

fn main() {
    unsafe {
        let a = [0u8, 1u8, 0u8, 0u8];
        let b = mem::transmute::<[u8; 4], u32>(a);
        println!("{}", b); // 256
        // o, più concisamente:
        let c: u32 = mem::transmute(a);
        println!("{}", c); // 256
    }
}

Dobbiamo avvolgere l'operazione in un blocco unsafe affinché compili con successo. Tecnicamente, solamente la chiamata mem::transmute stessa ha bisogno di essere nel blocco, ma in questo caso è carino racchiudere tutte le cose correlate, così da saper dove guardare. In questo caso, anche i dettagli su a sono importanti, e quindi sono nel blocco. Capiterà di vedere del codice in entrambi gli stili; talvolta il contesto è troppo lontano, e avvolgere tutto il codice in unsafe non è un'ottima idea.

Per quanto transmute faccia pochissime verifiche, almeno si assicura che i tipi siano della stessa dimensione. Questa codice:

fn main() { use std::mem; unsafe { let a = [0u8, 0u8, 0u8, 0u8]; let b = mem::transmute::<[u8; 4], u64>(a); } }
use std::mem;

unsafe {
    let a = [0u8, 0u8, 0u8, 0u8];

    let b = mem::transmute::<[u8; 4], u64>(a);
}

dà il seguente errore:

error: transmute called with differently sized types: [u8; 4] (32 bits) to u64
(64 bits)

A parte quello, ci si deve arrangiare!