DanLevy.net

Quiz: Gestione della Memoria in Rust

(Borrow) controlla te stesso prima di rovinarti! 🦀

Pronto a mettere alla prova le tue capacità di gestione della memoria in Rust? 🦀

Questo quiz metterà alla prova la tua comprensione del sistema di ownership di Rust, delle regole di borrowing, dei lifetimes e dei puntatori intelligenti.

Nota: Le domande sono formattate a circa 50 colonne di larghezza per garantire leggibilità su tutti i dispositivi. (Suggerimenti per miglioramenti sono benvenuti!)

Che tu sia un Rustacean esperto o stia appena iniziando a gestire la memoria, questo quiz ti aiuterà a consolidare le tue conoscenze. Andiamo! 🦀

Cosa succede quando esegui questo codice? Prova a prevedere l’output o l’errore:

fn main() {
let philosopher =
String::from("Zeno of Citium");
let greeting = philosopher;
println!("Hello, {}!", philosopher);
}

Questo codice non compila a causa delle regole di proprietà di Rust. Quando assegniamo philosopher a greeting, la proprietà della String viene spostata a greeting. Dopo questo spostamento, philosopher non è più valido.

Ecco tre modi per risolvere:

  1. Clonare la stringa (crea una nuova copia):
let greeting = philosopher.clone();
  1. Usa un riferimento (prende in prestito il valore):
let greeting = &philosopher;
  1. Usa una slice di stringa (prende in prestito una parte della stringa):
let greeting = &philosopher[..];

Ogni soluzione ha casi d’uso e implicazioni di performance diversi. Clonare è più costoso ma ti dà la proprietà, mentre i riferimenti sono più leggeri ma hanno vincoli di lifetime.

Cosa succede quando esegui questo codice? Pensa al trasferimento di proprietà:

fn take_knowledge(knowledge: String) {
println!("Knowledge: {}", knowledge);
}
fn main() {
let wisdom = String::from("know thyself");
take_knowledge(wisdom);
// What happens to our wisdom?
println!("Do you {}", wisdom);
}

Il codice non compila perché la proprietà di wisdom è stata spostata a take_knowledge e quindi non può più essere usata dopo.

Ecco tre modi per risolvere il problema:

  1. Passare per riferimento (prendere in prestito il valore):
fn borrow_it(text: &String) {
println!("Inside: {}", text);
}
borrow_it(&wisdom); // Now wisdom can be used after
  1. Clonare il valore (creare una nuova copia):
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. Restituire la proprietà dalla funzione:
fn take_and_return(text: String) -> String {
println!("Inside: {}", text);
text // Return ownership back
}
let wisdom = take_and_return(wisdom); // Reassign returned ownership

Ogni approccio ha casi d’uso diversi:

  • Riferimenti: i più efficienti, ma richiedono la gestione dei lifetimes
  • Clonazione: semplice ma potenzialmente costosa
  • Restituire la proprietà: utile per trasformare valori

Best practice: Usa i riferimenti a meno che non ti serva trasferire la proprietà.

Cosa succede con più riferimenti mutabili?

fn main() {
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom; // First mutable borrow
let ref2 = &mut wisdom; // Second mutable borrow
ref1.push_str(" himself never runs");
ref2.push_str(" out of things to laugh at.");
}

Rifletti sulle regole di Rust per i riferimenti mutabili.

Questo codice viola le regole fondamentali di borrowing di Rust:

  • Solo UN riferimento mutabile a un valore alla volta
  • OPPURE un numero qualsiasi di riferimenti immutabili
  • I riferimenti non possono superare la durata del loro referent

Ecco come correggere il codice:

  1. Usa uno scoping sequenziale:
let mut wisdom = String::from("He who laughs at");
{
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs");
} // ref1 goes out of scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. Oppure modifica la stringa in un unico prestito:
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs out of things to laugh at.");

Queste regole impediscono le data race al momento della compilazione, rendendo Rust thread-safe per impostazione predefinita.

Insidia comune: Tentare di usare più riferimenti mutabili per evitare il cloning o per modificare parti diverse dello stesso valore simultaneamente.

Questo codice compila? In tal caso, perché? Altrimenti, cosa c’è di sbagliato?

fn first_word(s: &str) -> &str { // No explicit lifetimes?
match s.find(' ') {
Some(pos) => &s[0..pos],
None => s,
}
}
fn main() {
let name = String::from("Seneca the Younger");
let first = first_word(&name);
println!("Hello, {}", first);
}

Questo codice compila con successo grazie alle regole di elisione delle durate di Rust. Queste regole permettono al compilatore di inferire automaticamente le durate nei pattern comuni.

Le tre regole di elisione delle durate sono:

  1. Ogni parametro ottiene il proprio parametro di durata
  2. Se c’è esattamente un parametro di durata in ingresso, quella durata viene assegnata a tutti i parametri di durata in uscita
  3. Se ci sono più parametri di durata in ingresso, ma uno di essi è &self o &mut self, la durata di self viene assegnata a tutti i parametri di durata in uscita

Questa funzione è equivalente a:

fn first_word<'a>(s: &'a str) -> &'a str {
// ... same implementation
}

Pattern comuni in cui l’elisione funziona:

// These don't need explicit lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

Buona pratica: lascia che l’elisione lavori per te quando possibile, ma comprendi quando sono necessarie durate esplicite.

Cosa c’è di sbagliato in questa definizione di tipo ricorsivo?

#[derive(Debug)]
enum CatList {
Cons(i32, CatList), // Recursive without indirection
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
CatList::Cons(2,
CatList::Cons(3,
CatList::Nil)));
}

Questo codice fallisce perché il compilatore non riesce a determinare la dimensione di CatList a tempo di compilazione. La natura ricorsiva del tipo significa che potrebbe essere infinitamente grande!

Ecco come correggerlo usando Box<T>:

#[derive(Debug)]
enum CatList {
Cons(i32, Box<CatList>), // Box provides a fixed-size pointer
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
Box::new(CatList::Cons(2,
Box::new(CatList::Cons(3,
Box::new(CatList::Nil))))));
}

Perché Box<T> funziona:

  1. Box fornisce un puntatore a dimensione fissa (di solito 8 byte su sistemi a 64 bit)
  2. I dati reali sono memorizzati sull’heap
  3. Il compilatore ora sa esattamente quanta memoria allocare

Casi d’uso comuni per Box<T>:

  • Strutture dati ricorsive (liste collegate, alberi)
  • Dati di grandi dimensioni che vuoi garantire siano allocati sull’heap
  • Oggetti trait quando serve dispatch dinamico

Buona pratica: Usa Box<T> quando ti serve:

  • Tipi ricorsivi
  • Per garantire l’allocazione su heap
  • Per spostare grandi dati senza copiarli

Cosa stamperà questo codice? Conta attentamente!

use std::rc::Rc;
fn main() {
let text = Rc::new(String::from("Meditations")); // Count: 1
let marcus = Rc::clone(&text); // What happens here?
let aurelius = Rc::clone(&text); // And here?
println!(
"Reference count: {}",
Rc::strong_count(&text)
);
}

Analizziamo come funziona Rc:

  1. Creazione iniziale con Rc::new(): conteggio = 1
  2. Primo clone per marcus: conteggio = 2
  3. Secondo clone per aurelius: conteggio = 3

Caratteristiche importanti di Rc:

use std::rc::Rc;
fn demonstrate_rc() {
let original = Rc::new(String::from("Shared"));
println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{
let copy = Rc::clone(&original);
println!("Count inside scope: {}", Rc::strong_count(&original)); // 2
} // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1
}

Punti chiave:

  • Rc::clone() è economico - incrementa solo un contatore
  • Rc è solo per scenari single-thread
  • Quando l’ultimo riferimento viene rilasciato, i dati vengono puliti
  • Usa riferimenti Weak per evitare cicli di riferimento

Buone pratiche:

  • Usa Rc quando ti serve proprietà condivisa
  • Considera Arc per scenari thread-safe
  • Evita di creare cicli di riferimento

Questa definizione di struct compila? Perché o perché no?

struct Philosopher {
name: &str, // Reference without lifetime
quote: &str, // Another reference without lifetime
}
fn main() {
let phil = Philosopher {
name: "Seneca",
quote: "Luck happens when preparation meets opportunity",
};
}

Il codice fallisce perché le struct che contengono riferimenti devono specificare i lifetimes. Ecco come correggerlo:

// Single lifetime parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

Pattern comuni:

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

Buone pratiche:

  1. Usa tipi posseduti (String) quando devi memorizzare dati indefinitamente
  2. Usa riferimenti quando la durata della struct è chiaramente più breve dei dati
  3. Considera più parametri di lifetime quando i riferimenti possono avere durate diverse
  4. Documenta le relazioni di lifetime nelle strutture complesse

Cosa succede con questa funzione che restituisce la più lunga tra due slice di stringa?

fn longest(text1: &str, text2: &str) -> &str {
if text1.len() > text2.len() {
text1 // Returning a reference, but which lifetime?
} else {
text2 // Could be this reference instead
}
}
fn main() {
println!("{}", longest(
"Seneca the Younger",
"Marcus Aurelius"
));
}

Questo codice fallisce perché il compilatore non riesce a determinare la relazione tra i lifetime di input e output. Ecco perché e come risolverlo:

// Correct version with explicit lifetime annotation
fn longest<'a>(text1: &'a str, text2: &'a str) -> &'a str {
if text1.len() > text2.len() {
text1
} else {
text2
}
}
// Alternative with different lifetimes
fn longest_flex<'a, 'b>(text1: &'a str, text2: &'b str) -> &'a str {
if text1.len() > text2.len() {
text1
} else {
text2.to_string().as_str() // Won't compile! Shows why we need same lifetime
}
}

Perché i lifetime sono necessari qui:

  1. Riferimenti di input multipli potrebbero avere lifetimes diversi
  2. Il valore di ritorno deve vivere almeno quanto entrambi gli input
  3. Il compilatore deve verificare queste relazioni

Schemi comuni:

// Single input reference - elision works
fn first_word(s: &str) -> &str { /* ... */ }
// Multiple references, same lifetime needed
fn compare_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { /* ... */ }
// Different lifetimes possible
fn combine<'a, 'b>(s1: &'a str, s2: &'b str) -> String { /* ... */ }

Migliori pratiche:

  1. Lasciare che l’elisione dei lifetime funzioni quando possibile
  2. Usare lifetimes espliciti quando le relazioni devono essere chiare
  3. Considerare di restituire tipi posseduti per evitare la complessità dei lifetime
  4. Documentare le relazioni di lifetime complesse

Cosa succede quando questo codice viene eseguito?

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow_mut(); // First mutable borrow
let _borrow2 = data.borrow_mut(); // Second mutable borrow
println!("Value: {}", _borrow2);
}

RefCell fornisce mutabilità interna ma comunque applica le regole di borrowing di Rust a runtime:

use std::cell::RefCell;
fn demonstrate_refcell() {
let data = RefCell::new(42);
// Correct way to use RefCell
{
let mut first = data.borrow_mut();
*first += 1;
} // first is dropped here
// Now we can borrow again
let second = data.borrow_mut();
// Or multiple immutable borrows
let read1 = data.borrow();
let read2 = data.borrow(); // This is OK
}

Concetti chiave:

  1. RefCell sposta i controlli di borrowing a runtime
  2. Può causare panici se le regole vengono violate
  3. Utile per il pattern di mutabilità interna

Casi d’uso comuni:

  • Oggetti mock nei test
  • Implementazione di strutture auto-referenziali
  • Quando è necessario mutare dati dietro un riferimento condiviso

Buone pratiche:

  1. Preferire il borrowing a tempo di compilazione quando possibile
  2. Tenere i prestiti di RefCell in scope ristretti
  3. Considerare l’uso di drop() per terminare esplicitamente i prestiti
  4. Usare RefCell quando serve mutabilità interna

Cosa stamperà questo codice?

use std::cell::Cell;
fn main() {
let life = Cell::new(42);
let meaning = &life; // Shared reference
println!("{}", life.get()); // What prints here?
meaning.set(43); // Mutation through shared ref
println!("{}", life.get()); // And here?
}

Cell e RefCell servono a scopi diversi per la mutabilità interna:

use std::cell::{Cell, RefCell};
// Cell for Copy types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

Differenze chiave:

  1. Cell:
  • Funziona al meglio con tipi Copy
  • Nessuna API di borrowing
  • Copia o sposta sempre i valori
  1. RefCell:
  • Funziona con qualsiasi tipo
  • Ha un’API di borrowing
  • Controllo del borrowing a runtime

Buone pratiche:

  1. Usa Cell per tipi Copy semplici (numeri, bool, ecc.)
  2. Usa RefCell quando devi prendere in prestito il contenuto
  3. Mantieni le mutazioni tramite Cell/RefCell al minimo
  4. Documenta perché è necessaria la mutabilità interna

Quando dovresti usare Rc (Reference Counting) in Rust?

Considera questo esempio:

use std::rc::Rc;
struct SharedConfig {
name: String,
value: i32,
}
fn main() {
let config = Rc::new(SharedConfig {
name: "settings".to_string(),
value: 42,
});
let config2 = Rc::clone(&config);
// Both config and config2 share ownership
}

Rc (Reference Counting) è progettato per scenari single‑thread in cui è necessario possedere condiviso.

Casi d’uso comuni:

use std::rc::Rc;
use std::cell::RefCell;
// Shared ownership in data structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

Punti chiave:

  1. Usa Rc quando:
  • Più parti del tuo codice hanno bisogno di possedere
  • Sai che la condivisione è single‑thread
  • La durata non può essere determinata staticamente
  1. Usa Arc invece quando:
  • Hai bisogno di condivisione thread‑safe
  • Più thread hanno bisogno di possedere
  1. Limiti di Rc:
  • Non è thread‑safe
  • Leggero overhead a runtime
  • Non può rompere automaticamente i cicli di riferimento

Buone pratiche:

  1. Preferisci la proprietà unica quando possibile
  2. Usa Rc per proprietà condivisa single‑thread
  3. Usa Arc per scenari multi‑thread
  4. Combinalo con Weak per prevenire cicli di riferimento

Qual è la differenza fondamentale tra RefCell e RwLock in Rust?

Considera questi esempi:

use std::cell::RefCell;
use std::sync::RwLock;
// Example 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let shared = RwLock::new(vec![1, 2, 3]);
let locked = shared.write().unwrap();

RefCell e RwLock hanno scopi simili ma in contesti diversi:

// Single-threaded scenario with RefCell
use std::cell::RefCell;
struct SingleThreaded {
data: RefCell<Vec<i32>>,
}
impl SingleThreaded {
fn modify(&self) {
self.data.borrow_mut().push(42);
}
}
// Multi-threaded scenario with RwLock
use std::sync::RwLock;
struct ThreadSafe {
data: RwLock<Vec<i32>>,
}
impl ThreadSafe {
fn modify(&self) {
self.data.write().unwrap().push(42);
}
}

Key differences:

  1. RefCell:
  • Solo single-thread
  • Nessun overhead di sincronizzazione
  • Panic in caso di violazioni di prestito
  1. RwLock:
  • Sicuro per i thread
  • Ha overhead di sincronizzazione
  • Può bloccare i thread invece di panicolare

Best practices:

  1. Usa RefCell per mutabilità interna single-thread
  2. Usa RwLock quando è necessaria la sicurezza dei thread
  3. Considera Mutex per una mutabilità thread-safe più semplice
  4. Documenta chiaramente i requisiti di sicurezza dei thread

Cosa succede quando questo codice viene eseguito?

use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(42));
let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock
let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);
}

Questo codice dimostra uno scenario classico di deadlock. Ecco come risolverlo:

use std::sync::{Arc, Mutex};
// Correct way - Release lock before acquiring it again
fn safe_mutex() {
let lock = Arc::new(Mutex::new(42));
{
let mut data = lock.lock().unwrap();
*data += 1;
} // Lock is released here
// Now we can acquire it again
let data2 = lock.lock().unwrap();
println!("Value: {}", data2);
}
// Using multiple mutexes safely
fn multiple_mutexes() {
let lock1 = Arc::new(Mutex::new(42));
let lock2 = Arc::new(Mutex::new(43));
// Always acquire locks in the same order
let guard1 = lock1.lock().unwrap();
let guard2 = lock2.lock().unwrap();
}

Le migliori pratiche per prevenire deadlock:

  1. Mantieni le sezioni critiche piccole
  2. Rilascia i lock tempestivamente usando gli scope
  3. Acquisisci più lock in un ordine coerente
  4. Usa parking_lot::Mutex per migliori prestazioni
  5. Considera l’uso di RwLock per carichi di lavoro con molte letture

Pattern comuni:

// Thread-safe counter
struct Counter {
count: Arc<Mutex<i32>>,
}
impl Counter {
fn increment(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
} // Lock automatically released here
}

Cosa succede quando esegui questo codice con riferimenti deboli?

use std::rc::{Rc, Weak};
fn main() {
let data = Rc::new(String::from("Wisdom"));
let weak = Rc::downgrade(&data); // Create weak reference
drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());
}

I riferimenti deboli non impediscono la deallocazione dei loro target. Ecco un esempio dettagliato:

use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child tree structure avoiding reference cycles
struct Node {
next: Option<Rc<Node>>,
parent: RefCell<Weak<Node>>, // Weak to prevent cycles
value: i32,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
next: None,
parent: RefCell::new(Weak::new()),
value,
})
}
fn set_parent(&self, parent: &Rc<Node>) {
*self.parent.borrow_mut() = Rc::downgrade(parent);
}
fn get_parent(&self) -> Option<Rc<Node>> {
self.parent.borrow().upgrade()
}
}

Casistiche comuni:

  1. Strutture tipo cache dove le voci possono essere svuotate
  2. Strutture ad albero con riferimenti ai genitori
  3. Pattern observer dove i soggetti possono essere eliminati
  4. Rompere i cicli di riferimento in strutture dati complesse

Buone pratiche:

  1. Usa riferimenti deboli per relazioni opzionali
  2. Controlla i risultati di upgrade() prima di usarli
  3. Documenta chiaramente le relazioni di proprietà
  4. Considera alternative come indici per casi più semplici

Cosa accade al handle del file in questo esempio RAII?

use std::fs::File;
struct FileWrapper {
file: File,
}
fn main() {
let file = File::create("test.txt").unwrap();
let wrapper = FileWrapper { file };
// ... use wrapper ...
// No Drop implementation
}

RAII in Rust garantisce che le risorse siano gestite correttamente. In questo esempio, FileWrapper non ha bisogno di un’implementazione personalizzata di Drop per chiudere il file handle: il suo campo File viene droppato automaticamente quando il wrapper esce dallo scope.

Implementi Drop solo quando il wrapper stesso ha un comportamento di pulizia aggiuntivo oltre a droppare i suoi campi:

use std::fs::File;
use std::io::{self, Write};
struct FileWrapper {
file: File,
path: String,
}
impl FileWrapper {
fn new(path: &str) -> io::Result<FileWrapper> {
Ok(FileWrapper {
file: File::create(path)?,
path: path.to_string(),
})
}
fn write(&mut self, content: &str) -> io::Result<()> {
self.file.write_all(content.as_bytes())
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Ensure file is properly closed
// Could also do cleanup like deletion
println!("Closing file: {}", self.path);
}
}

Pattern RAII:

  1. Il costruttore acquisisce le risorse
  2. I metodi usano le risorse in modo sicuro
  3. I campi vengono droppati automaticamente quando il proprietario esce dallo scope
  4. Un Drop personalizzato aggiunge pulizia extra quando necessario
  5. Usa ? per la propagazione degli errori

Buone pratiche:

  1. Affidati alle implementazioni Drop della libreria standard quando modellano già la risorsa
  2. Mantieni la gestione delle risorse semplice e evidente
  3. Usa i tipi della libreria standard quando possibile
  4. Documenta il comportamento di pulizia
  5. Considera l’uso di pattern guard per operazioni a scope limitato

Cosa succede quando cloni questa struct Philosophy?

#[derive(Clone)]
struct Philosophy {
school: String,
founder: String,
}
fn main() {
let stoicism = Philosophy {
school: String::from("Stoicism"),
founder: String::from("Zeno of Citium")
};
let new_school = stoicism.clone();
println!("{} - {}",
stoicism.school, new_school.school);
}

Comprendiamo in dettaglio Copy vs Clone:

// Types that can be Copy
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
// Types that can only be Clone
#[derive(Clone)]
struct ComplexData {
name: String, // String can't be Copy
points: Vec<i32> // Vec can't be Copy
}
// Manual implementation example
#[derive(Debug)]
struct Custom {
data: Vec<i32>,
identifier: u32,
}
impl Clone for Custom {
fn clone(&self) -> Self {
Custom {
data: self.data.clone(),
identifier: self.identifier, // Copy type
}
}
}

Differenze chiave:

  1. Copy:
  • Implicito, copia bit‑a‑bit
  • Deve essere Copy‑safe (nessuna allocazione su heap)
  • Tipicamente per tipi piccoli, solo stack
  1. Clone:
  • Esplicito, potenzialmente copia profonda
  • Può gestire allocazioni su heap
  • Più flessibile ma potenzialmente costoso

Buone pratiche:

  1. Implementa Copy per tipi piccoli, solo stack
  2. Usa Clone per tipi con risorse possedute
  3. Documenta le implicazioni di performance di Clone
  4. Considera implementazioni personalizzate di Clone per ottimizzare
  5. Stai attento alla derivazione automatica

Su un tipico target Rust a 64-bit attuale, qual è la dimensione di questa struct?

struct Metadata {
id: u32, // How many bytes?
name: String, // How many bytes?
active: bool // How many bytes + padding?
}

Analizziamo la disposizione della memoria della struct e l’ottimizzazione:

// Typical current 64-bit Rust layout: 32 bytes
struct Metadata {
id: u32, // 4 bytes
name: String, // 24 bytes on 64-bit systems
active: bool // 1 byte + padding/alignment
}
// Reordering fields may reduce padding for repr(C) structs,
// but default Rust layout is not a stable ABI guarantee.
struct OptimizedMetadata {
name: String, // 24 bytes
id: u32, // 4 bytes
active: bool // 1 byte + 3 padding
}
// Further optimization with packing
#[repr(packed)]
struct PackedMetadata {
id: u32,
active: bool,
name: String,
}

Considerazioni sul layout della memoria:

  1. Requisiti di allineamento:
  • u32: allineamento a 4 byte
  • String: allineamento a 8 byte e dimensione di 24 byte sui target 64-bit comuni
  • bool: allineamento a 1 byte
  1. Strategie di ordinamento dei campi:
  • Raggruppa campi di dimensioni simili
  • Metti prima gli allineamenti più grandi
  • Considera l’ottimizzazione della linea di cache

Buone pratiche:

  1. Per FFI o assunzioni di layout stabile, usa un repr(...) appropriato
  2. Usa dimensioni intere appropriate
  3. Considera l’uso di Option per campi opzionali
  4. Misura le struct critiche per la dimensione con std::mem::size_of
  5. Usa #[repr(packed)] con cautela - può influire sulle prestazioni

Come si confrontano le prestazioni di queste due implementazioni?

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn sum_loop(v: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..v.len() {
sum += v[i];
}
sum
}

Le astrazioni a costo zero di Rust si compilano in codice efficiente equivalente:

use std::ops::Range;
// High-level abstraction
trait ZeroCost {
fn process(&self) -> u32;
}
impl ZeroCost for Range<u32> {
fn process(&self) -> u32 {
self.fold(0, |acc, x| acc + x)
}
}
// Compiles to essentially the same code as:
fn manual_process(range: Range<u32>) -> u32 {
let mut sum = 0;
let mut i = range.start;
while i < range.end {
sum += i;
i += 1;
}
sum
}
// Even more abstractions, still zero-cost
fn complex_processing<T>(data: &[T]) -> u32
where T: AsRef<str> {
data.iter()
.map(|s| s.as_ref().len())
.filter(|&n| n > 3)
.fold(0, |acc, n| acc + n as u32)
}

Principi chiave:

  1. Ciò che non usi, non lo paghi
  2. Ciò che usi, non potresti scrivere a mano meglio

Buone pratiche:

  1. Usa liberamente astrazioni di alto livello
  2. Fidati delle ottimizzazioni del compilatore
  3. Fai profiling prima di ottimizzare
  4. Priorità alla leggibilità
  5. Usa iteratori e closure senza paura

Grazie per aver completato il quiz! Se ti è piaciuto mettere alla prova le tue conoscenze su Rust, dai un’occhiata alle mie altre sfide di programmazione! 🧠

Vuoi migliorare le tue abilità in Rust? Ecco alcune risorse consigliate: