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 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:
&mut T
to &T
Un'analoga conversione si ha per rimuovere la mutabilità da un puntatore grezzo:
*mut T
to *const T
I riferimenti possono anche essere forzati in puntatori grezzi:
&T
to *const T
&mut T
to *mut T
Si possono definire delle forzature personalizzate usando
Deref
.
Le forzature sono transitive.
as
La parola-chiave as
esegue una conversione sicura:
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
).
Una conversione e as U
è valida se e
ha tipo T
e T
può essere forzato
a U
.
Una conversione e as U
è pure valida in ognuno dei seguenti casi:
e
è di tipo T
, e T
e U
sono tipi numerici qualunque; numeric-caste
è un enum in stile C (cioò senza dati allegati alle varianti),
e U
è un tipo intero; enum-caste
è di tipo bool
o char
, e U
è un tipo intero; prim-int-caste
è di tipo u8
, e U
è char
; u8-char-castPer 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:
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:
e
è di tipo *T
, U
è *U_0
, e o vale U_0: Sized
o
vale unsize_kind(T) == unsize_kind(U_0)
; è un ptr-ptr-cast
e
è di tipo *T
e U
è un tipo numerico, mentre vale T: Sized
;
è un ptr-addr-cast
e
è un intero e U
è *U_0
, mentre vale U_0: Sized
; è un addr-ptr-cast
e
è di tipo &[T; n]
e U
è *const T
; è un array-ptr-cast
e
è un puntatore a funzione e U
è *T
,
mentre T: Sized
; è un fptr-ptr-cast
e
è un puntatore a funzione e U
è un tipo intero; è un fptr-addr-cast
transmute
as
consente solamente conversioni sicure, e per esempio respngerà
un tentativo di convertire quattro bye in 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 } }
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:
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!