Questa pagina risponde a domande comuni inerenti il linguaggio di programmazione Rust. Non rappresenta una guida completa al linguaggio e nemmeno uno strumento per insegnarlo. Costituisce invece un riferimento per rispondere alle domande più frequenti concernenti le scelte di progettazione su cui Rust si basa.
Se c'è una domanda comune o importante che pensi sia ingiustamente esclusa qui, sentiti libero di aiutarci ad aggiungerla.
Progettare e implementare un linguaggio sicuro, concorrente e pratico per la programmazione di sistemi.
Rust nasce perchè altri linguaggi a questo livello di astrazione e efficienza non sono soddisfacenti. In particolare:
Rust esiste come un’alternativa che fornisce sia codice efficiente che un livello confortevole di astrazione, contemporaneamente migliorando tutti questi quattro punti.
No. Rust ebbe inizio nel 2006 come un progetto hobbystico di Graydon Hoare ed è rimasto così per oltre 3 anni. Mozilla è entrata nel 2009, dopo che il linguaggio si è dimostrato abbastanza maturo per eseguire una serie di test di base automatizzati e dimostrare la valenza dei suoi principi base. Anche se sponsorizzato da Mozilla, Rust è un progetto sviluppato da
una variegata comunità di appassionati da molti paesi del mondo. Il Team di Rust è composto sia da membri Mozilla che da esterni e rust
su GitHub ha avuto oltre 1900 sviluppatori diversi fino a oggi.
Finché concesso dalla politica di gestione del progetto, Rust è amministrato da un team base che imposta la visione e le priorità del progetto, guidandolo globalmente. Esistono anche dei sottogruppi per guidare e incoraggiare lo sviluppo in alcune aree di interesse, inclusi il linguaggio, il compilatore, le librerie, gli strumenti, la moderazione delle comunità ufficiali. La progettazione è guidata da un processo RFC.
Per cambiamenti che non richiedono una RFC, le decisioni sono fatte attraverso richieste di unione sul repository rustc
.
Principalmente in Servo, un motore di navigazione sperimentale a cui Mozilla sta lavorando. Stanno anche lavorando per integrare componenti Rust in Firefox.
I due più grandi progetti open source sono al momento Servo e il compilatore Rust stesso.
Un crescente numero di organizzazioni!
Il modo più semplice per provare Rust è con la playpen, un’applicazione online per scrivere e provare codice Rust. Se invece desideri provare Rust sul tuo computer, installalo e prova a seguire la guida al gioco dell’indovino dal libro.
Ci sono diversi modi. Puoi:
L’obiettivo iniziale di Rust fu quello di creare un linguaggio di programmazione per sistemi stabile e usabile. Per perseguire questo scopo molte idee sono state esplorate, alcune sono state preservate (campi di esistenza, tratti) mentre altre sono state scartate (il sistema di stati dei tipi, il green threading). Inoltre durante la transizione verso la versione 1.0, buona parte della libreria standard è stata riscritta per consentire al codice passato di sfruttare al meglio le funzionalità di Rust, fornendo interfacce di programmazione di qualità, stabili e multipiattaforma. Ora che Rust è alla versione 1.0, il linguaggio è garantito come “stabile”; e mentre potrebbe continuare a evolversi, il codice funzionante sulla versione attuale dovrebbe continuare a farlo anche nelle versioni future.
Rust segue lo standard SemVer, dove cambiamenti non compatibili con le versioni passate sono ammesse nelle versioni minori se questi cambiamenti risolvono errori del compilatore, risolvono problemi di sicurezza o cambiano le regole di dichiarazione o inferenza dei tipi in modo da richiedere ulteriore specifica. Linee guida più dettagliate per cambi di versione minori sono disponibili come RFC approvate sia per il linguaggio che per la libreria standard.
Rust mantiene tre canali di rilascio: stabile, beta e “nightly”. I canali stabile e beta sono aggiornati ogni sei settimane, con la nightly che diviene la nuova beta e la beta che diviene la nuova stabile. Le funzionalità del linguaggio e della libreria standard indicate come non stabili o nascoste dietro a blocchi di implementazione possono essere utilizzati solo nel canale di rilascio “nightly”. Le nuove funzionalità arrivano come instabili ma sono “liberate” una volta approvate dal team di sviluppo e relativi sottogruppi. Questo approccio consente di sperimentare e di contemporaneamente fornire una forte garanzia di retrompatibilità del canale stabile.
Per dettagli ulteriori, leggi il post sul blog di Rust “Stability as a Deliverable.”
No, non puoi. Rust cerca duramente di fornire garanzie sulla stabilità delle funzioni incluse nei canali beta e stabile. Quando qualcosa è non stabile significa che non è possibile ancora garantire il suo utilizzo e quindi non desideriamo che ci si basi su queste funzionalità o che queste non vengano modificate. Questo ci permette di provare i cambiamenti nel canale nightly, preservando le promesse di stabilità.
Molte cose vengono incluse nella stabile e i canali beta e stabile vengono aggiornati ogni sei settimane, con occasionali modifiche dirette anche al canale beta a volte.
Se stai aspettando la disponbilità di una funzionalità e non vuoi usufruire del canale nighly, puoi seguirne lo stato controllando l’etichetta B-unstable
sulla bacheca dei problemi.
I “Feature gates” sono il modo con cui Rust stabilizza funzionalità del compilatore, del linguaggio e della libreria standard.
Una funzione protetta è accessibile esclusivamente nel canale di rilascio “nighly” e solo quando abilitata esplicitamente con la direttiva #[feature]
o con l’argomento a linea di comando -Z unstable-options
.
Quando una funzione è stabilizzata diviene disponbile sul canale di rilascio stabile e non è necessario abilitarla esplicitamente.
A quel punto la funzione è considerata “libera”.
I “Feature gates” consentono agli sviluppatori di provare funzionalità sperimentali mentre sono in fase di implementazione, prima che giungano nel linguaggio stabile.
La licenza Apache include importanti protezioni contro le aggressioni legali ma non è compatibile con la GPL versione 2. Per evitare problemi nell’utilizzo di Rust e GPL2 è stata aggiunta la licenza alternativa MIT.
Questo è parzialmente dovuto alla preferenza dello sviluppatore originario (Graydon) e parzialmente al fatto che i linguaggi tendono ad avere un pubblico più vasto e una serie più variegata di implementazioni e utilizzi di altri prodotti come i browser web. Noi vorremmo appellarci al maggior numero possibile di sviluppatori.
Molto! Rust è già competitivo con programmi C e C++ ben scritti in una serie di prove (come nel Benchmarks Game e altri).
Come il C++, il Rust possiede astrazioni a costo zero come uno dei suoi principi chiave: nessuna delle astrazioni di Rust impone un rallentamento, in qualsiasi caso.
Dato che Rust utilizza LLVM e cerca di assomigliare a Clang dal punto di vista dell’interazione con LLVM, ogni miglioramento di LLVM è condiviso da Rust. Nel lungo periodo, la quantità elevata di informazioni presente nel sistema dei tipi di Rust dovrebbe permettere ottimizzazioni difficili o impossibili da implementare in C/C++.
No. Una delle innovazioni fondamentali di Rust è il garantire la sicurezza della memoria (nessun errore di segmentazione) senza richiedere un garbage collector.
Evitando di utilizzare un GC, Rust offre numerosi vantaggi: liberazione prevedibile delle risorse, gestione della memoria meno onerosa e soprattutto nessun sistema aggiuntivo operante durante l’esecuzione. Tutte queste caratteristiche rendono Rust leggero e facile da implementare in contesti arbitrari e rendono più facile integrare Rust con i linguaggi in possesso di un GC.
Rust non necessita di un GC grazie al suo sistema di possesso e passaggio ma lo stesso sistema aiuta con una moltitudine di altri problemi, inclusi la gestione delle risorse in generale e la concorrenza.
Per quando il possesso di un valore non fosse abbastanza, i programmi Rust fanno riferimento al tipo puntatore intelligente standard a conteggio dei riferimenti Rc
e alla sua versione sicura in contesti paralleli Arc
, invece di affidarsi a un GC.
Stiamo comunque investigando una garbage collection opzionale come estensione futura. L’obiettivo è integrarsi fluidamente con ambienti garbage-collected, come quelli offerti dai motori di Javascript Spidermonkey e V8. Inoltre, qualcuno sta investigando l’implementazione di un garbage collector interamente in Rust senza supporto del compilatore.
Il compilatore di Rust non compila con le ottimizzazioni se non esplicitamente istruito considerato che le ottimizzazioni rallentano la compilazione e sono scosigliate durante lo sviluppo.
Se compili con cargo
usa il parametro --release
.
Se compili direttamente con rustc
, usa il parametro -O
.
Ciascuno di questi abiliterà le ottimizzazioni.
Principalmente per la traduzione del codice e le ottimizzazioni. Rust fornisce delle astrazioni ad alto livello che vengono compilate in un codice macchina efficiente e queste trasformazioni richiedono molto tempo per essere effettuate, specialmente se in combinazione con le ottimizzazioni.
Ma il tempo di compilazione di Rust non è male come sembra e c’è da essere fiduciosi in un suo miglioramento futuro. Comparando progetti di dimensioni simili tra il C++ e Rust il tempo di compilazione è generalmente comparabile. La percezione di lentezza è largamente dovuta alle differenze tra il modello di compilazione del C++ da quello di Rust, l’unità base del C++ è il file mentre in Rust è l’intero pacchetto, composto da molti file. Di conseguenza durante lo sviluppo la modifica di un file causa molta meno ricompilazione che in Rust. In questo momento è in corso uno sforzo per implementare una ristrutturazione del compilatore che permetta di effettuare la compilazione incrementale, che consentirà a Rust di implementare un modello più rapido e simile al C++.
Oltre al modello di compilazione, ci sono molti altri aspetti dell’implementazione di Rust e il suo compilatore che ne impattano le prestazioni in fase di compilazione.
Inizialmente, Rust has un sistema di tipi moderatamente complesso e deve spendere un discreto quantitativo di tempo durante la compilazione a verificarne limitazioni e utilizzi, rendendo Rust sicuro durante la sua esecuzione.
Secondariamente, il compilatore di Rust soffre di un debito tecnico di lunga data e risaputamente genera una rappresentazione intermedia per LLVM di bassa qualità a cui LLVM deve porre rimedio. Si spera che i futuri passaggi di trasformazione e ottimizzazione basati su MIR riducano la quantità di lavoro che Rust impone a LLVM.
Terziariamente, l’utilizzo da parte di Rust di LLVM per la generazione del codice macchina è una spada a doppio taglio: se da un lato questo permette a Rust di avere prestazioni degne di nota, LLVM è un insieme di strumenti non focalizzato alla velocità di compilazione, specialmente con input di bassa qualità.
Inoltre, mentre la strategia preferita da Rust per monomorfizzare i generici (simil C++) produca codice performante, domanda di generare molto più codice rispetto ad altre strategie di implementazione. I programmatori Rust possono utilizzare i tratti per rimuovere questo codice extra e utilizzando un dispacciamento dinamico.
HashMap
di Rust sono così lente?
Di base, le HashMap
di Rust utilizzano l’algoritmo di hashing SipHash, progettato per prevenire attacchi basati sulla collisione di hash fornendo comunque un livello di prestazione accettabile in una varietà di compiti.
Mentre SipHash dimostra prestazioni competitive in molti casi, un caso in cui è conclamatamente lento è in presenza di chiavi corte, come ad esempio i numeri interi.
Questo perché i programmatori Rust spesso incontrano problemi prestazionali con HashMap
.
FNV hasher è spesso consigliato per queste casistiche, tenendo comunque in considerazione che non possiede le stesse caratteristiche di protezione dagli attacchi a collisione di SipHash.
Esiste ma è riservata al canale di rilascio “nightly”. Progettiamo di costituire un sistema integrato e modulare di misurazione delle prestazioni ma nel frattempo il sistema attuale è considerato instabile.
In generale, no.
Le ottimizzazioni delle tail-call sono riservate ad alcune condizioni particolari
ma non è assicurato.
Siccome è sempre stata una funzionalità desiderata, Rust ha riservato l’identificatore (become
),
anche se non è ancora chiaro se sia possibile questa ottimizzazione o se verrà implementata.
Era stata proposta un’estensione che avrebbe permesso di eliminare le tail-call in alcuni casi ma al momento è stata rimandata.
Non nel senso tipico utilizzato da linguaggi come il Java ma componenti della libreria standard di Rust possono essere considerati un “ambiente di esecuzione”, fornendo controlli per heap, backtrace, unwinding, e stack.
C’è un piccolo quantitativo di codice di inizializzazione che viene eseguito prima della funzione main
.
La libreria standard di Rust inoltre è collegata alla libreria standard del C, che effettua una simile inizializzazione.
Il codice di Rust può essere compilato anche senza la libreria standard, in questo caso il suo ambiente è circa equivalente a quello del C.
L’utilizzo delle graffe per indicare blocchi di codice è una scelta comune in una moltitudine di linguaggi di programmazione e l’adesione di Rust allo standard è utile per le persone già familiari con lo stile.
Le graffe consentono inoltre una sintassi più flessibile per il programmatore e per un preprocessore più semplice nel compilatore.
if
,
perché allora devo mettere delle parentesi attorno a righe di codice singole?
Perché non è ammesso lo stile del C?
Mentre il C richiede parentesi obbligatorie per le condizioni dell’istruzione if
ma rende le parentesi del corpo opzionali,
Rust fa la scelta opposta per i suoi if
.
Questo lascia la condizione separata chiaramente dal corpo e evita il pericolo delle parentesi opzionali,
che possono condurre in inganno durante la modifica del codice, come il famoso errore goto fail commesso da Apple.
Le scelte stilistiche di Rust sono per limitare la dimensione del linguaggio contemporaneamente sfruttando potenti librerie.
Anche se Rust fornisce una sintassi per inizializzare array e costanti letterali stringa, questi sono gli unici tipi collezione inseriti nel linguaggio.
Tutti gli altri tipi definiti da librerie, incluso l’onnipresente Vec
utilizzano macro per la loro inizializzazione, ad esempio la macro vec!
.
La scelta di utilizzare le macro di Rust per facilitare l’inizializzazione di collezioni verrà probabilmente estesa a altre collezioni in futuro, permettendo inizializzazioni semplici non solo di
HashMap
e Vec
, ma anche altri tipi di collezioni come BTreeMap
.
Nel frattempo, se vuoi una sintassi più comoda per inizializzare le collezioni, puoi creare la tua macro per fornirla.
Rust è un linguaggio molto espression-centrico e i ritorni impliciti fanno parte del suo design.
Costrutti come gli if
, i match
e i normali blocchi sono tutte espressioni in Rust.
Ad esempio, il codice seguente controlla se un i64
è dispari, ritornando il risultato semplicamente fornendone il risultato:
fn dispari(x: i64) -> bool {
if x % 2 != 0 { true } else { false }
}
Anche se può essere ulteriormente semplificato come qui:
fn dispari(x: i64) -> bool {
x % 2 != 0
}
In ciascun esempio, l’ultima riga rappresenta il valore di ritorno della funzione.
Risulta importante specificare che se una funzione termina con un punto e virgola il suo tipo di ritorno sarà ()
, indicando nessun valore di ritorno.
I ritorni impliciti funzionano quindi esclusivamente in assenza del punto e virgola.
I ritorni espliciti sono utilizzati solo se un ritorno implicito risulta impossibile perchè si desidera ritornare un valore prima della fine del corpo della funzione.
Mentre ciascuna delle funzioni sopra avrebbe potuto includere un return
e un punto e virgola, questa aggiunta sarebbe inutilmente prolissa e inconsistente con le convenzioni del codice Rust.
In Rust le dichiarazioni sono tendelzialmente accompagnate da un tipo esplicito, mentre nel codice i tipi vengono dedotti. Ci sono multiple ragioni per questa scelta:
match
deve essere così completo?
Per assistere nelle modifiche e in chiarezza.
Prima di tutto, se ogni possibilità viene coperta da un match
, l’aggiunta di varianti a un enum
causerà un errore di compilazione, invece che un problema durante l’esecuzione.
Questo tipo di assistenza permette di modificare il codice Rust liberamente e senza paura.
Secondariamente, il controllo dettagliato rende esplicito il caso predefinito: in generale l’unico modo per creare un match
non esaustivo sarebbe di far andare in errore il processo se non viene incontrato alcun valore previsto.
Le versioni iniziali di Rust non prevedevano la completezza di match
ed è stato accertato che questa scelta ha causato una moltitudine di problematiche.
Risulta comunque semplice ignorare tutti i casi non specificati usando il carattere speciale _
:
match val.fai_qualcosa() {
Gatto(a) => { /* ... */ }
_ => { /* ... */ }
}
f32
e f64
per i calcoli in virgola mobile?
La scelta dipende dallo scopo del programma.
Se ciò che conta è la precisione massima, f64
è da preferire.
Se invece si vuole minimizzare l’impatto in memoria e l’efficienza, ignorando la precisione persa, f32
è più appropriato.
Svolgere operazioni sugli f32
è generalmente più veloce, anche in piattaforme a 64-bit.
Come esempio, la grafica sfrutta tipicamente gli f32
perché richiede alte prestazioni e i numeri in virgola mobile a 32-bit bastano per rappresentare i pixel a schermo.
Nel dubbio, scegli f64
per una maggiore precisione.
HashMap
o un BTreeMap
?
I numeri a virgola mobile si possono comparare con gli operatori ==
, !=
, <
, <=
, >
, e >=
, e attraverso la funzione partial_cmp()
.
==
e !=
possiedono il tratto PartialEq
, mentre <
, <=
, >
, >=
, e partial_cmp()
possiedono il tratto PartialOrd
.
I numeri a virgola mobile non sono confrontabili con la funzione cmp()
, possidente il tratto Ord
, dato che i numeri a virgola mobile non costituiscono insieme totalmente ordinato.
Inoltre non esiste relazione di uguaglianza completa numeri a virgola mobile e quindi non implementano il trattoEq
.
Non esiste l’ordinamento totale o l’uguaglianza tra numeri a virgola mobile a causa del valore NaN
che non è minore, maggiore o uguale di alcun altro numero o se stesso.
Visto che i numeri a virgola mobile non implementano i tratti Eq
o Ord
, non sono utilizzabili nei tipi le cui limitazioni esigono l’implementazione di queste caratteristiche, come BTreeMap
o HashMap
.
Questo è importante perché questi tipi suppongono che le chiavi forniscano relazioni di ordinamento totale o di uguaglianza, a pena di malfunzionamenti.
Esiste un pacchetto che racchiude f32
e f64
in modo da fornire i tratti Ord
e Eq
che potrebbe assistere in certi casi.
Ci sono due modi: la parola chiave as
, che esegue una semplice conversione per tipi primitivi e i tratti Into
e From
,
che sono implementati per una serie di conversioni tra tipi numerici (e che anche tu puoi implementare sui tuoi tipi).
I tratti Into
e From
sono implementati esclusivamente per le conversioni prive di perdita, qundi ad esempio, f64::from(0f32)
funziona mentre f32::from(0f64)
no.
D’altro canto, as
convertirà tra qualsiasi coppia di tipi primitivi, effettuando i necessari troncamenti..
Gli operatori di preincremento e postincremento (e relativi opposti equivalenti), anche se convenienti presentano una discreta complessità;
Richiedono una conoscenza dell’ordine di esecuzione e spesso conducono a piccoli problemi e comportamenti anormali in C e C++.
x = x + 1
o x += 1
è leggermente più prolisso ma chiaro.
String
o un Vec<T>
a una partizione (&str
e &[T]
)?
Solitamente, puoi passare un riferimento a una String
o ad un Vec<T>
ovunque ci si aspetti una partizione.
Utilizzando le costrizione da de-riferimento, String
e Vec
verrano automaticamente ridotti alle rispettive partizioni quando passate come riferimento tramite &
o & mut
.
I metodi implementati su &str
e &[T]
possono essere utilizzati direttamente su String
e Vec<T>
.
Ad esempio una_stringa.trim()
funzionerà anche se trim
è un metodo su &str
e una_stringa
è una String
.
In alcuni casi, come nella programmazione generica è necessario convertire manualmente, questa operazione è effettuabile utilizzando l’operatore di partizione, in questo modo: &mio_vettore[..]
.
&str
in String
e viceversa?
Il metodo to_string()
converte una &str
in una String
e le String
sono automaticamente convertite in &str
quando ne acquisisci un riferimento.
Entrambe sono visibili nell’esempio seguente:
fn main() {
let s = "Maria Rossi".to_string();
saluta(&s);
}
fn saluta(nome: &str) {
println!("Ciao {}!", nome);
}
String
è un’area di memoria allocata nell’heap, di byte UTF-8.
Le String
mutabili possono essere modificate, espanendosi se necessario.
&str
è una “vista” di capacità fissata in una String
allocata altrove, generalmente nel heap, in caso di partizioni riferite a String
, o in memoria statica, per le costanti letterali.
&str
è un tipo primitivo implementato nel linguaggio Rust, mentre String
è implementato dalla libreria standard.
String
con complessità asintottica O(1)?
Non è possibile. Senza una chiara comprensione di cosa intendi per “carattere” e una pre-elaborazione della stringa per ritrovare l’indice del carattere desiderato.
Le stringhe in Rust sono codificate in UTF-8. Un singolo carattere UTF-8 non è obbligatoriamente grande un singolo byte come sarebbe una stringa codificata in ASCII. Ogni byte è chiamato una “unità di codice” (nello UTF-16, le unità di codice sono di 2 byte, nello UTF-32 sono di 4 byte). I “punti di codice” sono composti di una o più unità di codice, combinate in “gruppi di grafemi” che fedelmente ricalcano il concetto tradizionale di caratteri.
Anche con la possibilità di indicizzare i byte in una stringa UTF-8, non puoi accedere all’i
-esimo elemento del gruppo di grafemi in un tempo costante.
Ad ogni modo, se conosci a quale byte si trova il punto di codice o gruppo di grafemi desiderato, quindi puoi accedervi in tempo costante.
Le funzioni, incluse str::find()
e le espressioni regolari ritornano indici dei byte, facilitando questo tipo di accesso.
Le str
sono UTF-8 perché nel mondo questa codifica è frequente - specialmente in trasmissioni di rete, che ignorano l’ordine di bit della piattaforma - pensiamo quindi sia meglio che il trattamento standard dell’I/O non preveda la ricodifica in entrambe le direzioni.
Questo significa che individuare un particolare punto di codice Unicode dentro una stringa è un’operazione O(n), anche se, conoscendo l’indice del byte ci si può accedere in un tempo O(1) come previsto. Sotto un certo punto di vista questo è chiaramente sconveniente; ma d’altro canto questo problema è pieno di compromessi e vorremmo sottolineare alcune precisazioni importanti:
Scorrere una str
per valori della codifica ASCII può essere fatto in sicurezza byte per byte,
utilizzando .as_bytes()
ed estraendo u8
con un costo O(1)
e producendo valori che possono essere trasformati e comparati con la codifica ASCII con il tipo char
.
Quindi se per (ad esempio) vogliamo andare a capo ad ogni '\n'
, una ricerca byte a byte continua a essere funzionante, grazie alla flessibilità dello standard UTF-8.
La maggior parte delle operazioni orientate ai caratteri sul testo funzionano su presupposti molto ristretti come ad esempio l’esclusione dei caratteri non ASCII. Anche in questo caso all’esterno della codifica ASCII si tende a utilizzare comunque un algoritmo complesso(con complessità non costante) per determinare i bordi di caratteri, parole e paragrafi. Noi consigliamo di utilizzare un algoritmo “onesto”, approvato da Unicode e adattato al linguaggio da considerare.
Il tipo char
è UTF-32.
Se hai la certezza di dover richiedere l’analisi di un “punto di codice” alla volta è semplicissimo scrivere type wstr = [char]
e caricarci dentro una str
in un solo passaggio, per poi lavorare con il nuovo wstr
.
In altre parole: il fatto che il linguaggio non decodifichi a UTF-32 come prima opzione non ti deve inibire da decodificare(o ri-codificare per il processo inverso) i caratteri se necessiti di lavorare in quella codifica.
Per una spiegazione più dettagliata su perché UTF-8 è preferibile rispetto a UTF-16 o UTF-32, leggi il manifesto di UTF-8 Everywhere.
Rust ha quattro paia di tipi stringa, ciascuno assolve un ruolo distinto. In ciascun paio, c’è un tipo stringa “posseduto” e un tipo stringa “partizione”. I tipi sono suddivisi così:
tipo “Partizione” | tipo “Posseduto” | |
---|---|---|
UTF-8 | str |
String |
Compatibile con il SO | OsStr |
OsString |
Compatibile con il C | CStr |
CString |
Percorso di sistema | Path |
PathBuf |
I diversi tipi di stringa di Rust assolvono ruoli diversi.
String
e str
sono stringhe ad uso generico codificate in in UTF-8.
OsString
e OsStr
sono codificati a seconda della piattaforma corrente e sono utilizzati per interagire con il sistema operativo.
CString
e CStr
sono l’equivalente in Rust delle stringhe del C, si utilizzano nelle chiamate FFI, mentre PathBuf
e Path
sono un’aggiunta di OsString
e OsStr
ed implementano metodi specifici alla manipolazione di percorsi.
&str
che String
?
Ci sono diverse opzioni, a seconda delle necessità della funzione:
Into<String>
.AsRef<str>
.Cow<str>.
Usare Into<String>
In questo esempio, la funzione accetta sia stringhe possedute che partizioni di stringa o facendo nulla o convertendo l’ingresso in stringa posseduta. Nota che la conversione deve essere fatta esplicitamente e non succede altrimenti.
fn accetta_entrambe<S: Into<String>>(s: S) {
let s = s.into(); // Questo converte s in una `String`.
// ... il resto della funzione
}
Usare AsRef<str>
In questo esempio, la funzione accetta sia stringhe possedute che partizioni di stringa o facendo nulla o convertendo l’ingresso in una partizione di stringa. Questo viene fatto automaticamente prendendo l’ingresso per riferimento, come qui:
fn accetta_entrambe<S: AsRef<str>>(s: &S) {
// ... il corpo della funzione
}
Usare Cow<str>
In questo esempio, la funzione accetta un Cow<str>
, che non è un tipo generico ma un contenitore, contenente o una stringa posseduta o una partizione di stringa all’occorrenza.
fn accetta_cow(s: Cow<str>) {
// ... il corpo della funzione
}
If vuoi implementare queste strutture dati per utilizzarle in altri programmi non è necessario, essendo implementazioni efficienti di queste strutture già disponibili nella libreria standard.
Se invece, vuoi solo imparare, probabilmente dovrai entrare nel codice insicuro. Anche se è possibile implementarle solo con codice sicuro, le prestazioni sarebbero probabilmente peggiori di come sarebbe stato lo stesso codice con l’utilizzo di codice insicuro. La semplice ragione per ciò è che le strutture dati come vettori e liste concatenate fanno affidamento a operazioni su puntatori e memoria che sono proibiti nel Rust sicuro.
Per esempio, una lista concatenata doppia richiede due riferimenti mutabili a ciascun nodo ma questo viola le regole di Rust sull’esclusività dei riferimenti mutabili.
Si può risolvere il problema utilizzando Weak<T>
, ma le prestazioni sarebbero probabilmente peggiori di quanto desiderato.
Con il codice insicuro puoi ignorare le regole di esclusività ma devi verificare manualmente che il tuo codice non introduca violazioni nella sua gestione della memoria.
Il modo più semplice è utilizzare l’implementazione del tratto IntoIterator
.
Ecco qui un esempio con &Vec
:
let v = vec![1,2,3,4,5];
for oggetto in &v {
print!("{} ", oggetto);
}
println!("\nLunghezza: {}", v.len());
I cicli for
in Rust chiamano la funzione into_iter()
(definita dal tratto IntoIterator
) per qualsiasi cosa stiano analizzando.
Tutto ciò che implementa il tratto IntoIterator
può essere traversato con un ciclo for
.
IntoIterator
è implementato per &Vec
e &mut Vec
, obbligando l’iteratore generato da into_iter()
a prendere in prestito i contenuti della collezione, al posto di consumarli o muoverli.
Lo stesso è vero per le altre collezioni della libreria standard.
Se si desidera un iteratore che muova/consumi i valori, basta scrivere lo stesso ciclo for
omettendo &
o &mut
.
Se si necessita accesso diretto all’iteratore, vi si può usualmente accedere invocando il metodo iter()
.
Non è necessario. Se dichiari direttamente un vettore, la dimensione è dedotta dal numero di elementi. Se invece dichiari una funzione che accetta un vettore di dimensione prefissata il compilatore deve avere modo di sapere quale sarà la dimensione di quel vettore.
Una cosa da notare è che attualmente Rust non offere generici su array di dimensioni diverse.
Se desideri quindi accettare un contenitore continuo di un numero variabile di valori, utilizza Vec
o una partizione (a seconda che tu richieda il possesso o no).
Esistono almeno quattro diverse opzioni (largamente discusse in Too Many Linked Lists):
Rc
e Weak
per permettere il possesso condiviso dei nodi,
anche se questo approccio richiede la gestione della memoria.unsafe
e i puntatori.
Anche se efficiente questo metodo ignora i paradigmi di sicurezza di Rust.UnsafeCell
. Esistono spiegazioni e codice per questo metodo.Si può fare ma è inutile. La struttura diventa permanentemente prestata a se stessa e quindi non può essere copiata. Ecco del codice per capire meglio:
use std::cell::Cell;
#[derive(Debug)]
struct Immobile<'a> {
x: u32,
y: Cell<Option<&'a u32>>,
}
fn main() {
let test = Immobile { x: 42, y: Cell::new(None) };
test.y.set(Some(&test.x));
println!("{:?}", test);
}
Sono parole diverse per dire la stessa cosa.
In tutti i casi significa che il valore è stato trasferito a un nuovo proprietario, o che la proprietà è stata trasferita dal possessore originario, che quindi non può più accedervi.
Se un tipo implementa il tratto Copy
, il valore del proprietario originale non viene invalidato e può essere utilizzato nuovamente.
Se un tipo implementa il tratto Copy
, esso verrà copiato durante il suo passaggio a una funzione.
Tutti i tipi numerici in Rust implementano Copy
ma le strutture in maniera predefinita non implementano Copy
, quindi invece di essere copiate sono mosse.
Ciò implica che la struttura non possa più essere utilizzata altrove, se non viene ritornata dalla funzione tramite un return
.
Questo errore significa che il valore che stai cercando di utilizzare è stato trasferito a un nuovo proprietario.
La prima cosa da controllare in questo caso è se il trasferimento era davvero necessario: se il valore era stato mosso a una funzione, potrebbe essere possibile riscriverla per utilizzare un riferimento invece che il trasferimento, ad esempio.
Altrimenti se il tipo mosso implementa il tratto Clone
, chiamare clone()
su di esso prima di muoverlo trasferirà una sua copia, mantenendo l’originale disponibile per utilizzi futuri.
Nota che la clonazione di un valore dovrebbe essere l’ultima spiaggia, essendo la procedura di clonazione costosa e causa di allocazioni di memoria.
Se il valore mosso è uno dei tuoi tipi personalizzati, considera implementare il tratto Copy
(per la copia implicita al posto del trasferimento) o Clone
(copia esplicita).
Copy
è generalmente implementato con la direttiva #[derive(Copy, Clone)]
(Copy
richiede Clone
), e Clone
con la direttiva #[derive(Clone)]
.
Se nulla di tutto questo è possibile, probabilmente devi modificare la funzione che ha acquisito il possesso per fare in modo che restituisca la proprietà alla sua uscita.
self
, &self
o &mut self
nella dichiarazione di un metodo?
self
quando una funzione deve consumare il valore&self
quando una funzione necessita solo di una copia di sola lettura del valore&mut self
quando una funzione necessita di modificare un valore ma senza consumarloIl gestore dei prestiti, mentre analizza il codice, applica poche semplici regole, che sono illustrate nella sezione del libro di Rust dedicata ai prestiti. Queste regole sono:
Prima cosa, ogni prestito deve durare per un periodo di tempo non superiore alla vita del possessore originale. Seconda cosa, puoi avere accesso a uno o l’altro di questi due tipi di prestiti, ma non a entrambi contemporaneamente:
- uno o più riferimenti (&T) a una risorsa.
- esattamente un riferimento mutabile (&mut T)
Nonostante le regole siano molto semplici, seguirle correttamente può essere molto complicato, specialmente per coloro che non sono abitutati a ragionare in termini di campi di esistenza e possesso.
La prima regola è comprendere che il gestore dei prestiti identifica veramente gli errori che produce: molto lavoro è stato profuso per renderlo un assistente di qualità nella risoluzione delle problematiche che individua. Quando incontri un problema segnalato dal gestore dei prestiti, il primo passo è di leggere lentamente e con attenzione l’errore e poi approcciarsi al codice una volta compreso davvero l’errore descritto.
Il secondo passo è cercare di familiarizzare con i tipi contenitore associati con i concetti di possesso e mutabilità forniti dalla libreria standard di Rust, includendo Cell
, RefCell
e Cow
.
Questi utili e necessari strumenti possono aiutare ad esprimere efficientemente alcune situazioni complesse di possesso e mutabilità.
La singola cosa più importante nella comprensione del gestore dei prestiti è la pratica. La potente analisi statica fatta da Rust è particolare e molto differente da molte esperienze di programmazione precedente. Ci vorrà un po’ di tempo per acquisire la giusta tranquillità.
Se ti ritrovi in difficoltà con il gestore dei prestiti, oppure hai finito la pazienza, sentiti libero di chiedere un aiuto alla comunità di Rust.
Rc
?
Questo è coperto dalla documentazione ufficiale per Rc
il tipo di puntatore non-atomico utilizzante il reference counting di Rust.
In breve, Rc
e il suo cugino, amico del multithreading Arc
sono utili per indicare possesso condiviso e vengono deallocati automaticamente dal sistema quando nessuno vi accede.
Per ritornare una chiusura da una funzione, essa deve essere una “chiusura da movimento”, ovvero che essa deve essere dichiarata dalla parola move
.
Come illustrato nel libro di Rust, questo fornisce alla chiusura la sua copia delle variabili catturate, indipendentemente dal quadro di allocazione del chiamante.
Altrimenti, ritornare da una chiusura sarebbe insicuro, visto che permetterebbe di accedere a variabili non più disponibili; detto in un altro modo: permetterebbe di leggere dati da locazioni di memoria errate.
La chiusura deve anche essere racchiusa in un Box
, in modo da essere allocata nella heap.
Puoi saperne di più nel libro.
Un deriferimento forzato è una pratica conversione automatica di delle referenze
a puntatori (es., &Rc<T>
or &Box<T>
) a referenze ai loro contenuti
(es., &T
).
Il deriferimento forzato esiste per rendere l’utilizzo di Rust più comodo e sono implementati dal tratto Deref
.
Una implementazione di Deref
indica che il tipo implementante potrebbe essere convertito in un valore con una chiamata al metodo deref
, che prende un riferimento immutabile al tipo chiamante e ritorna un riferimento(senza violare peró il campo di esistenza) del tipo obiettivo.
L’operatore *
, se utilizzato come prefisso è un metodo più veloce per accedere a deref
.
Sono chiamate “forzature” a causa delle regole spiegate qui nel libro di Rust:
Se hai un tipo
U
ed esso implementaDeref<Target=T>
, i valori&U
verranno automaticamente convertiti in&T
.
Ad esempio, se hai un &Rc<String>
, esso verrà forzato per questa regola a &String
, che può essere forzato anche a &str
nello stesso modo.
Quindi se una funzione accettasse un parametro &str
, puoi passare direttamente un &Rc<String>
e tutte le forzature e verranno gestite automaticamente dal tratto Deref
.
I tipici deriferimenti forzati sono:
&Rc<T>
a &T
&Box<T>
a &T
&Arc<T>
a &T
&Vec<T>
a &[T]
&String
a &str
I campi di esistenza sono la soluzione di Rust al problema della sicurezza della memoria. Questi consentono a Rust di assicurare la sicurezza della memoria senza i costi prestazionali della garbage collection. Sono basati su una serie di articoli accademici che possono essere trovati nel libro di Rust.
La sintassi 'a
proviene dalla famiglia ML di linguaggi di programmazione, dove 'a
viene utilizzato per indicare un parametro di tipo generico.
In Rust, la sintassi doveva rappresentare qualcosa di univoco, chiaramente visibile e integrato con le altre dichiarazioni dei tipi insieme ai vari tratti e riferimenti.
Sono state prese in considerazione anche altre scelte ma nessuna sintassi alternativa si è dimostrata chiaramente migliore.
Devi assicurarti che il campo di esistenza del valore prestato sia più lungo di quello della funzione. Puoi ottenere questo effetto puoi assegnare il campo di esistenza dell’uscita a quello di un parametro di ingresso come qui:
type Gruppo = TypedArena<Cosa>;
// (Il campo di esistenza sotto è esplicitato esclusivamente
/// per facilitarne la comprensione; esso può essere omesso
// tramite le regole di elisione consultabili in un'altra
// risposta presente in questa pagina)
fn crea_prestato<'a>(gruppo: &'a Gruppo,
x: i32,
y: i32) -> &'a Cosa {
gruppo.alloc(Cosa { x: x, y: y })
}
Un’alternativa è eliminare interamente il riferimento ritornando un valore posseduto come String
:
fn buon_compleanno(nome: &str, eta: i64) -> String {
format!("Ciao {}! Hai {} anni!", nome, eta)
}
Questo approccio è più semplice ma spesso genera allocazioni in memoria non necessarie.
&'a T
e altri no, tipo &T
?
In realtà, tutti i riferimenti hanno un campo di esistenza ma nella maggior parte dei casi non ti devi preoccupare di gestirlo esplicitamente. Le regole sono le seguenti:
&self
o un &mut self
, il campo di esistenza
di self
viene assegnato a tutti i campi di esistenza di uscita.struct
o enum
i campi di esistenza sono da dichiarare espressamente.Se queste regole danno origine a un errore di compilazione, il compilatore di Rust fornirà un messaggio di errore indicante l’errore e anche una potenziale soluzione basata sugli algoritmi di deduzione.
L’unico modo di creare un valore &Cosa
or &mut Cosa
è di specificare un valore preesistente di tipo Coso
a cui la referenza deve fare riferimento.
Il riferimento in questo modo ottiene in prestito il valore originale per un determinato blocco di codice(il campo di esistenza del riferimento) e il valore prestato non può essere spostato o distrutto per tutta la durata del prestito.
null
?
Puoi fare ciò con il tipo Option
che può alternativamente essere Some(T)
o None
.
Some(T)
indica che un valore di tipo T
è contenuto all’interno, mentre None
ne indica l’assenza.
La monomorfizzazione specializza ciascun utilizzo di una funzione(o struttura) generica in base ai tipi di parametri di ciascuna chiamata(o agli utilizzi della struttura).
Durante la monomorfizzazione viene generata una nuova versione specializzata della funzione per ciascun set univoco di tipi. Questa strategia, già utilizzata nel C++, genera del codice macchina efficiente, specializzato per ciascuna chiamata e invocato staticamente, con lo svantaggio che una funzione istanziata con tanti tipi diversi può dare luogo a molta duplicazione nel codice generato, generando quindi eseguibili più grandi rispetto ad altre strategie di traduzione.
Le funzioni che accettano oggetti caratterizzati solo da tratti invece dei tipi non sono soggette alla monomorfizzazione. Invece, i metodi invocati su oggetti tratto sono gestiti dinamicamente durante l’esecuzione.
Le funzioni e le chiusure si utilizzano allo stesso modo ma hanno una gestione differente in fase di esecuzione a causa di una implementazione diversa.
Le funzioni sono un costrutto fondamentale del linguaggio, mentre le chiusure sono essenzialemente un modo più semplice per indicare uno di questi tre tratti: Fn
, FnMut
e FnOnce
.
Quando scrivi una chiusura, il compilatore di Rust automaticamente provvede a generare una struttura implementante il tratto più idoneo tra quei tre e a catturare le variabili corrette come membri,
generando anche la possibilità di utilizzare la struttura come una funzione. Le strutture, al contrario, non catturano alcuna variabile.
La fondamentale differenza tra questi tratti è come acquisiscono il parametro self
.
Fn
prende &self
, FnMut
prende &mut self
mentreFnOnce
prende self
.
Anche se una cattura non cattura alcuna variabile di ambiente, viene rappresentata in fase di esecuzione tramite due puntatori, come qualsiasi altra chiusura.
I tipi di più alto livello sono tipi con parametri non specificati. I costruttori di tipi come Vec
, Result
e HashMap
sono tutti esempi di tipi di più alto livello:
ciascuno richiede alcuni tipi aggiuntivi per poter denotare effettivamente il suo tipo, come nel caso di Vec<u32>
.
Il supporto per i tipi di alto livello significa che questi tipi “incompleti” possono essere utilizzati ovunque possano essere utilizzati anche i tipi “completi”, non escludendo le funzioni generiche.
Ogni tipo completo, come i32
, bool
, o char
è di tipo *
(questa notazione deriva dalla teoria correlata con il sistema dei tipi).
Un tipo a parametro singolo, come Vec<T>
è invece * -> *
, ovvero che vec Vec<T>
accetta un tipo completo come i32
e ritorna il tipo completo Vec<i32>
.
Un tipo con tre parametri, come HashMap<K, V, S>
è di tipo * -> * -> * -> *
perché accetta tre tipi completi (come i32
, String
e RandomState
) per generare un nuovo tipo completo HashMap<i32, String, RandomState>
.
In aggiunta a questi esempi, i costruttori di tipo possono accettare dei parametri sul campo di esistenza, che denoteremo con Lt
.
Ad esempio slice::Iter
ha il tipo Lt -> * -> *
, perchè va istanziato ad esempio come Iter<'a, u32>
.
La mancanza di supporto per i tipi di più alto livello rende difficile scrivere alcuni tipi di codice generico. Risulta particolarmente problematico astrarre su concetti come gli iteratori, dato che essi sono spesso parametrizzati nei confronti di uno specifico campo di esistenza. Queste premesse hanno impedito la creazione di tratti che astraggano ulteriormente le collezioni presenti in Rust.
Un altro esempio frequente è da ricercare nei concetti di functor e monad, entrambi dei quali sono costruttori di tipi, invece che tipi individuali.
Rust al momento non possiede supporto per i tipi di più alto livello perché non è stato prioritizzato lo sviluppo di questa funzione rispetto ad altre funzionalità che rispecchiano meglio gli obiettivi del progetto. Essendo la progettazione di funzionalità importanti come queste un campo minato, vorremmo procedere con cautela, non c’è un’altra ragione particolare sul perché Rust non possiede questa funzionalità.
<T=Foo>
nel codice generico?
Questi sono chiamati tipi associati, permettono di indicare limitazioni di tratto non esprimibili con un costrutto where
.
Ad esempio, una limitazione generica X: Bar<T=Foo>
significa che X
deve implementare il tratto Bar
e in tale implementazione di bar Bar
, X
deve scegliere Foo
come il tipo associato di Bar
, T
.
Gli esempi in cui una limitazione di tal genere non è esprimibile con un costrutto where
includono i tipi tratto come Box<Bar<T=Foo>>
.
I tipi associati esistono perché spesso i generici riguardano famiglie di tipi, dove un tipo determina tutti gli altri membri. Per esempio, un tratto per grafi potrebbe avere per Self
il grafo stesso e avere dei tipi correlati per i suoi nodi e vertici. Ciascun tipo grafo identifica univocamente i tipi associato, rendendo molto più conciso lavorare con questi tipi di strutture e fornendo anche una migliore gestione sulla deduzione dei tipi in molti casi.
Puoi personalizzare l’implementazione di una varietà di operatori utilizzando i loro tratti associati: Add
per il +
, Mul
per il *
e via dicendo. Si può fare così:
use std::ops::Add;
struct Foo;
impl Add for Foo {
type Uscita = Foo;
fn add(self, rhs: Foo) -> Self::Uscita {
println!("Sommando!");
self
}
}
I seguenti operatori possono essere sovrascritti:
Operation | Trait |
---|---|
+ |
Add |
+= |
AddAssign |
- binario |
Sub |
-= |
SubAssign |
* |
Mul |
*= |
MulAssign |
/ |
Div |
/= |
DivAssign |
unary - |
Neg |
% |
Rem |
%= |
RemAssign |
& |
BitAnd |
| |
BitOr |
| = |
BitOrAssign |
^ |
BitXor |
^= |
BitXorAssign |
! |
Not |
<< |
Shl |
<<= |
ShlAssign |
>> |
Shr |
>>= |
ShrAssign |
* |
Deref |
mut * |
DerefMut |
[] |
Index |
mut [] |
IndexMut |
Eq
/PartialEq
e Ord
/PartialOrd
?
Ci sono alcuni tipi in Rust i cui valori sono solo parzialmente ordinati oppure hanno relazioni di equivalenza parziali. Ordinamento parziale significa che potrebbero esserci valori di quel tipo che non sono né più piccoli né più grandi di un altro. Uguaglianza parziale significa che ci potrebbero essere dei valori di un certo tipo che non sono uguali a loro stessi.
I tipi a virgola mobile (f32
e f64
) sono un buon esempio di questo. Ogni tipo in virgola mobile potrebbe avere il valore NaN
(ovvero “non un numero”). NaN
non è uguale a se stesso (NaN == NaN
è falso) e nemmeno più grande o più piccolo di un qualsiasi valore.
Di conseguenza sia f32
che f64
implementano PartialOrd
e PartialEq
ma non Ord
e nemmeno Eq
.
Come spiegato nella precedente domanda sui numeri in virgola mobile, queste distinzioni sono importanti perchè alcune collezioni fanno affidamento sul totale ordinamento/uguaglianza per funzionare.
String
?
Usando la funzione read_to_string()
, definita nel tratto Read
di std::io
.
use std::io::Read;
use std::fs::File;
fn leggi_file(path: &str) -> Result<String, std::io::Error> {
let mut f = try!(File::open(path));
let mut s = String::new();
try!(f.read_to_string(&mut s)); // `s` contiene il contenuto di "foo.txt"
Ok(s)
}
fn main() {
match leggi_file("foo.txt") {
Ok(_) => println!("Letti i contenuti del file!"),
Err(err) => println!("Non sono riuscito a leggere i contenuti del file, errore: {}", err)
};
}
Il tipo File
implementa il tratto Read
che include una moltitudine di funzioni per leggere e scrivere, includendo read()
, read_to_end()
, bytes()
, chars()
e take()
.
Ciascuna di queste funzioni legge un pochino dal file.
read()
legge quanto il sottostante sistema di input fornisce.
read_to_end()
legge l’intero buffer in un vettore, allocando lo spazio necessario. bytes()
e chars()
permettono rispettivamente di iterare sui byte e caratteri del file, respectively.
Inoltre, take()
permette di leggere un numero arbitrario di byte dal file. Insieme, questi metodi permettono di leggere efficientemente ogni tipo di file.
Per le letture con buffer utilizza la struttura BufReader
che aiuta a ridurre il carico di lavoro al sistema durante la lettura.
Ci sono molte librerie che forniscono input / output asincroni in Rust, come mioco, coio-rs e rotor.
Il modo più semplice è utilizzare Args
, che fornisce un iteratore sui parametri da riga di comando.
Se stai cercando qualcosa di più potente, ci sono una serie di librerie disponbili su crates.io.
Le eccezioni complicano la comprensione del flusso del programma, esprimono validità o invalidità all’infuori del sistema dei tipi e funzionano male in ambienti multicore(un obiettivo primario per Rust).
Rust preferisce un approccio alla gestione degli errori basato sui tipi come spiegato nel dettaglio nel libro. Questo è più compatibile con il flusso di controllo tipico di Rust, la concorrenza e tutto il resto.
unwrap()
ovunque?
unwrap()
è una funzione che estrae il valore da una Option
o un Result
e va in errore se il valore non è presente.
unwrap()
non dovrebbe essere il tuo modo principale per gestire gli errori prevedibili, tipo un errore nell’input dell’utente.
Nel tuo codice, dovrebbe essere trattato come test per la non nullità di un valore, pena il mandare in errore il programma.
Viene utilizzato anche per provare velocemente quando non si vuole ancora gestire tutti i casi o negli articoli, quando la gestione degli errori potrebbe distrarre dal resto.
try!
?
Quasi sicuramente è un problema con il tipo ritornato dalla funzione. La macro try!
estrae un valore da Result
o ritorna con l’errore portato in Result
.
Ció significa che try
vale solo per le funzioni che ritornano un Result
, dove il tipo costruito Err
implementa From::from(err)
.
In particolare ciò significa che la macro try!
non è utlizzabile nella funzione main
.
Result
ovunque?
Se stai cercando un modo per evitare di gestire i Result
nel codice di altre persone, puoi sempre utilizzare unwrap()
ma probabilmente non è ciò che desideri.
Result
indica che una qualche operazione potrebbe fallire. Richiedere di gestire esplicitamente questi problemi è uno dei tanti modi in cui Rust incoraggia la scrittura di programmi affidabili.
Rust fornisce degli strumenti come la macro try!
per gestire in ergonomia queste situazioni.
Se davvero desideri non gestire un errore, utilizza unwrap()
ma sappi che fare ciò implica che il codice arresterà la sua esecuzione in caso di fallimento, usualmente terminando il processo.
unsafe
?
La mutabilità è sicura solo se sincronizzata. Mutare un Mutex
statico (inizializzato tramite il pacchetto lazy-static) non richiede un blocco di codice unsafe
come non lo richiede la modifica di un AtomicUsize
(inizializzabile anche senza lazy_static).
Piú in generale, se un tipo implementa il tratto Sync
e non implementa Drop
, esso è utilizzabile in una static
.
Al momento no. Le macro di Rust sono “hygienic macros” che intenzionalmente evitano la cattura o la creazione di identificatori che potrebbero collidere con altri. Le loro capacità sono significativamente differenti dagli stili delle macro normalmente associate ai preprocessori C. Le invocazioni delle macro possono comparire sono il luoghi dove sono esplicitamente supportate: oggetti, dichiarazioni, espressioni e motivi. Dove per “dichiarazioni” si intende uno spazio dove è possibile inserire un metodo. Non si possono utilizzare le macro per completare una dichiarazione avventura parzialmente e seguendo la stessa logica, nemmeno per completare dichiarazioni parziali di variabili.
Si possono debuggare con gdb o lldb, allo stesso modo di C e C++. In realtà, ogni installazione di Rust viene fornita con uno o entrambi tra rust-gdb e rust-lldb(a seconda della piattaforma). Questi due componenti estendono gdb e lldb con funzioni per permettere una migliore esperienza.
rustc
ha detto che del codice della libreria standard è andato in crash, come faccio a trovare il problema?
Questo errore è spesso causato dall’utilizzo di unwrap()
su un None
o Err
.
Abilitando la variabile di ambiente RUST_BACKTRACE=1
potresti ottenere ulteriori informazioni.
Potrebbe essere di aiuto anche la compilazione in modalità debug (predefinita per il comando cargo build
).
Si possono anche utilizzare i sopracitati rust-gdb
o rust-lldb
.
Ci sono molte opzioni per sviluppare in Rust, tutte illustrate sulla pagina ufficiale sul supporto agli ambienti di sviluppo.
gofmt
è fantastico. Dov'è rustfmt
?
rustfmt
è proprio qui, sta venendo sviluppato proprio per permettere di rendere il codice Rust il più semplice e prevedibile possibile.
memcpy
?
Se vuoi clonare una partizione esistente in sicurezza, puoi usare clone_from_slice
.
Per copiare byte potenzialmente in conflitto usa copy
.
Per copiare byte non in conflitto usa copy_nonoverlapping
.
Entrambe le funzioni elencate sono unsafe
, visto che possono eludere le garanzie di sicurezza. Sono quindi da utilizzare con attenzione.
Si. I programmi Rust possono scegliere di non caricare la libreria standard utilizzando l’attributo #![no_std]
.
Una volta impostato, è ancora possibile utilizzare la libreria chiave di Rust, composta esclusivamente da primitivi indipendenti dalla piattaforma di esecuzione. Essa non include IO, concorrenza, allocazioni nella heap, ecc.
Si! In realtà al momento ci sono molti progetti che stanno facendo proprio questo.
i32
o f64
in formato big-endian o little-endian in un file o un flusso di bit?
Dovresti provare il pacchetto byteorder, che fornisce strumenti proprio per quello.
Non in maniera predefinita. In generale, enum
e struct
non sono definiti.
Questo per permettere al compilatore di effettuare delle ottimizzazioni tipo riutilizzare la distanziatura per il discriminante, compattare le varianti di enum
annidate, riordinare campi per eliminare spaziature, ecc.
Le enum
prive di dati (“simil-C”) possono avere rappresentazione definita. Tali enum
sono facilmente distinte dal fatto che sono semplicemente una lista di nomi senza dati:
enum SimilC {
A,
B = 32,
C = 34,
D
}
L’attributo #[repr(C)]
se applicato a tali enum
gli fornisce la stessa rappresentazione che avrebbero avuto nel C.
Questo permette nella maggior parte dei casi di utilizzare le enum
di Rust nella FFI insieme alle enum
fornite dal C.
Tale attributo è applicabile alle struct
per ottenere la stessa rappresentazione delle struct
del C.
I comportamenti specifici alla piattaforma sono esprimibili utilizzando attributi di compilazione condizionale come ad esempio target_os
, target_family
, target_endian
, ecc.
Si! Ci sono già alcuni esempi utilizzanti Rust sia per Android che per iOS. Richiede un pochino di lavoro di preparazione ma Rust funziona correttamente su entrambe le piattaforme.
Non ancora ma sono in corso degli sforzi per permettere di compilare Rust per il web con Emscripten.
La compilazione incrociata è possibile in Rust ma richiede alcune accortezze per essere impostata. Ogni compilatore Rust permette anche la compilazione incrociata ma le librerie necessitano di essere ricompilate per ogni piattaforma obiettivo.
Rust distribuisce copie della libreria standard per ciascuna delle piattaforme supportate, ritrovabili nei file rust-std-*
presenti nella pagina citata ma ad oggi non esistono metodi automatizzati per installarle.
use
?
Ci sono diverse possibilità ma un errore comune è non comprendere che le dichiarazioni use
sono relative al livello base del pacchetto.
Prova a riscrivere le tue dichiarazioni in modo che utilizzino i percorsi relativi alla cartella base del pacchett per provare a risolvere il problema.
Ci sono anche self
e super
, che rendono i percorsi di use
riferiti rispettivamente al modulo corrente o al modulo padre.
Per ulteriori informazioni su come utilizzare use
, leggi il capitolo del libro di Rust “Crates and Modules”.
mod
al posto di poterli invocare con use
direttamente?
Ci sono due modi per dichiarare i moduli in Rust, in linea o in un altro file. Ecco un esempio:
// Dentro a main.rs
mod ciao {
pub fn f() {
println!("ciao!");
}
}
fn main() {
ciao::f();
}
// Dentro a main.rs
mod ciao;
fn main() {
ciao::f();
}
// Dentro a ciao.rs
pub fn f() {
println!("ciao!");
}
Nel primo esempio, il modulo è definito nello stesso file in cui è utilizzato, nel secondo, la dichiarazione del modulo dice al compilatore di cercare o il file ciao.rs
o ciao/mod.rs
e di caricarlo.
Notare la differenza tra mod
e use
: mod
dichiara l’esistenza di un modulo, mentre use
fa riferimento a un modulo dichiarato altrove, rendendone accessibili i suoi contenuti all’interno del modulo corrente.
Come spiegato nella guida alla configurazione di Cargo, può essere impostato un proxy impostando la variabile “proxy” sotto [http]
nel file di configurazione.
use
sul pacchetto che la contiene?
Per i metodi definiti su un tratto, devi esplicitamente importare la dichiarazione del tratto. Questo significa che non è sufficiente importare un modulo dove una struct
implementa un tratto, bisogna importare anche il tratto stesso.
use
da solo?
Probabilmente potrebbe ma non lo vorresti. Mentre in molti casi è probabile che il compilatore possa determinare correttamente il modulo da importare guardando le definizioni questo potrebbe non applicarsi a tutte le casistiche.
Ogni decisione fatta da rustc
genererebbe sopresa e confusione in alcuni casi e Rust preferisce essere esplicito riguardo all’origine dei nomi.
Ad esempio, il compilatore potrebbe decidere che nel casi due identificatori fossero in conflitto sia da preferire l’identificatore la cui dichiarazione è meno recente.
In questo caso sia il modulo foo
che il modulo bar
definiscono l’identificatore baz
ma foo
viene registrato per primo e quindi il compilatore inserirebbe use foo::baz;
.
mod foo;
mod bar;
// use foo::baz // ciò che sarebbe inserito.
fn main() {
baz();
}
Sapendo questa dinamica, probabilmente risparmieresti qualche carattere ma aumenteresti anche la possibilità di generare degli errori imprevisti quando in realtà al posto di baz()
intendevi bar::baz()
, diminuendo anche la leggibilità del codice,
avendo reso la chiamata alla funzione dipendente dall’ordine di dichiarazione. Questi sono compromessi che Rust non ha intenzione di prendere.
Ad ogni modo, in futuro, un ambiente di sviluppo integrato potrebbe assistere nella gestione delle dichiarazioni, fornendo il massimo di entrambi i mondi: assistenza automatica nelle dichiarazioni ma chiarezza sulle origini dei nomi importati.
Puoi importare librerie dinamiche in Rust con libloading, che fornisce un sistema multipiattaforma per il link dinamico.
Citando la spiegazione ufficiale sul design di https://crates.io:
Nel primo mese di crates.io, un buon numero di persone hanno richiesto la possibilità di introdurre pacchetti con spazi dei nomi.
Mentre questi permettono a autori multipli di utilizzare un singolo nome generico, aggiungono complessità su come i pacchetti vengono indicati nel codice Rust e nella comunicazione su di essi. A una prima occhiata questo permetterebbe a più persone di associarsi al nome
http
, ma questo implicherebbe che per riferirsi a due pacchetti di autori diversi si debbe parlare ad esempio dihttp di wycats
o dihttp di reem
, offrendo pochi vantaggi rispetto a nomi comewycats-http
oreem-http
.Inoltre, osservando questa scelta abbiamo scoperto che le persone tendono a utilizzare nomi più creativi (come
nokogiri
invece che “libxml2 di tendelove”). Questi nomi creativi tendono a essere brevi e memorabili, in parte anche grazie della mancanza di dipendenze da altri. Rendono anche più semplice parlare in modo conciso e non ambiguo di pacchetti, creando nuovi nomi altisonanti. Esistono diversi ecosistemi da oltre 10,000 pacchetti come NPM e RubyGems le cui comunità prosperano anche sotto un singolo spazio dei nomi.In breve, non pensiamo che l’ecosistema Cargo avrebbe giovamento se Piston scegliesse un nome come
bvssvni/game-engine
(permettendo ad altri di sceglierewycats/game-engine
) invece che semplicementepiston
.Proprio perché gli spazi dei nomi sono più complessi in diversi ambiti ed essendo la loro aggiunta possibile se necessario, per ora abbiamo intenzione di preservare un singolo spazio dei nomi condiviso.
La libreria standard non contiene un’implementazione di HTTP quindi dovrai utilizzare un pacchetto esterno. Hyper è la più popolare ma ce ne sono tante altre.
Ci sono molti modi per fare applicazioni con interfaccia grafica in Rust. Guarda questa lista di librerie per realizzare interfacce grafiche.
Serde è la libreria consigliata per serializzare e deserializzare di dati in Rust da e verso una moltitudine di formati.
Non ancora! Puoi farne una tu?
Glium è la principale libreria per utilizzare OpenGL in Rust. GLFW è un’altra opzione valida.
Certo! La principale libreria per programmare giochi in Rust è Piston ci sono sia un subreddit per la creazione di videogiochi in Rust e un canale IRC (#rust-gamedev
su Mozilla IRC).
Rust è multi paradigma. Molte cose possibili in linguaggi orientati agli oggetti sono possibili in Rust ma non proprio tutto e non sempre utilizzando un livello di astrazione uguale a quello a cui si è abituati.
Dipende. Esistono modi per convertire concetti orientati agli oggetti come ereditarietà multiple a Rust ma non essendo Rust orientato agli oggetti il risultato della conversione potrenne apparire sostanzialmente diverso dalla sua rappresentazione in un linguaggio orientato agli oggetti.
Il modo più semplice è utilizzare il tipo Option
in qualsiasi funzione venga utilizzata per costruire istanze della struttura (generalmente new()
).
Un altro modo è utilizzare il metodo del costruttore, dove alcune funzioni devono essere chiamate dopo la costruzione del tipo.
Le globali in Rust possono essere fatte utilizzando la dichiarazione const
per le globali computate al momento della compilazione, mentre static
è utilizzabile per globali mutabili.
Nota che la modifica di una variabile static mut
richiede unsafe
, visto che permette problemi di concorrenza, una cosa impossibile nel Rust sicuro.
Una differenza importante tra const
e static
è che si possono prendere riferimenti a valori static
ma non a valori const
se privi di posizione in memoria specificata.
Per ulteriori informazioni su const
e static
, leggi il libro di Rust.
Rust attualmente possiede un supporto limitato per le costanti al momento della compilazione.
Puoi definire dei primitivi con le dichiarazioni const
(simili a static
ma immutabili e senza una specifica locazione in memoria) funzioni const
e metodi correlati.
Per definire costanti procedurali che non possono essere definite tramite questi meccanismi usa il pacchetto lazy-static
, che emula l’assegnazione al momento della compilazione assegnando il valore al primo utilizzo.
Rust non consente l’esistenza di qualcosa prima di main
.
La cosa più vicina può essere fatta tramite il pacchetto lazy-static
, simulante una situazione “pre-main” in cui le variabili statiche vengono inizializzate al loro primo utilizzo.
No. Le globali non possono non avere un costruttore costante e non possiedono un destrutture. I costruttori statici sono sconvenienti perché assicurare un ordine di inizializzazione statico è complesso. Le situazioni “pre-main” sono spesso considerate sconvenienti e Rust non le consente.
Leggi anche il domande frequenti del C++ che fa menzione del “problema dell’ordine di inizializzazione per le static” e il blog di Eric Lippert per le problematiche in C#, che anche esso possiede queste funzioni.
Puoi emulare globali il cui valore non è costante con il pacchetto lazy-static.
struct X { static int X; };
?
Rust non possiede campi static
come nel codice sopra. Al loro posto puoi dichiarare una varibile static
a un determinato modulo, che viene preservata privata all’interno dello stesso.
Convertire una enum simil-C a un intero è possibile con l’espressione as
come in e as i64
(dove e
è una enum).
La conversione in altre direzioni può essere svolta tramite match
, che associa differenti valori numerici a differenti potenziali valori per la enum.
Ci sono diversi fattori che contribuiscono alla tendenza di Rust di avere in generale file binari più grandi di programmi funzionalmente equivalenti in C. In generale Rust si focalizza su ottimizzare le prestazioni di programmi reali, non le dimensioni di piccoli programmi.
Monomorfizzazione
Rust monomorfizza i generici, ovvero che viene generata una nuova versione di una funzione generica o tipo per ciascuna dichiarazione effettuata con tipi distinti. Questo assomiglia ai template in C++. Ad esempio, nel programma seguente:
fn foo<T>(t: T) {
// ... qualcosa
}
fn main() {
foo(10); // i32
foo("ciao"); // &str
}
Nel file eseguibile finale vi saranno due versioni diverse di foo
, una specifica al tipo in ingresso i32
e l’altra specifica al tipo in ingresso &str
.
Questo permette un efficiente dispacciamento statico della funzione generica ma aumentando le dimensioni dell’eseguibile finale.
Simboli di debug
I programmi Rust sono compilati con i simboli di debug inclusi, anche se in modalità rilascio. Questi sono utilizzabili per fornire informazioni in caso di crash e possono essere rimossi con strip
, o un qualsiasi altro strumento per la rimozione di simboli di debug.
Risulta utile sapere anche che compilare in modalità rilascio con Cargo equivale a impostare il livello di ottimizzazione 3 con rustc.
Un livello alternativo di ottimizzazione (chiamato s
o z
) aggiunto recentemente indica al compilatore di focalizzarsi invece che sulle prestazioni, sulle dimensioni dell’eseguibile finale.
Jemalloc
Rust utilizza jemalloc come il suo allocatore predefinito, questo aumenta le dimensioni dei binari compilati. Jemalloc è stato scelto perché è un allocatore consistente e di qualità con caratteristiche prestazionali preferibili rispetto agli allocatori forniti da molti sistemi. Al momento si stanno facendo esperimenti su come rendere più facile l’utilizzo di allocatori personalizzati ma la funzionalità non è stata ancora ultimata.
Ottimizzazione del linker
Rust di base non effettua ottimizzazioni al momento del linking ma gli più essere detto di farlo. Questo incrementa la quantità di ottimizzazioni effettuabili e può avere un effetto sulle dimensioni dei binari generati, questo effetto può essere amplificato se in combinazione con l’opzione di ottimizzazione per dimensioni sopracitata.
Libreria standard
La libreria standard di Rust include libbacktrace e libunwind, che potrebbero essere non volute in alcuni programmi.
Utilizzare #![no_std]
può quindi fornire dei binari più piccoli ma cambia in modo sostanziale il modo in cui il codice deve essere scritto.
Nota che utilizzare Rust senza la libreria standard è spesso funzionalmente vicino al codice C equivalente.
Per esempio, il programma seguente in C legge un nome e dice “ciao” alla persona con quel nome:
#include <stdio.h>
int main(void) {
printf("Come ti chiami?\n");
char input[100] = {0};
scanf("%s", input);
printf("Ciao %s!\n", input);
return 0;
}
Per riscrivere questo programma in Rust scriveresti una cosa del genere:
use std::io;
fn main() {
println!("Come ti chiami?");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
println!("Ciao {}!", input);
}
Questo programma, quando compilato e confrontato con il programma C avrà una dimensione maggiore e utilizzerà più memoria ma non è esattamente equivalente al codice C che lo precede. Il reale equivalente in realtà assomiglia di più a questo:
#![feature(lang_items)]
#![feature(libc)]
#![feature(no_std)]
#![feature(start)]
#![no_std]
extern crate libc;
extern "C" {
fn printf(fmt: *const u8, ...) -> i32;
fn scanf(fmt: *const u8, ...) -> i32;
}
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
unsafe {
printf(b"Come ti chiami?\n\0".as_ptr());
let mut input = [0u8; 100];
scanf(b"%s\0".as_ptr(), &mut input);
printf(b"Ciao %s!\n\0".as_ptr(), &input);
0
}
}
#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"] fn panic_fmt() -> ! { loop {} }
#[lang="stack_exhausted"] extern fn stack_exhausted() {}
Che dovrebbe certamente eguagliare il C nell’utilizzo della memoria, incrementando peró la complessità e rimuovendo le garanzie fornite dal codice Rust (evitate utilizzando unsafe
).
Dedicarsi a una ABI è una decisione importante che può limitare i cambiamenti vantaggiosi futuri. Dato che Rust ha raggiunto la versione 1.0 a Maggio 2015 è troppo presto per impegnarsi a costruire una ABI stabile. Ció peró non implica che non possa succedere nel futuro. (Nonostante questo il C++ è riuscito a vivere per molti anni senza specificare una ABI stabile.)
Il lemma extern
permette di usare con Rust specifiche ABI, come quella ben definita del C, per interoperare con altri linguaggi.
Si. Chiamare il C da Rust è progettato per avere la stessa efficienza delle chiamate di codice C dal C++.
Si. Il codice Rust deve essere sposto mediante una dichiarazione extern
, che lo rende compatibile con la ABI del C.
Tale funzione può essere passata al codice C come puntatore a una funzione o, se con l’attributo #[no_mangle]
chiamata direttamente dal C.
Il C++ moderno include molte funzioni che rendono la scrittura di codice sicuro e corretto meno prono ad errori ma non è perfetto e rimane comunque facile introdurre vulnerabilità. Gli sviluppatori del C++ stanno cercando di porre rimedio a queste problematiche ma il C++ è limitato da una lunga storia che impedisce di attuare molte idee che si vorrebbero sperimentare.
Rust è stato disegnato sin dal primo giorno per essere un linguaggio di programmazione per sistemi sicuro, questo significa che non è limitato da scelte pregresse che potrebbero impedire di raggiungere il corretto livello di sicurezza come il C++. In C++, la sicurezza si ottiene mediante una rigorosa disciplina personale ed è semplice commettere errori mentre in Rust, la sicurezza è predefinita. Rust permette quindi di lavorare in un gruppo di persone meno perfette di te, senza dover spendere il tuo tempo per controllare il codice altrui per controllare potenziali falle di sicurezza nel codice altrui.
Rust attualmente non ha un equivalente esatto alla specializzazione dei template ma ci si sta lavorando su e verrà probabilmente aggiunta a presto. Ad ogni modo, effetti simili si possono ottenere con i tipi associati.
I concetti base sono simili ma i due sistemi differiscono nella pratica. In entrambi i sistemi “muovere” un valore è un modo per trasferire il possesso delle risorse sottostanti. Ad esempio, muovere una stringa trasferisce il suo buffer al posto di copiarla.
In Rust il trasferimento di possesso è il comportamento standard.
Ad esempio, se scrivo una funzione che accetta una String
come parametro,
questa funzione prenderà possesso del valore della String
fornita dal chiamante:
fn elabora(s: String) { }
fn chiamante() {
let s = String::from("Ciao mondo!");
elabora(s); // Trasferisce la proprietà di `s` a `elabora`
elabora(s); // Errore! il possesso è già stato trasferito.
}
Come puoi vedere nel frammento di codice sopra, nella funzione chiamante
,
la prima chiamata a elabora
trasferisce il possesso della variabile s
.
Il compilatore tiene traccia del possesso, quindi una seconda chiamata a
elabora
genere un errore perché non è consentito trasferire il possesso
dello stesso valore due volte.
Rust previene anche il movimento di un valore se vi è ancora un
riferimente ad esso.
Il C++ ha un approccio distinto, il comportamento predefinito è infatti
di copiare un valore (nello specifico invocandone il costruttore della copia).
Ad ogni modo i chiamati possono dichiarare i loro parametri utilizzando
un “riferimento rvalore” come string&&
, per indicare che prenderanno
possesso di parte delle risorse possedute dal paramentro(in questo caso
il buffer interno della stringa).
Il chiamante deve quindi passare un’espressione temporanea o effettuare
un movimento esplicito utilizzando std::move
.
Un abbozzo della funzione elabora
sopra sarebbe quindi:
void elabora(string&& s) { }
void chiamante() {
string s("Ciao mondo!");
elabora(std::move(s));
elabora(std::move(s));
}
I compilatori C++ non sono tenuti a tenere traccia dei movimenti.
Ad esempio, il codice sopra viene compilato senza alcun avviso o errore
utilizzando l’ultima versione di Clang.
Inoltre in C++ il possesso della stringa s
stessa
(se non del suo buffer interno) rimane in chiamante
, e quindi il
destruttore di s
verrà eseguito quando chiamante
ritorna, anche se
è stato spostato (in Rust, al contrario, i valori spostati sono rimossi
dai nuovi proprietari).
Il Rust è il C++ possono interoperare tramite il C. Sia il Rust che il C++ forniscono una foreign function interface per il C, che può essere utilizzata per comunicare tra di loro. Se scrivere dei collegamenti in C è troppo complicato, puoi sempre utilizzare rust-bindgen per generare automaticamente dei collegamenti C++ funzionanti.
No. Al loro posto si utilizzano delle funzioni, il cui nome usuale è new()
, ad ogni modo questa è semplicemente una convenzione e non una regola del linguaggio.
La funzione new()
è semplicemente un’altra funzione. Un esempio di ciò è questo:
struct Foo {
a: i32,
b: f64,
c: bool,
}
impl Foo {
fn new() -> Foo {
Foo {
a: 0,
b: 0.0,
c: false,
}
}
}
Non esattamente. I tipi che implementano Copy
faranno una copia simil-C senza alcun lavoro aggiuntivo.
Non è possibile peró implementare tipi Copy
che richiedono un comportamento personalizzato alla copia.
Al loro posto in Rust i costruttori copia sono creati implementando il tratto e successivamente chiamando il metodo clone
.
Permettere di definire manualmente l’operatore copia permette di ridurre la complessità, facilitando per lo sviluppatore l’identificazione di operazioni potenzialmente costose.
No. I valori di tutti i tipi sono mossi tramite memcpy
.
Questo permette di scrivere del codice unsafe
generico molto più semplice, dato che l’assegnazione, il passaggio e il ritorno di valori sono privi di effetti collaterali.
Rust e Go hanno degli obiettivi molto differenti. Le differenze seguenti non sono le uniche (sarebbero troppe per elencarle) ma eccone alcune tra le più importanti:
I tratti in Rust somigliano alle typeclasses di Haskell ma attualmente non sono così potenti, dato che Rust non può esprimere i tipi di più altro livello. I tipi associati di Rust sono gli equivalenti delle famiglie di tipi di Haskell.
Alcune differenze specifiche tra le typeclasses di Haskell e i tratti di Rust includono:
Self
. trait Bar
in Rust corrisponde a class Bar self
in Haskell e trait Bar<Foo>
in Rust corrisponde a class Bar foo self
in Haskell.trait Sub: Super
, mentre in Haskell class Super self => Sub self
.impl
di Rust considera le clausole where
e i relativi tratti per decidere se due impl
si sovrappongono, o per scegliere tra diverse impl
possibili. Haskell considera ciò solo nelle dichiarazioni instance
, ignorando ogni limitazione posta altrove.ExistentialQuantification
.Il linguaggio Rust è pubblico da diversi anni e ha raggiunto la versione 1.0 a Maggio del 2015. Nei periodi precedenti il linguaggio ha ricevuto delle modifiche sostanziali e molte risposte fanno riferimento a versioni vecchie del linguaggio.
Nel tempo sempre più risposte saranno disponibili per la versione corrente, migliorando la problematica alterando il rapporto tra le risposte corrette e quelle sbagliate.
Puoi segnalare problemi con la documentazione sul pannello delle problematiche di Rust. Assicurati peró di leggere linee guida alla contribuzione prima.
Quando utilizzi cargo doc
per generare la documentazione per il tuo progetto, il comando genera anche la documentazione per le dipendenze attive.
Le puoi trovare nella cartella target/doc
del progetto.
Usa cargo doc --open
per aprire i documenti dopo averli generati o semplicemente apri da solo target/doc/index.html
.