Rust - I caratteri
I caratteri in Rust sono valori scalari Unicode. Essi, nella pratica, rappresentano un singolo carattere e sono identificati all'interno di una coppia di singoli apici. Per cui:
'a' '6' '%' 'T'
sono certamente dei caratteri. Quindi ogni elemento all'interno di una coppia di singoli apici perde in certo senso la sua natura e diventa un carattere. La cifra 6 che trovate in seconda posizione non è più un i32 pertanto ma un carattere. Sotto il cofano i caratteri sono collegati a precisi range numerici in particolare andiamo da 0 a 0x10FFFF incluso. In realtà però questo range non è continuo e, come specificato anche su sito ufficiale, esiste un gap che interrompe la continuità dell'intervallo.
C’è un intervallo di valori riservato, chiamato surrogate range, che non può rappresentare caratteri. Si tratta di elementi UTF-16 che non sono caratteri veri e propri. Rust è molto rigoroso nel non accettare questi valori.
Quel range è:
U+D800 ..= U+DFFF
se cercate di attribuire uno dei valori compresi in questo gap ad una variabile di tipo char il compilatore di arrabbia.
Per quanto riguarda l'occupazione di memoria Rust prevede 4 byte per le variabili di questo tipo. Questo vale su qualsiasi piattaforma. La cosa può sembrare un po' dispendiosa ma in realtà non esistono solo le nostre lettere latine, un po' di simboli e le cifre da 0 a 9, molti caratteri sono veri e propri grafemi la cui rappresentazione necessita di più caratteri appaiati.
La definizione formale di una variabile di tipo char è la consueta:
let c1 = 'a';
let c2: char = 'b';
lasciando fare all'inferenza oppure no. E' anche possibile usare il formato tipicamente Unicode come segue:
'\u{valore}'
ad
esempiolet c4 = '\u{D7FF}';
Per leggere l'occupazione di memoria
del singolo carattere possiamo usare anche qui un semplice programma come
il seguente:
Esempio 8.1
fn main() {
let c1: char = 'a';
let c2: char = '1';
let size_in_bytes = std::mem::size_of_val(&c1);
println!("Occupazione memoria del carattere 'a' = {} byte", size_in_bytes);
let size_in_bytes = std::mem::size_of_val(&c2);
println!("Occupazione memoria del carattere '1' = {} byte", size_in_bytes);
}
E' interessante tuttavia notare che nell'ambito delle stringhe, che sono sequenze di caratteri, come vedremo questo non è sempre vero, anzi. Il legame tra caratteri e numeri è comprovato dal fatto che è immediato passare da carattere a numero tramite cast. E' possibile anche l'inverso ma questa conversione non è considerata sicura a causa ovviamente della differenza di range tra i due tipi. Usando as, la nostra consueta keyword, finora, arma totale (mica tanto sicura però, lo ribadirò sempre...) per le conversioni:
Esempio 8.2
fn main() {
let c1 = 'a';
println!("{}", c1 as i32);
println!("{}", 98 as char);
println!("{}", (char::MAX) as i32);
println!("{}", (char::MIN) as i32);
}
con il seguente output:
| 97 b 1114111 0 |
All'interno del nostro piccolo programma troviamo la costante MAX
che ci indica il limite numerico massimo possibile per il tipo char mentre
MIN ci indica il minimo che poi è semplicemente 0. Le righe
4 e 5 ci mostrano rispettivamente una conversione da carattere verso intero,
come detto sempre possibile ed una da intero verso carattere, che in questo
caso non dà problemi. La conversione da intero verso char la ritroveremo nel
capitolo dedicato alle conversioni e vedremo come le cose possono essere
eseguite in maniera un po' più sicura.
I caratteri dispongono di vari
metodi che, come di consueto, trovate nel sito ufficiale e ce ne sono
davvero tanti. Qui di seguito darò solo un breve esempio per chiarire le
modalità d'uso:
Esempio 8.3
fn main() {
let c1 = '9';
println!("{}", c1.is_digit(10));
println!("{}", c1.is_digit(8));
let c2 = 'A';
println!("{}", c2.is_uppercase());
println!("{}", c2.is_lowercase());
println!("{}", c1.is_ascii_digit());
println!("{}", c2.is_ascii_uppercase());
}
La riga 4 ci restituirà true in quanto 9 è un numero in base 10 mentre la
riga 5 ci restituisce false in quanto 9 non è contemplato in base 8.
Interessante è il metodo from_digit che
che viene utilizzato per convertire un numero (di tipo u32) in un carattere
rappresentante una cifra numerica (cioè '0' - '9') in una determinata base
(radice) che può variare da 2 a 36 mentre sopra il valore 36 il programma va
in panico.
Esempio 8.4
fn main() {
let digit = 7;
let base = 10;
if let Some(character) = char::from_digit(digit, base) {
println!("La cifra {} in base {} -> '{}'", digit, base, character);
} else {
println!("{} non è una cifra valida in base {}", digit, base);
}
let hex_digit = 15;
let hex_base = 16;
if let Some(character) = char::from_digit(hex_digit, hex_base) {
println!("La cifra {} in base {} -> '{}'", hex_digit, hex_base, character);
} else {
println!("{} non è una cifra valida in base {}", hex_digit, hex_base);
}
}
Torniamo un attimo sulla conversione da valori numerici a char.
Anticipiamo un po' il discorso che affronteremo quando parleremo delle
conversioni. Considerando che as, come
detto e ridetto è comodo ma non è la soluzione più solida e sicura il metodo
migliore è ricorrere a try_from().
Infatti:
try_from controlla
automaticamente che il valore intero:
1) sia un punto di
codice Unicode valido
2) non cada nel surrogate range
3) rientri nel range massimo (≤ 0x10FFFF)
Se qualcosa
non va, restituisce un errore, non un valore corrotto.
Vediamo un semplice esempio:
use std::convert::TryFrom;
fn main() {
let n: u32 = 65;
let c =
char::try_from(n).unwrap();
println!("{}", c); // A
}
Se vogliamo un esempio con gestione dell'errore, eccolo qua:
use std::convert::TryFrom;
fn main() {
let n: u32 = 0xD800; //
dentro il surrogate range
match char::try_from(n) {
Ok(c) =>
println!("Carattere: {}", c),
Err(e) => println!("Errore: {}", e),
}
}
e qui l'errore:
Errore: converted integer out of range for `char`
Come potrete vedere ne ricavamo un errore, come è giusto che sia.
I caratteri possono essere usati nei range, dei quali parleremo in apposito paragrafo, ne anticipiamo la definizione, potrete tornare su questo argomento in un secondo momento:
a..=z;
A..=Z;
Una particolarità di cui a volte non si fa cenno a volte sono i byte-literal. Sono definiti ad esempio come segue:
let bb: u8 = b'a';
In pratica assegniamo a bb, il valore del carattere ASCII 'a' che è 97.
In pratica equivale a 97u8. Il tipo u8 è ovviamente perfetto per contenere
il range degli ASCII. L'utilità dei byte literal è che ci permettono di
lavorare a livello di byte singoli e non con stringhe.
Reincontreremo
presto i caratteri, il loro uso nell'ambito di sequenze ci porterà nel cuore
di Rust.