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.