Storia della questione:
In Rust, il sistema dei moduli controlla rigorosamente la gerarchia e le dipendenze tra file e moduli. Man mano che il progetto cresce, spesso si presenta la necessità di organizzare dipendenze complesse tra parti del codice (ad esempio, se i tipi di un modulo sono necessari in un altro). In altri linguaggi (ad esempio, C/C++) questa situazione può portare a dipendenze cicliche, conflitti impliciti e errori di compilation.
Problema:
In Rust non è possibile creare dipendenze cicliche dirette (ogni modulo può riferirsi solo verso l'alto o verso il basso nella gerarchia). Pertanto, se ad esempio, il tipo A del modulo mod_a utilizza il tipo B di mod_b, e mod_b desidera utilizzare il tipo A, si verifica una situazione di stallo. Una cattiva organizzazione può portare all'impossibilità di suddividere il progetto in componenti indipendenti, o a una duplicazione del codice.
Soluzione:
Rust consiglia di introdurre tipi e trait comuni in moduli o crate separati e di utilizzare riferimenti esterni (fully qualified paths) tra di essi. A volte aiuta estrarre interfacce (trait) in un ulteriore anello intermedio. In questo modo, le dipendenze diventano unidirezionali e sono più facili da analizzare durante la fase di compilazione.
Esempio di codice:
// src/common.rs pub trait Drawable { fn draw(&self); } // src/shapes/mod.rs use crate::common::Drawable; pub struct Circle { pub r: f64 } impl Drawable for Circle { fn draw(&self) { /* ... */ } } // src/scene.rs use crate::common::Drawable; pub struct Scene<T: Drawable> { pub items: Vec<T> }
Caratteristiche chiave:
È possibile utilizzare pub use per evitare dipendenze cicliche e importare un modulo da se stesso?
No, pub use non è una soluzione per le dipendenze cicliche: funziona solo per il riesportazione di un elemento già definito. Se si tenta di utilizzare pub use per un modulo che non è ancora stato compilato o dichiarato, si verificherà un errore di compilazione.
Perché non è consentita la forward declaration dei moduli, come in C/C++?
In Rust non esiste il meccanismo di dichiarazione anticipata di tipi o moduli: tutti i moduli, i tipi e le costanti devono essere dichiarati e definiti al momento della compilazione. Questo consente al compilatore di verificare completamente la gerarchia dei tipi e di evitare conflitti inaspettati. La forward declaration indebolirebbe le garanzie di integrità del sistema dei tipi.
È possibile implementare riferimenti reciproci tra le strutture di due moduli attraverso Box o Rc?
Sì, se i tipi sono coerenti nelle dipendenze (ad esempio, tramite trait o enum comuni), è possibile utilizzare riferimenti indiretto (Box, Rc, Arc) tra le strutture. Tuttavia, ciò non esenta dall'obbligo di dichiararli in ambiti visivi che non creano moduli realmente ciclici.
Nel progetto sono stati creati moduli separati shapes/mod.rs e render/mod.rs, ma entrambi iniziano a utilizzare i tipi l'uno dell'altro direttamente. Si verifica un ciclo di dipendenze, il compilatore restituisce un errore di importazione non risolta.
Pro:
Contro:
I tipi comuni sono stati estratti nel modulo common, anche i trait sono stati estratti, e le dipendenze sono diventate unidirezionali (scene dipende da shapes, shapes e scene dipendono da common).
Pro:
Contro: