È uscito il Corso SQL Completo

Stringhe in Rust

Le stringhe in Rust sono uno degli argomenti che più sorprendono chi proviene da altri linguaggi di programmazione. Rust ha due tipi principali di stringa: String e &str, ciascuno con caratteristiche e casi d’uso specifici. In questa guida esploreremo entrambi i tipi e le operazioni più comuni.

I Due Tipi di Stringa

String (Owned, Heap)

String è un tipo di stringa owned (posseduto), allocato sull’heap, crescibile e mutabile:

fn main() {
    let mut s = String::from("Ciao");
    s.push_str(", mondo!");
    println!("{}", s); // Stampa: Ciao, mondo!
}

&str (Borrowed, Slice)

&str e una string slice (fetta di stringa), un riferimento a una sequenza di byte UTF-8. I letterali stringa sono di tipo &str:

fn main() {
    let saluto: &str = "Ciao, mondo!"; // Letterale stringa, memorizzato nel binario
    println!("{}", saluto);
}

La differenza fondamentale: String possiede i dati, &str prende in prestito i dati da qualcun altro.

Creare Stringhe

Esistono diversi modi per creare una String:

fn main() {
    // Da un letterale stringa
    let s1 = String::from("Ciao");
    let s2 = "Ciao".to_string();
    let s3 = "Ciao".to_owned();

    // Stringa vuota
    let s4 = String::new();

    // Con capacità pre-allocata
    let s5 = String::with_capacity(100);

    // Da un formato
    let nome = "Rust";
    let s6 = format!("Benvenuto in {}!", nome);

    println!("{}", s1);
    println!("{}", s6);
    println!("Capacità s5: {}", s5.capacity());
}

Metodi Principali delle Stringhe

push_str e push

fn main() {
    let mut s = String::from("Ciao");

    // push_str aggiunge una string slice
    s.push_str(" mondo");

    // push aggiunge un singolo carattere
    s.push('!');

    println!("{}", s); // Stampa: Ciao mondo!
}

len e is_empty

fn main() {
    let s = String::from("Ciao");
    let vuota = String::new();

    println!("Lunghezza: {} byte", s.len());    // 4
    println!("È vuota? {}", s.is_empty());       // false
    println!("È vuota? {}", vuota.is_empty());   // true

    // Attenzione: len() restituisce i byte, non i caratteri
    let emoji = String::from("🦀");
    println!("Byte emoji: {}", emoji.len());      // 4
    println!("Caratteri emoji: {}", emoji.chars().count()); // 1
}

contains e replace

fn main() {
    let frase = String::from("Rust è un linguaggio fantastico");

    println!("Contiene 'Rust'? {}", frase.contains("Rust"));     // true
    println!("Contiene 'Python'? {}", frase.contains("Python")); // false

    let nuova = frase.replace("fantastico", "straordinario");
    println!("{}", nuova); // Rust è un linguaggio straordinario
}

trim, to_uppercase, to_lowercase

fn main() {
    let spazi = "  Ciao, Rust!  ";
    println!("Trim: '{}'", spazi.trim());           // 'Ciao, Rust!'
    println!("Trim start: '{}'", spazi.trim_start()); // 'Ciao, Rust!  '
    println!("Trim end: '{}'", spazi.trim_end());    // '  Ciao, Rust!'

    let testo = "Ciao Mondo";
    println!("Maiuscolo: {}", testo.to_uppercase()); // CIAO MONDO
    println!("Minuscolo: {}", testo.to_lowercase()); // ciao mondo
}

starts_with, ends_with e split

fn main() {
    let percorso = "src/main.rs";

    println!("Inizia con 'src'? {}", percorso.starts_with("src"));  // true
    println!("Finisce con '.rs'? {}", percorso.ends_with(".rs"));    // true

    // Split
    let csv = "uno,due,tre,quattro";
    let parti: Vec<&str> = csv.split(',').collect();
    println!("{:?}", parti); // ["uno", "due", "tre", "quattro"]
}

Concatenazione di Stringhe

Operatore +

L’operatore + consuma la prima stringa (ownership) e prende in prestito la seconda:

fn main() {
    let s1 = String::from("Ciao");
    let s2 = String::from(" mondo");

    let s3 = s1 + &s2; // s1 viene spostato, s2 viene preso in prestito
    // println!("{}", s1); // Errore! s1 non è più valido
    println!("{}", s2);    // OK
    println!("{}", s3);    // Ciao mondo
}

La Macro format!

Per concatenazioni complesse, format! è la soluzione più leggibile e non consuma nessuna stringa:

fn main() {
    let nome = String::from("Marco");
    let cognome = String::from("Rossi");
    let eta = 30;

    let presentazione = format!("Mi chiamo {} {}, ho {} anni", nome, cognome, eta);
    println!("{}", presentazione);

    // nome e cognome sono ancora validi
    println!("Nome: {}, Cognome: {}", nome, cognome);
}

String Slicing

È possibile ottenere una slice (sottostringa) di una stringa utilizzando gli indici di byte:

fn main() {
    let saluto = String::from("Ciao mondo");

    let ciao = &saluto[0..4];   // "Ciao"
    let mondo = &saluto[5..10]; // "mondo"

    println!("{} - {}", ciao, mondo);
}

Attenzione: gli indici devono cadere su confini di caratteri UTF-8 validi, altrimenti il programma va in panic:

fn main() {
    let emoji = "🦀 Rust";
    // let slice = &emoji[0..1]; // Panic! Non è un confine UTF-8 valido
    let slice = &emoji[0..4];    // OK: "🦀" occupa 4 byte
    println!("{}", slice);
}

Iterare sulle Stringhe

Iterare sui Caratteri

fn main() {
    let parola = "Ciao 🦀";

    // Iterare sui caratteri Unicode
    for c in parola.chars() {
        print!("'{}' ", c);
    }
    println!();
    // Output: 'C' 'i' 'a' 'o' ' ' '🦀'

    // Ottenere un vettore di caratteri
    let caratteri: Vec<char> = parola.chars().collect();
    println!("{:?}", caratteri);
}

Iterare sui Byte

fn main() {
    let parola = "Ciao";

    // Iterare sui byte
    for b in parola.bytes() {
        print!("{} ", b);
    }
    println!();
    // Output: 67 105 97 111
}

Codifica UTF-8

Le stringhe in Rust sono sempre UTF-8 valido. Questo significa che un singolo “carattere” visibile può occupare da 1 a 4 byte:

fn main() {
    let ascii = "A";         // 1 byte
    let accento = "è";       // 2 byte
    let cinese = "漢";       // 3 byte
    let emoji = "🦀";        // 4 byte

    println!("'A' = {} byte", ascii.len());
    println!("'è' = {} byte", accento.len());
    println!("'漢' = {} byte", cinese.len());
    println!("'🦀' = {} byte", emoji.len());

    // La frase mista ha una lunghezza in byte diversa dal numero di caratteri
    let mista = "Ciao 漢字 🦀";
    println!("Byte: {}, Caratteri: {}", mista.len(), mista.chars().count());
}

Conversione tra String e &str

La conversione tra i due tipi di stringa è semplice e comune:

fn stampa_saluto(messaggio: &str) {
    println!("Saluto: {}", messaggio);
}

fn main() {
    // Da &str a String
    let slice: &str = "Ciao";
    let owned: String = slice.to_string();
    let owned2: String = String::from(slice);

    // Da String a &str (deref coercion)
    let owned3 = String::from("Mondo");
    stampa_saluto(&owned3); // String viene automaticamente convertita a &str

    // Slice esplicita
    let slice2: &str = &owned3[..];
    let slice3: &str = owned3.as_str();

    println!("{} {} {} {}", owned, owned2, slice2, slice3);
}

La deref coercion di Rust converte automaticamente &String in &str quando necessario, rendendo le funzioni che accettano &str molto flessibili.

Conclusione

Le stringhe in Rust possono sembrare complesse rispetto ad altri linguaggi, ma questa complessita riflette la realta della gestione delle stringhe: ownership, allocazione di memoria e codifica UTF-8 sono aspetti fondamentali che Rust rende espliciti. Comprendere la differenza tra String e &str, e quando usare ciascuno, è una competenza essenziale per scrivere codice Rust efficiente e corretto.