00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.