Rust Basics

Tipi generici

Crea una struttura in cui uno dei suoi valori può essere di qualsiasi tipo.

struct Wrapper<T> {
value: T,
}

impl<T> Wrapper<T> {
pub fn new(value: T) -> Self {
Wrapper { value }
}
}

Wrapper::new(42).value
Wrapper::new("Foo").value, "Foo"

Option, Some & None

Il tipo Option significa che il valore potrebbe essere di tipo Some (c'è qualcosa) o None:

pub enum Option<T> {
None,
Some(T),
}

Puoi utilizzare le funzioni come is_some() o is_none() per verificare il valore dell'Opzione.

Macro

Le macro sono più potenti delle funzioni perché si espandono per produrre più codice rispetto a quello che hai scritto manualmente. Ad esempio, una firma di funzione deve dichiarare il numero e il tipo di parametri che la funzione ha. Le macro, d'altra parte, possono prendere un numero variabile di parametri: possiamo chiamare println!("ciao") con un argomento o println!("ciao {}", nome) con due argomenti. Inoltre, le macro vengono espandete prima che il compilatore interpreti il significato del codice, quindi una macro può, ad esempio, implementare un trait su un determinato tipo. Una funzione non può farlo, perché viene chiamata a runtime e un trait deve essere implementato a compile time.

macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
fn main() {
my_macro!();
my_macro!(7777);
}

// Export a macro from a module
mod macros {
#[macro_export]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}

Iterare

L'iterazione è un concetto fondamentale nella programmazione. Consiste nel ripetere un blocco di codice per un certo numero di volte o fino a quando una determinata condizione viene soddisfatta. In Rust, ci sono diverse opzioni per implementare l'iterazione.

Loop

Il loop è il modo più semplice per iterare in Rust. Puoi utilizzare il costrutto loop per creare un ciclo infinito che può essere interrotto manualmente utilizzando l'istruzione break. Ad esempio:

loop {
    // Blocco di codice da eseguire ripetutamente
    if condition {
        break; // Interrompe il ciclo
    }
}

While

Il ciclo while viene utilizzato per eseguire un blocco di codice finché una determinata condizione è vera. Ad esempio:

while condition {
    // Blocco di codice da eseguire finché la condizione è vera
}

For

Il ciclo for viene utilizzato per iterare su una sequenza di elementi. Può essere utilizzato con una serie di numeri, un iteratore o una collezione. Ad esempio:

for item in collection {
    // Blocco di codice da eseguire per ogni elemento nella collezione
}

Iteratori

Gli iteratori sono un concetto potente in Rust che consentono di eseguire operazioni su una sequenza di elementi in modo dichiarativo. Gli iteratori possono essere combinati e concatenati per creare pipeline di elaborazione dei dati. Ad esempio:

let numbers = vec![1, 2, 3, 4, 5];

let sum: i32 = numbers.iter()
                      .filter(|&x| x % 2 == 0)
                      .map(|x| x * 2)
                      .sum();

In questo esempio, viene creato un vettore di numeri e viene calcolata la somma dei numeri pari moltiplicati per 2 utilizzando gli iteratori.

L'iterazione è un concetto fondamentale nella programmazione e comprendere le diverse opzioni disponibili in Rust ti aiuterà a scrivere codice più pulito ed efficiente.

// Iterate through a vector
let my_fav_fruits = vec!["banana", "raspberry"];
let mut my_iterable_fav_fruits = my_fav_fruits.iter();
assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry"));
assert_eq!(my_iterable_fav_fruits.next(), None); // When it's over, it's none

// One line iteration with action
my_fav_fruits.iter().map(|x| capitalize_first(x)).collect()

// Hashmap iteration
for (key, hashvalue) in &*map {
for key in map.keys() {
for value in map.values() {

Box Ricorsivo

A recursive box is a data structure in Rust that allows for the creation of self-referential types. It is similar to a regular box, but with the ability to contain a value that refers to itself.

To create a recursive box, you can use the Rc (reference counting) or Arc (atomic reference counting) types provided by the std::rc module. These types allow multiple ownership of a value and keep track of the number of references to that value.

Here is an example of how to create a recursive box using Rc:

use std::rc::Rc;

struct Node {
    value: i32,
    next: Option<Rc<Node>>,
}

fn main() {
    let node1 = Rc::new(Node {
        value: 1,
        next: None,
    });

    let node2 = Rc::new(Node {
        value: 2,
        next: Some(Rc::clone(&node1)),
    });

    // Make node1 refer to node2
    node1.next = Some(Rc::clone(&node2));

    // Access the value of node1
    println!("Value of node1: {}", node1.value);

    // Access the value of node2 through node1
    if let Some(node2) = node1.next {
        println!("Value of node2: {}", node2.value);
    }
}

In this example, we define a Node struct that contains a value of type i32 and an optional Rc<Node> that represents the next node in the linked list. By using Rc, we can create a circular reference between node1 and node2, allowing us to traverse the linked list indefinitely.

Note that Rc is not thread-safe, so if you need to share the recursive box across multiple threads, you should use Arc instead.

enum List {
Cons(i32, List),
Nil,
}

let list = Cons(1, Cons(2, Cons(3, Nil)));

Condizioni

if

let n = 5;
if n < 0 {
print!("{} is negative", n);
} else if n > 0 {
print!("{} is positive", n);
} else {
print!("{} is zero", n);
}

corrispondenza

La parola chiave match in Rust viene utilizzata per eseguire il controllo dei modelli. Consente di confrontare un valore con una serie di modelli e di eseguire il codice corrispondente al modello che viene trovato. La sintassi di base è la seguente:

match valore {
    modello1 => {
        // codice da eseguire se il valore corrisponde al modello1
    },
    modello2 => {
        // codice da eseguire se il valore corrisponde al modello2
    },
    // altri modelli e codice corrispondente
}

È possibile utilizzare il modello _ per gestire tutti i casi non corrispondenti ai modelli specificati. Ad esempio:

match valore {
    1 => {
        println!("Il valore è 1");
    },
    2 => {
        println!("Il valore è 2");
    },
    _ => {
        println!("Il valore non è né 1 né 2");
    }
}

Il blocco di codice corrispondente al modello che viene trovato viene eseguito e l'esecuzione del match termina. Se nessun modello corrisponde al valore, il codice non viene eseguito.

match number {
// Match a single value
1 => println!("One!"),
// Match several values
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// TODO ^ Try adding 13 to the list of prime values
// Match an inclusive range
13..=19 => println!("A teen"),
// Handle the rest of cases
_ => println!("Ain't special"),
}

let boolean = true;
// Match is an expression too
let binary = match boolean {
// The arms of a match must cover all the possible values
false => 0,
true => 1,
// TODO ^ Try commenting out one of these arms
};

loop (infinito)

The loop keyword in Rust is used to create an infinite loop. The code inside the loop will continue to execute indefinitely until a break statement is encountered.

loop {
    // code to be executed repeatedly
    // until a break statement is encountered
}

Il termine loop in Rust viene utilizzato per creare un ciclo infinito. Il codice all'interno del ciclo continuerà ad eseguire indefinitamente fino a quando non viene incontrata un'istruzione di interruzione.

loop {
    // codice da eseguire ripetutamente
    // fino a quando non viene incontrata un'istruzione di interruzione
}
loop {
count += 1;
if count == 3 {
println!("three");
continue;
}
println!("{}", count);
if count == 5 {
println!("OK, that's enough");
break;
}
}

while

Il costrutto while in Rust viene utilizzato per eseguire un blocco di codice ripetutamente fintanto che una determinata condizione è vera. La sintassi del costrutto while è la seguente:

while condizione {
    // blocco di codice da eseguire
}

La condizione viene valutata all'inizio di ogni iterazione. Se la condizione è vera, il blocco di codice viene eseguito. Dopo l'esecuzione del blocco di codice, la condizione viene nuovamente valutata. Se la condizione è ancora vera, il blocco di codice viene eseguito di nuovo. Questo processo continua finché la condizione diventa falsa.

Ecco un esempio di utilizzo del costrutto while in Rust:

let mut count = 0;

while count < 5 {
    println!("Il valore di count è: {}", count);
    count += 1;
}

In questo esempio, il blocco di codice all'interno del costrutto while viene eseguito finché il valore di count è inferiore a 5. Ad ogni iterazione, viene stampato il valore di count e viene incrementato di 1.

let mut n = 1;
while n < 101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
n += 1;
}

per

for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else {
println!("{}", n);
}
}

// Use "..=" to make inclusive both ends
for n in 1..=100 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
}

// ITERATIONS

let names = vec!["Bob", "Frank", "Ferris"];
//iter - Doesn't consume the collection
for name in names.iter() {
match name {
&"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//into_iter - COnsumes the collection
for name in names.into_iter() {
match name {
"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//iter_mut - This mutably borrows each element of the collection
for name in names.iter_mut() {
*name = match name {
&mut "Ferris" => "There is a rustacean among us!",
_ => "Hello",
}
}

if let

La sintassi if let in Rust è un modo conciso per gestire il pattern matching in una singola istruzione. È utile quando si desidera eseguire un'azione specifica solo se un valore soddisfa un determinato pattern.

La sintassi di base è la seguente:

if let pattern = expression {
    // codice da eseguire se il pattern corrisponde all'espressione
} else {
    // codice da eseguire se il pattern non corrisponde all'espressione
}

In pratica, if let controlla se l'espressione corrisponde al pattern specificato. Se corrisponde, il codice all'interno del blocco if viene eseguito. In caso contrario, il codice all'interno del blocco else viene eseguito.

Ecco un esempio di utilizzo di if let:

let value = Some(5);

if let Some(x) = value {
    println!("Il valore è {}", x);
} else {
    println!("Il valore non è presente");
}

In questo esempio, value è un'opzione che contiene il valore 5. Utilizzando if let, controlliamo se value corrisponde al pattern Some(x). Se corrisponde, il valore x viene estratto e stampato a schermo. Altrimenti, viene stampato un messaggio che indica che il valore non è presente.

È possibile utilizzare if let anche con altri tipi di pattern, come ad esempio Some(x), None, Ok(x), Err(x), ecc. Questa sintassi è particolarmente utile quando si lavora con opzioni, risultati o altri tipi di dati che possono avere valori o errori.

let optional_word = Some(String::from("rustlings"));
if let word = optional_word {
println!("The word is: {}", word);
} else {
println!("The optional word doesn't contain anything");
}

while let

Il costrutto while let in Rust è un modo conciso per iterare su un'opzione fino a quando non diventa None. Questo costrutto è utile quando si desidera eseguire un blocco di codice solo se l'opzione contiene un valore.

Ecco un esempio di come utilizzare while let:

let mut optional_number = Some(5);

while let Some(number) = optional_number {
    println!("Il numero è: {}", number);
    optional_number = None;
}

In questo esempio, abbiamo un'opzione optional_number che inizialmente contiene il valore Some(5). Utilizzando while let, iteriamo sull'opzione finché contiene un valore. All'interno del blocco while let, stampiamo il numero e quindi assegnamo None all'opzione per terminare il ciclo.

L'output di questo codice sarà:

Il numero è: 5

Quando l'opzione diventa None, il ciclo termina.

let mut optional = Some(0);
// This reads: "while `let` destructures `optional` into
// `Some(i)`, evaluate the block (`{}`). Else `break`.
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
// ^ Less rightward drift and doesn't require
// explicitly handling the failing case.
}

Traits

Creare un nuovo metodo per un tipo

trait MyTrait {
    fn my_method(&self);
}

struct MyStruct;

impl MyTrait for MyStruct {
    fn my_method(&self) {
        println!("Hello from my_method!");
    }
}

fn main() {
    let my_struct = MyStruct;
    my_struct.my_method();
}

Il codice sopra definisce un trait chiamato MyTrait che ha un metodo chiamato my_method. Successivamente, viene definita una struttura chiamata MyStruct e viene implementato il trait MyTrait per questa struttura. L'implementazione del metodo my_method stampa "Hello from my_method!". Infine, nel metodo main, viene creato un'istanza di MyStruct chiamata my_struct e viene chiamato il metodo my_method su di essa.

trait AppendBar {
fn append_bar(self) -> Self;
}

impl AppendBar for String {
fn append_bar(self) -> Self{
format!("{}Bar", self)
}
}

let s = String::from("Foo");
let s = s.append_bar();
println!("s: {}", s);

Test

I test sono una parte essenziale dello sviluppo del software. I test consentono di verificare che il codice funzioni correttamente e che soddisfi i requisiti specificati. Ci sono diversi tipi di test che possono essere eseguiti durante il processo di sviluppo del software.

Test di unità

I test di unità sono utilizzati per verificare il corretto funzionamento di singole unità di codice, come funzioni o metodi. Questi test sono solitamente scritti dagli sviluppatori stessi e possono essere eseguiti in modo automatico.

Test di integrazione

I test di integrazione sono utilizzati per verificare che le diverse unità di codice funzionino correttamente quando vengono combinate insieme. Questi test sono solitamente scritti dagli sviluppatori o dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di sistema

I test di sistema sono utilizzati per verificare che l'intero sistema funzioni correttamente. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di accettazione

I test di accettazione sono utilizzati per verificare che il sistema soddisfi i requisiti specificati dal cliente o dagli utenti finali. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di regressione

I test di regressione sono utilizzati per verificare che le modifiche apportate al codice non abbiano introdotto nuovi bug o rotto funzionalità esistenti. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di carico

I test di carico sono utilizzati per verificare come il sistema si comporta sotto carichi di lavoro elevati. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di sicurezza

I test di sicurezza sono utilizzati per verificare la sicurezza del sistema e identificare eventuali vulnerabilità o falle di sicurezza. Questi test sono solitamente eseguiti da specialisti in sicurezza informatica o hacker etici.

Test di performance

I test di performance sono utilizzati per verificare le prestazioni del sistema, come la velocità di risposta e la scalabilità. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di stress

I test di stress sono utilizzati per verificare come il sistema si comporta in situazioni di stress estreme, come carichi di lavoro molto elevati o risorse limitate. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di usabilità

I test di usabilità sono utilizzati per verificare la facilità d'uso del sistema da parte degli utenti finali. Questi test sono solitamente eseguiti da specialisti in usabilità o dagli ingegneri di test.

Test di compatibilità

I test di compatibilità sono utilizzati per verificare che il sistema funzioni correttamente su diverse piattaforme, browser o dispositivi. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di localizzazione

I test di localizzazione sono utilizzati per verificare che il sistema funzioni correttamente in diverse lingue o regioni. Questi test sono solitamente eseguiti da specialisti in localizzazione o dagli ingegneri di test.

Test di automazione

I test di automazione sono utilizzati per automatizzare l'esecuzione dei test, riducendo così il tempo e lo sforzo necessari per eseguirli manualmente. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico.

Test di copertura

I test di copertura sono utilizzati per verificare quanto del codice è stato testato. Questi test sono solitamente eseguiti dagli strumenti di test e possono essere eseguiti in modo automatico.

Test di robustezza

I test di robustezza sono utilizzati per verificare come il sistema si comporta in presenza di input non validi o inaspettati. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di manutenibilità

I test di manutenibilità sono utilizzati per verificare quanto sia facile mantenere e modificare il codice. Questi test sono solitamente eseguiti dagli strumenti di test e possono essere eseguiti in modo automatico.

Test di ripristino

I test di ripristino sono utilizzati per verificare che il sistema possa essere ripristinato correttamente dopo un guasto o un'interruzione. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di regressione visiva

I test di regressione visiva sono utilizzati per verificare che le modifiche apportate al sistema non abbiano alterato l'aspetto visivo. Questi test sono solitamente eseguiti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.

Test di accessibilità

I test di accessibilità sono utilizzati per verificare che il sistema sia accessibile a persone con disabilità o esigenze speciali. Questi test sono solitamente eseguiti da specialisti in accessibilità o dagli ingegneri di test.

Test di uscita

I test di uscita sono utilizzati per verificare che il sistema sia pronto per essere rilasciato in produzione. Questi test sono solitamente scritti dagli ingegneri di test e possono essere eseguiti in modo automatico o manuale.