Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 7
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. Anche Python, ad esempio, non accetta questo range.

Quel range è:
U+D800 ..= U+DFFF

(numero decimale: 55296 ..= 57343)

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 esempio
let c4 = '\u{D7FF}';

Per leggere l'occupazione di memoria del singolo carattere possiamo usare anche qui un semplice programma come il seguente:

Esempio 7.1
1
2
3
4
5
6
7
8
fn main() {
    let c1: char = 'a';
    let c2: char = '1';
    let size_in_bytes = std::mem::size_of_val(&c1);
    println!("L'occupazione in memoria del carattere 'a' è di {} byte", size_in_bytes);
    let size_in_bytes = std::mem::size_of_val(&c2);
    println!("L'occupazione in memoria del carattere '1' è di {} 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 7.2
1
2
3
4
5
6
7
8
fn main()
{
    let c1 = 'a';
    println!("{}", c1 as i32);
    println!("{}", 98 as char);
    println!("{}", (char::MAX) as i32);
    println!("{}", (char::MIN) as i32);
}

che ci dà 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 consueto, trovate nel sito ufficiale. Qui di seguito darò un breve esempio:

  Esempio 7.3
1
2
3
4
5
6
7
8
9
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());
}

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 7.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 ecco 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),
  }
}

Come potrete vedere ne ricavamo un errore, come è giusto che sia.

I caratteri possono essere usati nei range, dei quali parleremo in apposito paragrafo, anticipiamo la definizione, ci potrete tornare sopra 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.