Dimensione: 12638
Commento:
|
Dimensione: 26611
Commento: bacon successore di cargo-watch (deprecato)
|
Le cancellazioni sono segnalate in questo modo. | Le aggiunte sono segnalate in questo modo. |
Linea 5: | Linea 5: |
<<Informazioni(forum="https://forum.ubuntu-it.org/viewtopic.php?t=79413";rilasci="22.04 22.10")>> | <<Informazioni(forum="https://forum.ubuntu-it.org/viewtopic.php?f=46&t=651508";rilasci="24.04 22.04")>> |
Linea 11: | Linea 11: |
Le sue caratteristiche lo hanno portato ad essere il più probabile erede dei linguaggi '''C''' e '''C++'''. Rispetto ad essi '''Rust''' ha in modo ''originale'' risolto le problematiche relative alla gestione manuale della memoria, praticamente bypassandola grazie ai meccanismi di [[#ownership|ownership/borrowing]] senza dover ricorrere al [[https://it.wikipedia.org/wiki/Garbage_collection|garbage collector]]. In particolare '''Rust''' è stato impostato in modo tale che il maggior numero di errori nel codice possano essere intercettati alla compilazione piuttosto che durante l'esecuzione del software. Un importante ruolo viene svolto dai messaggi di errore del compilatori, che risultano decisamente [[https://www.youtube.com/watch?v=CJtvnepMVAU|esaustivi]] nella gran parte dei casi. Oltre a questo il linguaggio si distingue anche grazie a funzionalità e un ecosistema di strumenti al passo coi tempi, come ad esempio [[#cargo|Cargo]] che semplifica e standardizza la creazione e gestione dei progetti software, o la capacità di compilare codice [[https://it.wikipedia.org/wiki/WebAssembly|WebAssembly]] che permette di scrivere ''single page app'' senza dover scrivere codice '''!JavaScript''' (vedere ad esempio il framework [[https://yew.rs/|Yew]]), e altro ancora. Pur nascendo come linguaggio complesso adatto alla scrittura di driver, sistemi operativi ([[https://www.redox-os.org/|Redox]]), database, ecc.. il suo approccio volto alla alla generazione di codice sicuro ha attratto molto interesse anche da altre aree della programmazione. = Installazione - aggiornamento = '''Rust''' può essere [[AmministrazioneSistema/InstallareProgrammi|installato]] tramite il pacchetto [[apt://rustc|rustc]], tuttavia per poter usufruire a pieno del suo ecosistema è consigliabile utilizzare il pacchetto ufficiale. |
Le sue caratteristiche lo hanno portato a essere il più probabile erede dei linguaggi '''C''' e '''C++'''. Rispetto a essi '''Rust''' ha risolto in modo ''originale'' le problematiche relative alla gestione manuale della memoria, praticamente bypassandola grazie ai meccanismi di [[#ownership|ownership/borrowing]] senza dover ricorrere al [[https://it.wikipedia.org/wiki/Garbage_collection|garbage collector]]. In particolare '''Rust''' è stato impostato per intercettare il maggior numero di errori nel codice in fase di compilazione piuttosto che durante l'esecuzione del software, grazie soprattutto all'utilizzo intensivo di [[#errori|Option e Result]] e ai messaggi di errore del compilatore, che risultano decisamente [[https://www.youtube.com/watch?v=CJtvnepMVAU|esaustivi]] nella gran parte dei casi. Inoltre il linguaggio si distingue anche grazie a funzionalità e un ecosistema di strumenti al passo coi tempi, come ad esempio [[#cargo|Cargo]] che semplifica e standardizza la creazione e gestione dei progetti software, o la capacità di compilare codice [[https://it.wikipedia.org/wiki/WebAssembly|WebAssembly]] che permette di creare ''single page app'' senza dover scrivere codice '''!JavaScript''' (vedere ad esempio il framework [[https://yew.rs/|Yew]]), e altro ancora. Pur nascendo come linguaggio complesso adatto alla scrittura di driver, sistemi operativi ([[https://www.redox-os.org/|Redox]] sperimentale), database, ecc., il suo approccio volto alla alla generazione di codice sicuro ha attratto molto interesse anche da altre aree della programmazione.<<BR>> '''Rust''' è stato introdotto anche nel kernel Linux a partire dalla versione 6.1. = Installazione e aggiornamento = '''Rust''' può essere [[AmministrazioneSistema/InstallareProgrammi|installato]] tramite il pacchetto [[apt://rustc|rustc]]. Tuttavia per poter usufruire appieno del suo ecosistema è consigliabile utilizzare il pacchetto ufficiale. |
Linea 27: | Linea 28: |
curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
Linea 36: | Linea 37: |
Per '''aggiornare''' a una nuova versione digitare:{{{ | Per gestire la versione di '''Rust''' installata, viene utilizzato il comando '''rustup''', ad esempio: * Per '''aggiornare''' a una nuova versione digitare:{{{ |
Linea 38: | Linea 40: |
}}} * Se si desidera rimuovere '''Rust''' è sufficiente digitare:{{{ rustup self uninstall |
|
Linea 64: | Linea 69: |
'''Cargo''' è il principale strumento di sviluppo all'interno di '''Rust''' attraverso il quale vengono gestiti svariati aspetti: organizzazione dei file di un progetto, gestione librerie aggiuntive, compilazione/esecuzione del codice e altro. Viene qui creato un semplice progetto per passare in rassegna le varie funzionalità. Qui una tabella riassuntiva dei comandi più frequenti: ||<:-2 #cccccc> '''Comandi'''|| |
'''Cargo''' è il principale strumento di sviluppo all'interno di '''Rust''' attraverso il quale vengono gestiti svariati aspetti: organizzazione dei file di un progetto, gestione librerie aggiuntive, compilazione/esecuzione del codice e altro. In breve se si desidera creare il progetto '''prova''' ed eseguirne il codice, sarà sufficiente: 0. creare il progetto nella directory corrente:{{{ cargo new prova }}} 0. Spostarsi nella directory `prova`:{{{ cd prova }}} 0. Compilare il codice ed eseguire il programma digitando:{{{ cargo run }}} Nei paragrafi successivi vengono forniti maggiori dettagli sui comandi eseguiti ed eventuali varianti. Qui una tabella riassuntiva dei comandi più frequenti: ||<:-2> '''Comandi'''|| |
Linea 74: | Linea 90: |
|| `cargo test` || Esegue unit test || {{{#!wiki tip Per una panoramica più esaustiva delle funzionalità, fare riferimento alla [[https://doc.rust-lang.org/cargo/index.html|documentazione ufficiale]]. }}} |
|
Linea 81: | Linea 102: |
Verrà creata la cartella `primo_progetto` con i relativi file. All'interno di essa in prima battuta spiccheranno i seguenti file: * '''Cargo.toml''': contiene i dati relativi al progetto (nome, versione, ecc..) ed eventuali dipendenze. * '''src/main.rs''': il file principale del progetto software. Nella cartella `src` saranno ospitati eventuali file aggiuntivi. |
Verrà creata la cartella `primo_progetto` con i relativi file. Al suo interno in prima battuta spiccheranno i seguenti file: * '''Cargo.toml''': contiene i dati relativi al progetto (nome, versione, ecc.) ed eventuali dipendenze. *'''Cargo.lock''': generato ed aggiornato automaticamente da cargo build (cargo run), traccia l'esatta versione delle singole dipendenze del progetto. * '''src/''': contenente il file '''main.rs''', ossia il file principale del progetto, conterrà eventuali file `.rs` e sotto-directory per '''moduli''' aggiuntivi. * '''.git''' e '''.gitignore''': presenti sotto forma di file nascosti in quanto in modo predefinito viene impostato un repository '''[[Programmazione/Git|Git]]'''. Man a mano che il codice verrà compilato, '''Cargo''' si preoccuperà di aggiungere file e directory aggiuntive: * '''target/debug/''': qui verrà salvato il file binario ogni volta che il codice sarà compilato durante la prima stesura del software. In questa fase, per rendere la compilazione più veloce, il codice viene compilato senza ottimizzazioni. * '''/target/release/''': qui verrà salvato il file binario una volta raggiunta la prima release del software. A discapito di una compilazione più lenta, verranno applicate le ottimizzazioni per rendere più efficiente il software. * '''test/''': può essere creata per effettuare [[https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html|unit test]]. In tal caso al suo interno può essere predisposto un file contenente funzioni che testino il comportamento del codice nelle più svariate casistiche. Il test viene svolto con il comando:{{{ cargo test }}} |
Linea 90: | Linea 120: |
0. Nel file `~/primo_progetto/src/main.rs` è già presente un esempio di codice '''hello world'''. Per compilarlo ed eseguirlo, digitare:{{{ | 0. Nel file `src/main.rs` è già presente un esempio di codice '''hello world'''. Per compilarlo ed eseguirlo, digitare:{{{ |
Linea 92: | Linea 122: |
}}}Verrà creato l'eseguibile `~/primo_progetto/target/debug/primo_progetto` e verrà restituita la stringa:{{{ | }}}Verrà creato l'eseguibile `primo_progetto` all'interno di `target/debug/` e verrà restituita la stringa:{{{ |
Linea 94: | Linea 124: |
}}} * Se interessa solo sapere se il codice sia compilabile o meno, digitare:{{{ cargo check |
|
Linea 98: | Linea 131: |
* Se interessa solo sapere se il codice sia compilabile o meno, digitare:{{{ cargo check }}} |
|
Linea 105: | Linea 135: |
Da notare che stavolta il file eseguibile sarà creato in `~/primo_progetto/target/release` con opportune ottimizzazioni che renderanno il software più veloce ma al contempo allungheranno i tempi di compilazione. }}} == Librerie esterne == Supponiamo di voler modificare il programma in modo che venga simulato il lancio di un dado. Per generare numeri random da 1 a 6 verrà sfruttata la libreria esterna '''Rand''' (in Rust le librerie esterne vengono chiamate '''Crate'''). |
Stavolta il file eseguibile sarà creato in `target/release/` con le opportune ottimizzazioni. }}} == Crates (librerie esterne) == Si supponga di voler modificare il programma in modo che venga simulato il lancio di un dado. Per generare numeri random da 1 a 6 verrà sfruttata la libreria esterna '''Rand''' (in Rust le librerie vengono chiamate [[https://crates.io/|Crates]]). |
Linea 114: | Linea 144: |
}}}oppure lanciare, nella directory del progetto, il comando:{{{ cargo add rand |
|
Linea 121: | Linea 153: |
if numero == 6 { println!("Hai fatto 6, hai vinto!"); } else { println!("Peccato! Hai fatto {numero}"); |
match numero { 6 => println!("Hai fatto 6, hai vinto!"), _ => println!("Peccato! Hai fatto {numero}"), |
Linea 136: | Linea 167: |
== Integrazione con Git == Il comando `cargo new` inizializza un nuovo repository '''[[Programmazione/Git|Git]]'''. All'interno della directory del progetto saranno presenti sotto forma di file nascosti la cartella `.git` e il file `.gitignore`. |
== fmt - formato del codice == '''fmt''' è uno strumento in grado di formattare il codice secondo le [[https://doc.rust-lang.org/1.0.0/style/README.html|linee guida]]. Attraverso un comando possono essere reimpostati: spazi, indentazione, posizioni delle parentesi, ecc. Si supponga di avere del codice scritto in modo disordinato (comunque valido per la compilazione): {{{ fn main( ) { let a = 8; let b = 10; println!( "Valore di a: {a}" ); println!("Valore di b: {b}" );} }}} Attraverso il comando: {{{ cargo fmt }}} oppure (se risultasse obsoleto): {{{ rustfmt --force src/main.rs }}} Il codice verrà riordinato migliorandone la lettura: {{{ fn main() { let a = 8; let b = 10; println!("Valore di a: {a}"); println!("Valore di b: {b}"); } }}} == clippy - suggerimenti per il codice == '''Clippy''' è uno strumento che indica modi più convenienti per scrivere il codice.<<BR>> Si supponga di avere le seguenti linee: {{{ fn main() { let a = 8; let vero_o_falso = if a % 2 == 0 { true } else { false }; println!("a è pari: {}", vero_o_falso); } }}} Eseguendo il comando:{{{ cargo clippy }}} nel messaggio che verrà restituito, verrà fatto notare che la parte `if/else` è superflua dal momento che già `a % 2 == 0` restituisce un valore booleano:{{{ warning: this if-then-else expression returns a bool literal let vero_o_falso = if a % 2 == 0 { ________________________^ | true } else { false}; |____________________________^ help: you can reduce it to: `a % 2 == 0` }}} Oltre a questo la variabile `vero_o_falso` può essere scritta direttamente nelle parentesi graffe interne alla macro `println!`: {{{ warning: variables can be used directly in the `format!` string - println!("a è pari: {}", vero_o_falso); + println!("a è pari: {vero_o_falso}"); }}} Andando a correggere il codice si ottiene: {{{ fn main() { let a = 8; let vero_o_falso = a % 2 == 0; println!("a è pari: {vero_o_falso}"); } }}} == bacon == {{{#!wiki note '''bacon''' è il successore di '''cargo-watch''' tutt'ora installabile ma [[https://github.com/watchexec/cargo-watch#maintenance|non più sviluppato]]. }}} A seconda dei casi può diventare tedioso ripetere in continuazione i passaggi per modificare il codice nell'editor di testo e avviare la compilazione/esecuzione nel terminale. Attraverso il pacchetto '''bacon''' è possibile fare in modo che a ogni salvataggio del file il programma venga automaticamente ricompilato ed eseguito. 0. Installare ''bacon'':{{{ cargo install --locked bacon }}} 0. Dopo essersi spostati nella directory del proprio progetto, come esempio essenziale per attuare compilazione ed esecuzione del codice, digitare:{{{ bacon -j run }}}A questo punto a ogni salvataggio del codice sorgente, il terminale aggiornerà in automatico il nuovo output. Per maggiori dettagli consultare il [[https://dystroy.org/bacon/|sito ufficiale]] del progetto e il comando `bacon --help`. = Plugin per editor di testo = Aggiungere al proprio editor di testo funzionalità come l'auto-completamento del codice, evidenziazione della sintassi, funzionalità di debug e altro. * [[Ufficio/EditorDiTesto/VisualStudioCode|VisualStudioCode]]: installare l'estensione '''rust-analyzer'''. * [[Ufficio/EditorDiTesto/SublimeText|Sublime Text]]: installare l'estensione '''LSP-rust-analyzer'''. * [[https://zed.dev/|Zed]]: supporto a '''Rust''' incluso di default. * [[Ufficio/EditorDiTesto/Helix/Lsp|Helix]]: consultare la [[Ufficio/EditorDiTesto/Helix/Lsp#rust|seguente procedura]]. = Variabili e tipo di dati = Rust è un linguaggio staticamente tipizzato, questo comporta che deve conoscere i tipi delle variabili al momento della compilazione. Il tipo di un variabile intera può essere dichiarato con una delle varianti presenti in tabella: || '''Size''' || '''Signed''' || '''Unsigned''' || || `8-bit` || i8 || u8 || || `16-bit` || i16 || u16 || || `32-bit` || i32 || u32 || || `64-bit` || i64 || u64 || || `128-bit` || i128|| u128 || || `arch` || isize || usize || dove '''Size''' rappresenta la dimensione massima della celletta di memoria atta a contenere la variabile intera, mentre '''Signed''' e '''Unsigned''' permettono alla celletta di contenere numeri negativi o positivi nel primo caso, e solo positivi nel secondo. Infine, `arch` crea la dimensione del tipo di dato in base all'architettura della macchina.<<BR>> In Rust, la dichiarazione e l'inizializzazione di una variabile è semplice e breve: {{{ let x=5; }}} Nel caso sopra riportato, il compilatore definisce di default una variabile intera di tipo `i32` assegnandole il valore 5.<<BR>> Nel caso, poi, si volesse specificare il tipo di variabile: {{{ let x:i32=5; }}} = Gestione memoria e sicurezza = {{{#!wiki tip Si vogliono qui mostrare solo alcuni cenni alla programmazione in '''Rust'''. Per una trattazione accurata si rimanda alla [[#doc|documentazione]] esistente. }}} Vengono qui evidenziati agli occhi dei meno esperti alcuni aspetti di '''Rust''' che pongono dei nuovi standard, come l'innovativa gestione della memoria (''ownership/borrowing'') e l'accurata gestione degli errori (utilizzo intensivo di ''Result'' e ''Option''), caratteristiche volte a intercettare il maggior numero di errori durante la compilazione piuttosto che durante l'utilizzo del software. |
Linea 141: | Linea 304: |
= Ownership e borrowing = {{{#!wiki note Il seguente paragrafo non intende essere una guida alla programmazione in '''Rust''', per la quale si rimanda alla copiosa [[#doc|documentazione]] già esistente. Nei seguenti passaggi si vogliono giusto mettere in evidenza agli occhi degli utenti meno esperti alcuni aspetti che hanno reso '''Rust''' unico nel suo genere e che lo hanno portato a essere la principale alternativa a '''C'''/'''C++''' nell'ambito dei linguaggi compilati ad alte prestazioni. }}} |
== Ownership e borrowing == |
Linea 160: | Linea 319: |
In un passaggio di valore fra due variabili del tipo '''b = a''' a seconda del linguaggio di programmazione e del tipo di dato (numero, stringa, array, ecc..), si hanno di solito due possibili situazioni: * il valore assegnato a '''b''' viene allocato in un differente indirizzo di memoria rispetto a quello di '''a'''. Si ha pertanto una copia a se stante e indipendente. * '''b''' farà riferimento al medesimo indirizzo di memoria in cui è allocato il valore di '''a'''. Più variabili possono essere ''responsabili'' per eventuali modifiche a quel valore. |
In un passaggio di valore fra due variabili del tipo '''b = a''' a seconda del linguaggio di programmazione e del tipo di dato (numero, stringa, array, ecc.), si hanno di solito due possibili situazioni: * il valore assegnato a '''b''' viene allocato in un differente indirizzo di memoria rispetto a quello di '''a'''. Si ha pertanto una copia a sé stante e indipendente. * Oppure '''b''' farà riferimento al medesimo indirizzo di memoria in cui è allocato il valore di '''a'''. A quel punto modificare '''b''' significa modificare '''a'''. |
Linea 170: | Linea 329: |
Quello che avviene è che temporaneamente il valore di '''a''' viene '''prestato''' (''borrowing'') a '''b'''. A questo punto esisterà un determinato ambito ('''scope''') nel quale '''b''' può essere utilizzato, e all'interno di esso la variabile '''a''' non può essere chiamata in causa.<<BR>> A tal proposito vediamo un nuovo esempio di codice che genererà un errore:{{{ |
Quello che avviene è che temporaneamente il valore di '''a''' viene '''prestato''' (''borrowing'') a '''b'''.<<BR>> Qualora entrassero in gioco modifiche alle variabili, dovrà essere tenuto in conto il concetto di '''scope''', un ambito nel quale durante un borrowing non può essere chiamata in causa la variabile originaria.<<BR>> Segue un nuovo esempio di codice che genererà un '''errore''':{{{ |
Linea 179: | Linea 339: |
0. viene assegnata stringa `"ciao"` alla variabile '''a''', stavolta inizializzata come mutabile ('''mut'''); | 0. viene assegnata la stringa `"ciao"` alla variabile '''a''', stavolta inizializzata come mutabile ('''mut'''); |
Linea 184: | Linea 344: |
'''a''' non può essere chiamata in causa finché il ciclo di utilizzo di '''b''' non è terminato, o in altri termini finché è uscito dal suo ''scope''. Perché il codice possa essere compilato è sufficiente spostare il passaggio `a.push('!')`:{{{ | '''a''' non può essere chiamata in causa finché il ciclo di utilizzo di '''b''' non è terminato, o in altri termini finché non è uscito dal suo ''scope''. Perché il codice possa essere compilato è sufficiente spostare il passaggio `a.push('!')`:{{{ |
Linea 192: | Linea 352: |
Sicuramente programmare in questi termini risulta più complesso, ma è altresì vero che non è più necessario gestire manualmente la memoria ne tanto meno ricorrere a un garbage collector, il tutto andando a produrre un codice intrinsecamente più sicuro che elimina alla base intere classi di bug che portano a gravi bug di sicurezza. = Plugin per editor di testo = Per aggiungere al proprio editor di testo funzionalità come l'auto-completamento del codice, evidenziazione della sintassi, funzionalità di debug e altro, è consigliabile l'utilizzo di '''rust-analyzer''' generalmente disponibile per i principali editor di testo. |
Sicuramente programmare in questi termini risulta più complesso, ma è altresì vero che non è più necessario gestire manualmente la memoria ne tanto meno ricorrere a un garbage collector, il tutto andando a produrre un codice intrinsecamente più sicuro che elimina alla base intere classi di bug che portano a gravi problemi di sicurezza. <<Anchor(errori)>> == Gestione errori == In programmazione quando si utilizzano funzioni per ricavare dati, c'è la possibilità che qualcosa vada storto (file non esistente, dividere un numero per zero, formato non valido, ecc..). Occorre prevedere e gestire l'eventualità che una funzione possa fallire.<<BR>> Una particolarità di '''Rust''' riguarda il fatto che moltissime funzioni non restituiscono direttamente il dato che stiamo cercando, ma piuttosto dei ''contenitori'' chiamati [[https://doc.rust-lang.org/std/option/|Option]] e [[https://doc.rust-lang.org/std/result/|Result]] che possono contenere tale dato: * '''Option''': se un dato esiste equivale a '''Some(dato)''', se è assente '''None'''. * '''Result''': se un risultato viene ottenuto equivale a '''Ok(risultato)''', in caso di errore '''Err(tipo_di_errore)'''. Come esempio di utilizzo dei '''Result''', supponiamo di voler trasformare la stringa `"4"` in un numero intero a 32bit tramite la funzione `parse()`, stamparla e quindi ripetere i passaggi ma utilizzando erroneamente la stringa `"ciao"` al posto di `"4"`:{{{ fn main() { let num = "4".parse::<i32>(); println!("{:?}", num); let num = "ciao".parse::<i32>(); println!("{:?}", num); } }}} * `::<i32>` notazione ''turbofish'', indica in questo caso al compilatore che si vuole ottenere un intero a 32bit. * `:?` notazione di debug utilizzata per stampare tipi di dati che non implementano la stampa. Come risultato si ottiene:{{{ Ok(4) Err(ParseIntError { kind: InvalidDigit }) }}} Come si può notare sia che la stringa sia convertibile o meno, il programma viene eseguito senza crash.<<BR>> Nel primo caso in cui non ci sono stati problemi di conversione, il numero `4` è stato inserito all'interno del tipo `Ok()`. Nel secondo invece si notano dei dati che fanno riferimento a errori di conversione, inseriti all'interno del tipo `Err()`. L'estrapolazione e utilizzo dei dati avviene tipicamente tramite [[https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html|pattern matching]]:{{{ match num { Ok(n) => println!("Il numero inserito è: {n}"), Err(e) => println!("Messaggio di errore: {e}"), } }}}Da notare che se all'interno del `match` una delle due voci dovesse mancare, la compilazione non andrebbe in porto. Se si sta valutando il valore di un `Result`, '''devono''' essere presenti entrambe le casistiche `Ok()` e `Err()`. Nel caso la variabile '''num''' contenga una stringa convertibile in numero come `"4"`, verrà stampato il messaggio:{{{ Il numero inserito è: 4 }}}in caso contrario verrà stampato il messaggio di errore:{{{ Messaggio di errore: invalid digit found in string }}} In definitiva programmare in questi termini significa mettere l'accento sull'eventuale "piano B" qualora non si ottenga il risultato sperato. Questo modo di procedere è pressoché onnipresente in '''Rust''' sin dai primi passi fino agli argomenti più complessi La gestione degli errori di per se è sempre esistita nella programmazione, tuttavia molto è stato lasciato al buon senso di chi programma. In '''Rust''' viene invece messa in primo piano tanto da "obbligare" il programmatore a tentarle tutte per riuscire a intercettare i possibili errori in fase di compilazione.<<BR>> Questa caratteristica ampiamente ispirata dal linguaggio [[https://ocaml.org/|OCaml]] va a elevare lo standard di correttezza del codice in produzione ed è uno degli aspetti che richiamano attenzione da vari ambiti della programmazione. <<Anchor(func)>> = Funzione = Oltre alla funzione primaria denominata `main()`, che agisce come punto di ingresso al programma, in Rust è possibile definire '''funzioni'''. Si tratta di un blocco di istruzioni costruito per un compito specifico. Una volta definite, possono essere richiamate, in qualsiasi punto all'interno della funzione `main()`, per accedere al codice in esse contenuto. Permettono, pertanto, di organizzare il programma in blocchi logici di codice, rendendo questi riutilizzabili ogni qual volta lo si desideri.<<BR>> In Rust, una '''[[https://doc.rust-lang.org/book/ch03-03-how-functions-work.html|funzione]]''' viene definita o dichiarata dalla parola chiave '''fn''' seguita dal nome assegnatole con eventuali '''parametri''' posti tra parantesi tonde e dal blocco di codice posto tra parentesi graffe. I parametri sono variabili speciali che fungono da input per la funzione. Servono per passare valori ad essa. Per ogni parametro è necessario specificarne il tipo. Di seguito due semplici definizioni di funzione: * con parametri, x e y, a 32 bit: {{{ fn print_sum(x: i32, y: i32) { println!("sum is: {}", x + y); } }}} * senza parametri: {{{ fn fn_hello(){ println!("hello word"); } }}} Per eseguirla occorre richiamarla, ovvero '''invocarla''', nel punto desiderato all'interno della funzione `main()`, mentre la sua definizione risiederà all'esterno di quest'ultima (prima o dopo): {{{ fn main() { print_sum(5, 6); fn_hello(); } fn fn_hello(){ println!("hello word"); } fn print_sum(x: i32, y: i32) { println!("sum is: {}", x + y); } }}} Il numero degli eventuali valori passati alla funzione invocata, definiti come '''argomenti''', ed il loro tipo di dato devono coincidere con quello fissato per i parametri.<<BR>> Le funzioni possono restituire un '''valore di ritorno''' come risultato del loro blocco di codice. Nel caso specifico, la sintassi si presenterà come: {{{ fn nome_funzione(parametro1: Tipo1,....) -> return_type { //statements return value; } }}} Lo statement `return` non è obbligatorio quando il valore ritornato è l'ultima espressione nella funzione: {{{ fn nome_funzione(parametro1: Tipo1,....) -> return_type { //statements value } }}} si noti, in questo caso, l'assenza di `;`.<<BR>> Il passaggio dei parametri alla funzione può avvenire per '''valore''' (esempi sopra riportati) o per '''reference'''. Nel primo caso, una nuova locazione di memoria viene creata per ogni argomento passato alla funzione invocata. I cambiamenti ai valori, passati come argomenti, avranno effetto solo all'interno della funzione. Nel secondo caso, invece, nessuna nuova locazione di memoria viene creata. La funzione opera direttamente sui valori passati. Dopo l'invocazione, quando il controllo ritorna alla funzione main(), il valore della variabile originale diventa quello modificato secondo blocco di codice interno alla funzione invocata.<<BR>> Di seguito un esempio di funzione con passaggio per `reference`: {{{ fn nome_funzione(parametro: &tipdo di dato) { blocco di codice } }}} |
Linea 201: | Linea 497: |
== Libri ufficiali == * [[https://doc.rust-lang.org/book/title-page.html|The Rust Programming Language]] |
== Libri - siti web == * [[https://doc.rust-lang.org/book/title-page.html|The Rust Programming Language - Steve Klabnik, Carol Nichols]] * [[https://dhghomon.github.io/easy_rust/|Easy Rust - Dave MacLeod]] |
Linea 206: | Linea 503: |
* [[https://marabos.nl/atomics/|Rust Atomics and Locks - Mara Bos]] == Tutorial == * [[https://tourofrust.com/|Tour of Rust]] * [[https://www.tutorialspoint.com/rust/index.htm|tutorialspoint - Rust Tutorial]] |
|
Linea 211: | Linea 514: |
* [[https://rust-exercises.com/|100 Exercises To Learn Rust]] | |
Linea 214: | Linea 518: |
* [[https://www.youtube.com/watch?v=lzKeecy4OmQ|Rust 101 Crash Course: Learn Rust - Zero To Mastery]] * [[https://www.youtube.com/playlist?list=PLai5B987bZ9CoVR-QEIN9foz4QCJ0H2Y8|The Rust Lang Book - Let's Get Rusty]] |
|
Linea 215: | Linea 521: |
* [[https://www.youtube.com/playlist?list=PLai5B987bZ9CoVR-QEIN9foz4QCJ0H2Y8|The Rust Lang Book - Let's Get Rusty]] | |
Linea 222: | Linea 527: |
* [[https://doc.rust-lang.org/std/index.html|Libreria standard]] * [[https://crates.io/|crates.io]] * [[https://poul.org/courses/rust/]] |
Guida verificata con Ubuntu: 22.04 24.04
Problemi in questa pagina? Segnalali in questa discussione
Introduzione
La seguente pagina mostra come installare e gestire il linguaggio di programmazione Rust.
Le sue caratteristiche lo hanno portato a essere il più probabile erede dei linguaggi C e C++. Rispetto a essi Rust ha risolto in modo originale le problematiche relative alla gestione manuale della memoria, praticamente bypassandola grazie ai meccanismi di ownership/borrowing senza dover ricorrere al garbage collector.
In particolare Rust è stato impostato per intercettare il maggior numero di errori nel codice in fase di compilazione piuttosto che durante l'esecuzione del software, grazie soprattutto all'utilizzo intensivo di Option e Result e ai messaggi di errore del compilatore, che risultano decisamente esaustivi nella gran parte dei casi.
Inoltre il linguaggio si distingue anche grazie a funzionalità e un ecosistema di strumenti al passo coi tempi, come ad esempio Cargo che semplifica e standardizza la creazione e gestione dei progetti software, o la capacità di compilare codice WebAssembly che permette di creare single page app senza dover scrivere codice JavaScript (vedere ad esempio il framework Yew), e altro ancora.
Pur nascendo come linguaggio complesso adatto alla scrittura di driver, sistemi operativi (Redox sperimentale), database, ecc., il suo approccio volto alla alla generazione di codice sicuro ha attratto molto interesse anche da altre aree della programmazione.
Rust è stato introdotto anche nel kernel Linux a partire dalla versione 6.1.
Installazione e aggiornamento
Rust può essere installato tramite il pacchetto rustc. Tuttavia per poter usufruire appieno del suo ecosistema è consigliabile utilizzare il pacchetto ufficiale.
Assicurarsi di aver installato i pacchetti curl e build-essential digitando nel terminale:
sudo apt install curl build-essential
Digitare quindi il comando:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
e rispondere a eventuali domande durante la procedura di installazione.- Riavviare il terminale per aggiornare le variabili d'ambiente.
Per verificare che l'installazione sia andata a buon fine, controllare la versione di Rust installata:
rustc --version
che dovrebbe restituire una stringa del tipo:
rustc 1.66.0 (69f9c33d7 2022-12-12)
Per gestire la versione di Rust installata, viene utilizzato il comando rustup, ad esempio:
Per aggiornare a una nuova versione digitare:
rustup update
Se si desidera rimuovere Rust è sufficiente digitare:
rustup self uninstall
rustc
Se si desidera testare velocemente del codice senza dover creare un progetto, lo si può fare attraverso il comando rustc.
Creare un file nella propria Home, ad esempio prova.rs e inserire al suo interno le seguenti linee di codice:
fn main() { println!("Testo di prova 🦀") }
Per compilare il file prova.rs ed eseguire il relativo file binario prova, digitare i comandi:
rustc prova.rs
./prova
Verrà restituita la stringa:
Testo di prova 🦀
I due precedenti comandi possono essere più comodamente combinati nel seguente modo:
rustc prova.rs && ./prova
Cargo
Cargo è il principale strumento di sviluppo all'interno di Rust attraverso il quale vengono gestiti svariati aspetti: organizzazione dei file di un progetto, gestione librerie aggiuntive, compilazione/esecuzione del codice e altro.
In breve se si desidera creare il progetto prova ed eseguirne il codice, sarà sufficiente:
creare il progetto nella directory corrente:
cargo new prova
Spostarsi nella directory prova:
cd prova
Compilare il codice ed eseguire il programma digitando:
cargo run
Nei paragrafi successivi vengono forniti maggiori dettagli sui comandi eseguiti ed eventuali varianti. Qui una tabella riassuntiva dei comandi più frequenti:
Comandi |
|
cargo new nome_progetto |
Crea una cartella contenente i file del progetto |
cargo check |
Controlla che il codice sorgente sia compilabile |
cargo build |
Compila il codice sorgente |
cargo run |
Compila il codice sorgente ed esegue il binario |
cargo build --release |
Compila il codice sorgente creando la nuova release del software |
cargo test |
Esegue unit test |
Per una panoramica più esaustiva delle funzionalità, fare riferimento alla documentazione ufficiale.
Creazione progetto
Creare nella propria Home il progetto primo_progetto digitando nel terminale:
cargo new primo_progetto
Verrà creata la cartella primo_progetto con i relativi file. Al suo interno in prima battuta spiccheranno i seguenti file:
Cargo.toml: contiene i dati relativi al progetto (nome, versione, ecc.) ed eventuali dipendenze.
Cargo.lock: generato ed aggiornato automaticamente da cargo build (cargo run), traccia l'esatta versione delle singole dipendenze del progetto.
src/: contenente il file main.rs, ossia il file principale del progetto, conterrà eventuali file .rs e sotto-directory per moduli aggiuntivi.
.git e .gitignore: presenti sotto forma di file nascosti in quanto in modo predefinito viene impostato un repository Git.
Man a mano che il codice verrà compilato, Cargo si preoccuperà di aggiungere file e directory aggiuntive:
target/debug/: qui verrà salvato il file binario ogni volta che il codice sarà compilato durante la prima stesura del software. In questa fase, per rendere la compilazione più veloce, il codice viene compilato senza ottimizzazioni.
/target/release/: qui verrà salvato il file binario una volta raggiunta la prima release del software. A discapito di una compilazione più lenta, verranno applicate le ottimizzazioni per rendere più efficiente il software.
test/: può essere creata per effettuare unit test. In tal caso al suo interno può essere predisposto un file contenente funzioni che testino il comportamento del codice nelle più svariate casistiche. Il test viene svolto con il comando:
cargo test
Compilazione ed esecuzione
Spostarsi all'interno della cartella primo_progetto:
cd primo_progetto
Nel file src/main.rs è già presente un esempio di codice hello world. Per compilarlo ed eseguirlo, digitare:
cargo run
Verrà creato l'eseguibile primo_progetto all'interno di target/debug/ e verrà restituita la stringa:
Hello, world!
Se interessa solo sapere se il codice sia compilabile o meno, digitare:
cargo check
Se si desidera solo compilare il sorgente, digitare:
cargo build
Se si è pronti a rilasciare la prima release del progetto, digitare:
cargo build --release
Stavolta il file eseguibile sarà creato in target/release/ con le opportune ottimizzazioni.
Crates (librerie esterne)
Si supponga di voler modificare il programma in modo che venga simulato il lancio di un dado. Per generare numeri random da 1 a 6 verrà sfruttata la libreria esterna Rand (in Rust le librerie vengono chiamate Crates).
Aggiornare la sezione [dependencies] del file ~/primo_progetto/Cargo.toml in modo che risulti:
[dependencies] rand = "0.8.5"
oppure lanciare, nella directory del progetto, il comando:
cargo add rand
Modificare il file ~/primo_progetto/src/main.rs nel seguente modo:
use rand::Rng; fn main() { let numero = rand::thread_rng().gen_range(1..=6); match numero { 6 => println!("Hai fatto 6, hai vinto!"), _ => println!("Peccato! Hai fatto {numero}"), } }
Sarà sufficiente eseguire il comando:
cargo run
Cargo si occuperà di scaricare Rand ed eventuali dipendenze e quindi procederà a compilare ed eseguire il programma, che restituirà in caso di vittoria:
Hai fatto 6, hai vinto!
oppure qualcosa del tipo:
Peccato! Hai fatto 1
fmt - formato del codice
fmt è uno strumento in grado di formattare il codice secondo le linee guida. Attraverso un comando possono essere reimpostati: spazi, indentazione, posizioni delle parentesi, ecc. Si supponga di avere del codice scritto in modo disordinato (comunque valido per la compilazione):
fn main( ) { let a = 8; let b = 10; println!( "Valore di a: {a}" ); println!("Valore di b: {b}" );}
Attraverso il comando:
cargo fmt
oppure (se risultasse obsoleto):
rustfmt --force src/main.rs
Il codice verrà riordinato migliorandone la lettura:
fn main() { let a = 8; let b = 10; println!("Valore di a: {a}"); println!("Valore di b: {b}"); }
clippy - suggerimenti per il codice
Clippy è uno strumento che indica modi più convenienti per scrivere il codice.
Si supponga di avere le seguenti linee:
fn main() { let a = 8; let vero_o_falso = if a % 2 == 0 { true } else { false }; println!("a è pari: {}", vero_o_falso); }
Eseguendo il comando:
cargo clippy
nel messaggio che verrà restituito, verrà fatto notare che la parte if/else è superflua dal momento che già a % 2 == 0 restituisce un valore booleano:
warning: this if-then-else expression returns a bool literal let vero_o_falso = if a % 2 == 0 { ________________________^ | true } else { false}; |____________________________^ help: you can reduce it to: `a % 2 == 0`
Oltre a questo la variabile vero_o_falso può essere scritta direttamente nelle parentesi graffe interne alla macro println!:
warning: variables can be used directly in the `format!` string - println!("a è pari: {}", vero_o_falso); + println!("a è pari: {vero_o_falso}");
Andando a correggere il codice si ottiene:
fn main() { let a = 8; let vero_o_falso = a % 2 == 0; println!("a è pari: {vero_o_falso}"); }
bacon
bacon è il successore di cargo-watch tutt'ora installabile ma non più sviluppato.
A seconda dei casi può diventare tedioso ripetere in continuazione i passaggi per modificare il codice nell'editor di testo e avviare la compilazione/esecuzione nel terminale. Attraverso il pacchetto bacon è possibile fare in modo che a ogni salvataggio del file il programma venga automaticamente ricompilato ed eseguito.
Installare bacon:
cargo install --locked bacon
Dopo essersi spostati nella directory del proprio progetto, come esempio essenziale per attuare compilazione ed esecuzione del codice, digitare:
bacon -j run
A questo punto a ogni salvataggio del codice sorgente, il terminale aggiornerà in automatico il nuovo output.
Per maggiori dettagli consultare il sito ufficiale del progetto e il comando bacon --help.
Plugin per editor di testo
Aggiungere al proprio editor di testo funzionalità come l'auto-completamento del codice, evidenziazione della sintassi, funzionalità di debug e altro.
VisualStudioCode: installare l'estensione rust-analyzer.
Sublime Text: installare l'estensione LSP-rust-analyzer.
Zed: supporto a Rust incluso di default.
Helix: consultare la seguente procedura.
Variabili e tipo di dati
Rust è un linguaggio staticamente tipizzato, questo comporta che deve conoscere i tipi delle variabili al momento della compilazione. Il tipo di un variabile intera può essere dichiarato con una delle varianti presenti in tabella:
Size |
Signed |
Unsigned |
8-bit |
i8 |
u8 |
16-bit |
i16 |
u16 |
32-bit |
i32 |
u32 |
64-bit |
i64 |
u64 |
128-bit |
i128 |
u128 |
arch |
isize |
usize |
dove Size rappresenta la dimensione massima della celletta di memoria atta a contenere la variabile intera, mentre Signed e Unsigned permettono alla celletta di contenere numeri negativi o positivi nel primo caso, e solo positivi nel secondo. Infine, arch crea la dimensione del tipo di dato in base all'architettura della macchina.
In Rust, la dichiarazione e l'inizializzazione di una variabile è semplice e breve:
let x=5;
Nel caso sopra riportato, il compilatore definisce di default una variabile intera di tipo i32 assegnandole il valore 5.
Nel caso, poi, si volesse specificare il tipo di variabile:
let x:i32=5;
Gestione memoria e sicurezza
Si vogliono qui mostrare solo alcuni cenni alla programmazione in Rust. Per una trattazione accurata si rimanda alla documentazione esistente.
Vengono qui evidenziati agli occhi dei meno esperti alcuni aspetti di Rust che pongono dei nuovi standard, come l'innovativa gestione della memoria (ownership/borrowing) e l'accurata gestione degli errori (utilizzo intensivo di Result e Option), caratteristiche volte a intercettare il maggior numero di errori durante la compilazione piuttosto che durante l'utilizzo del software.
Ownership e borrowing
Il seguente codice, apparentemente banale, produrrà un errore:
fn main() { let a = "ciao".to_string(); let b = a; println!("{a}"); }
Quello che si è tentato di fare è stato:
assegnare una stringa alla variabile a (in Rust esistono differenti tipi di stringa, qui il riferimento è al tipo String);
passare la variabile a alla variabile b;
stampare il valore della variabile a.
In un passaggio di valore fra due variabili del tipo b = a a seconda del linguaggio di programmazione e del tipo di dato (numero, stringa, array, ecc.), si hanno di solito due possibili situazioni:
il valore assegnato a b viene allocato in un differente indirizzo di memoria rispetto a quello di a. Si ha pertanto una copia a sé stante e indipendente.
Oppure b farà riferimento al medesimo indirizzo di memoria in cui è allocato il valore di a. A quel punto modificare b significa modificare a.
In Rust viene introdotto il concetto di ownership, ossia: può esistere un solo proprietario di un determinato valore.
Nel passaggio b = a non solo b punterà al valore di a, ma andrà a prendersi la proprietà (ownership) della stringa. Per questo motivo, nell'esempio sopra mostrato, la macro println!() non sarà in grado di stampare il valore di a che è stato ceduto a b.
Per poter fare in modo che la variabile a possa essere utilizzata anche in seguito, può essere copiata tramite a.clone(), ma quello che più tipicamente avviene è di impostare una referenza riscrivendo il passaggio b = a nel seguente modo:
let b = &a;
Quello che avviene è che temporaneamente il valore di a viene prestato (borrowing) a b.
Qualora entrassero in gioco modifiche alle variabili, dovrà essere tenuto in conto il concetto di scope, un ambito nel quale durante un borrowing non può essere chiamata in causa la variabile originaria.
Segue un nuovo esempio di codice che genererà un errore:
fn main() { let mut a = "ciao".to_string(); let b = &mut a; a.push('!'); println!("{b}"); }
viene assegnata la stringa "ciao" alla variabile a, stavolta inizializzata come mutabile (mut);
a viene passata a b sfruttando la sintassi per referenziare variabili mutabili &mut;
viene aggiunto il carattere '!' alla stringa "ciao" in modo che divenga "ciao!"
si vuole stampare il valore di b... ma senza successo.
a non può essere chiamata in causa finché il ciclo di utilizzo di b non è terminato, o in altri termini finché non è uscito dal suo scope. Perché il codice possa essere compilato è sufficiente spostare il passaggio a.push('!'):
... let b = &mut a; println!("{b}"); a.push('!');
Grazie a questo meccanismo in Rust la memoria occupata dalle variabili può essere automaticamente liberata una volta che una variabile ha terminato il suo ciclo all'interno del suo scope.
Sicuramente programmare in questi termini risulta più complesso, ma è altresì vero che non è più necessario gestire manualmente la memoria ne tanto meno ricorrere a un garbage collector, il tutto andando a produrre un codice intrinsecamente più sicuro che elimina alla base intere classi di bug che portano a gravi problemi di sicurezza.
Gestione errori
In programmazione quando si utilizzano funzioni per ricavare dati, c'è la possibilità che qualcosa vada storto (file non esistente, dividere un numero per zero, formato non valido, ecc..). Occorre prevedere e gestire l'eventualità che una funzione possa fallire.
Una particolarità di Rust riguarda il fatto che moltissime funzioni non restituiscono direttamente il dato che stiamo cercando, ma piuttosto dei contenitori chiamati Option e Result che possono contenere tale dato:
Option: se un dato esiste equivale a Some(dato), se è assente None.
Result: se un risultato viene ottenuto equivale a Ok(risultato), in caso di errore Err(tipo_di_errore).
Come esempio di utilizzo dei Result, supponiamo di voler trasformare la stringa "4" in un numero intero a 32bit tramite la funzione parse(), stamparla e quindi ripetere i passaggi ma utilizzando erroneamente la stringa "ciao" al posto di "4":
fn main() { let num = "4".parse::<i32>(); println!("{:?}", num); let num = "ciao".parse::<i32>(); println!("{:?}", num); }
::<i32> notazione turbofish, indica in questo caso al compilatore che si vuole ottenere un intero a 32bit.
:? notazione di debug utilizzata per stampare tipi di dati che non implementano la stampa.
Come risultato si ottiene:
Ok(4) Err(ParseIntError { kind: InvalidDigit })
Come si può notare sia che la stringa sia convertibile o meno, il programma viene eseguito senza crash.
Nel primo caso in cui non ci sono stati problemi di conversione, il numero 4 è stato inserito all'interno del tipo Ok(). Nel secondo invece si notano dei dati che fanno riferimento a errori di conversione, inseriti all'interno del tipo Err().
L'estrapolazione e utilizzo dei dati avviene tipicamente tramite pattern matching:
match num { Ok(n) => println!("Il numero inserito è: {n}"), Err(e) => println!("Messaggio di errore: {e}"), }
Da notare che se all'interno del match una delle due voci dovesse mancare, la compilazione non andrebbe in porto. Se si sta valutando il valore di un Result, devono essere presenti entrambe le casistiche Ok() e Err().
Nel caso la variabile num contenga una stringa convertibile in numero come "4", verrà stampato il messaggio:
Il numero inserito è: 4
in caso contrario verrà stampato il messaggio di errore:
Messaggio di errore: invalid digit found in string
In definitiva programmare in questi termini significa mettere l'accento sull'eventuale "piano B" qualora non si ottenga il risultato sperato. Questo modo di procedere è pressoché onnipresente in Rust sin dai primi passi fino agli argomenti più complessi
La gestione degli errori di per se è sempre esistita nella programmazione, tuttavia molto è stato lasciato al buon senso di chi programma. In Rust viene invece messa in primo piano tanto da "obbligare" il programmatore a tentarle tutte per riuscire a intercettare i possibili errori in fase di compilazione.
Questa caratteristica ampiamente ispirata dal linguaggio OCaml va a elevare lo standard di correttezza del codice in produzione ed è uno degli aspetti che richiamano attenzione da vari ambiti della programmazione.
Funzione
Oltre alla funzione primaria denominata main(), che agisce come punto di ingresso al programma, in Rust è possibile definire funzioni. Si tratta di un blocco di istruzioni costruito per un compito specifico. Una volta definite, possono essere richiamate, in qualsiasi punto all'interno della funzione main(), per accedere al codice in esse contenuto. Permettono, pertanto, di organizzare il programma in blocchi logici di codice, rendendo questi riutilizzabili ogni qual volta lo si desideri.
In Rust, una funzione viene definita o dichiarata dalla parola chiave fn seguita dal nome assegnatole con eventuali parametri posti tra parantesi tonde e dal blocco di codice posto tra parentesi graffe. I parametri sono variabili speciali che fungono da input per la funzione. Servono per passare valori ad essa. Per ogni parametro è necessario specificarne il tipo. Di seguito due semplici definizioni di funzione:
- con parametri, x e y, a 32 bit:
fn print_sum(x: i32, y: i32) { println!("sum is: {}", x + y); }
- senza parametri:
fn fn_hello(){ println!("hello word"); }
Per eseguirla occorre richiamarla, ovvero invocarla, nel punto desiderato all'interno della funzione main(), mentre la sua definizione risiederà all'esterno di quest'ultima (prima o dopo):
fn main() { print_sum(5, 6); fn_hello(); } fn fn_hello(){ println!("hello word"); } fn print_sum(x: i32, y: i32) { println!("sum is: {}", x + y); }
Il numero degli eventuali valori passati alla funzione invocata, definiti come argomenti, ed il loro tipo di dato devono coincidere con quello fissato per i parametri.
Le funzioni possono restituire un valore di ritorno come risultato del loro blocco di codice. Nel caso specifico, la sintassi si presenterà come:
fn nome_funzione(parametro1: Tipo1,....) -> return_type { //statements return value; }
Lo statement return non è obbligatorio quando il valore ritornato è l'ultima espressione nella funzione:
fn nome_funzione(parametro1: Tipo1,....) -> return_type { //statements value }
si noti, in questo caso, l'assenza di ;.
Il passaggio dei parametri alla funzione può avvenire per valore (esempi sopra riportati) o per reference. Nel primo caso, una nuova locazione di memoria viene creata per ogni argomento passato alla funzione invocata. I cambiamenti ai valori, passati come argomenti, avranno effetto solo all'interno della funzione. Nel secondo caso, invece, nessuna nuova locazione di memoria viene creata. La funzione opera direttamente sui valori passati. Dopo l'invocazione, quando il controllo ritorna alla funzione main(), il valore della variabile originale diventa quello modificato secondo blocco di codice interno alla funzione invocata.
Di seguito un esempio di funzione con passaggio per reference:
fn nome_funzione(parametro: &tipdo di dato) { blocco di codice }
Documentazione
Libri - siti web
Tutorial
Esercizi
Video corsi YouTube