Articolo originale: https://www.freecodecamp.org/news/rust-in-replit/
Per sei anni di fila, Rust è stato votato su Stack Overflow come il linguaggio di programmazione più amato.
Quindi se sei pronto a imparare questo popolare linguaggio di programmazione, questo corso ti farà conoscere Rust, così da poter iniziare a usarlo per i tuoi progetti.
Lavorerai interamente all'interno del tuo browser usando l'ambiente di programmazione interattivo Replit. freeCodeCamp ha stretto una collaborazione con Replit, che ha reso possibile questo corso.
La versione video di questo corso è disponibile (in lingua originale inglese) sul canale YouTube di freeCodeCamp.
Per ottenere il massimo da questo corso, dovresti avere una conoscenza intermedia di almeno un altro linguaggio di programmazione. Se sei nuovo al mondo della programmazione, dovresti provare il curriculum interattivo di freeCodeCamp e poi tornare a questo corso.
Per aiutarti a imparare Rust, creeremo due progetti:
- Una calcolatrice per la riga di comando
- Uno strumento per la riga di comando che prende due immagini e ne combina i pixel
Sommario
Ecco le sezioni e gli argomenti che tratteremo in questo corso. Puoi cliccare sulle voci nel sommario per andare direttamente a una particolare sezione, o puoi affrontare il corso dall'inizio alla fine.
- Una panoramica su Rust
- Come usare Rust in Replit
- Le basi di Rust
- Variabili in Rust
- Funzioni in Rust
- Stringhe e slice in Rust
- Il tipo
char
in Rust - Tipi numerici in Rust
- Struct in Rust
- Enum in Rust
- Macro in Rust
- Proprietà in Rust
- Progetto #1 - Costruire una calcolatrice per la riga di comando in Rust
- Risultato del progetto
- Metodologia del progetto calcolatrice
- Step 1 - Creare un nuovo progetto
- Step 2 - Capire la sintassi
- Step 3 - Eseguire il progetto
- Step 4 - Argomenti della riga di comando
- Step 5 - Interpretare le stringhe in numeri
- Step 6 - Eseguire operazioni aritmetiche di base
- Step 7 - Formattare l'output
- Step 8 - Mettere tutto insieme
- Progetto #2 - Costruire un combinatore di immagini in Rust
- Risultato del progetto
- Metodologia del progetto combinatore di immagini
- Step 1 - Creare un nuovo progetto
- Step 2 - Aggiungere un nuovo modulo per args
- Step 3 - Importare e usare il modulo
args
- Step 4 - Aggiungere un crate esterno
- Step 5 - Leggere un file immagine
- Step 6 - Gestire gli errori con
Result
- Step 7 - Ridimensionare le immagini
- Step 8 - Creare una FloatingImage
- Step 9 - Creare i dati dell'immagine combinata
- Step 10 - Inserire i dati combinati in Floating Image
- Step 11 - Scrivere l'immagine in un file
- Step 12 - Mettere tutto insieme
- Conclusione
Una panoramica su Rust
Rust è un linguaggio di programmazione al livello di sistema.
"[Rust] tratta dettagli di basso livello di gestione della memoria, rappresentazione dei dati e concorrenza."
"... il linguaggio è progettato per guidarti naturalmente verso del codice affidabile, efficiente in termini di velocità e utilizzo della memoria." (Fonte: documentazione Rust)
La strumentazione principale dell'ecosistema Rust è:
- rustc – Il compilatore che prende il codice Rust e lo compila in binario (codice leggibile dalla macchina)
- rustup – L'utilità da riga di comando per installare e aggiornare Rust
- cargo – Il sistema di build e gestore di pacchetti di Rust
Come usare Rust in Replit
Per questo corso, utilizzerai questo repository GitHub come boilerplate.
Per iniziare, clicca sul seguente link per creare un nuovo REPL dal boilerplate:
Poi nella finestra "Import from GitHub", apri il menu a discesa "Language" e seleziona "Bash":
Clicca sul pulsante "Import from GitHub" in basso a destra per importare il codice di partenza in Replit.
Infine, per iniziare il corso, clicca sul pulsante Run nella parte superiore dello schermo e segui le istruzioni nella console sulla destra:
Le basi di Rust
Variabili in Rust
Puoi dichiarare delle variabili usando le parole chiave let
, const
o static
:
let my_variable = 0;
const MY_CONSTANT: u8 = 0;
static MY_STATIC: u8 = 0;
Come impostazione predefinita, tutte le variabili sono immutabili. Puoi rendere mutabile una variabile usando la parola chiave mut
:
let mut my_mutable_variable = 0;
In Rust, le convenzioni di nomenclatura sono le seguenti:
Oggetto | Stile |
---|---|
Variabili | snake_case |
Funzioni | snake_case |
File | snake_case |
Costanti | SCREAMING_SNAKE_CASE |
Static | SCREAMING_SNAKE_CASE |
Tipi | PascalCase |
Tratti | PascalCase |
Enum | PascalCase |
Dato che Rust è staticamente tipizzato, devi scrivere esplicitamente il tipo di variabile – a meno che la variabile venga dichiarata con let
e il tipo può essere dedotto.
Funzioni in Rust
Puoi dichiarare delle funzioni usando la parola chiave fn
:
fn main() {
// Questo è un commento
}
Le funzioni restituiscono qualcosa con la parole chiave return
e devi specificare esplicitamente il tipo del valore di ritorno di una funzione, a meno che si tratti di una tupla vuota ()
:
fn main() -> () { // Tipo del valore di ritorno non necessario
my_func();
}
fn my_func() -> u8 {
return 0;
}
Le funzioni restituiscono un'espressione anche omettendo il punto e virgola:
fn my_func() -> u8 {
0
}
I parametri di funzione vengono scritti usando la sintassi :
:
fn main() {
let _unused_variable = my_func(10);
}
fn my_func(x: u8) -> i32 {
x as i32
}
Il trattino basso prima del nome di una variabile è una convenzione per indicare che la variabile non è utilizzata. La parola chiave as
afferma il tipo dell'espressione, a condizione che la conversione sia valida.
Stringhe e slice in Rust
Un motivo di confusione comune per i principianti di Rust è la differenza tra la struct String
e il tipo str
.
let my_str: &str = "Hello, world!";
let my_string: String = String::from("Hello, world!");
Nell'esempio qui sopra, my_str
è un riferimento a una stringa letterale, e my_string
è un'istanza della struct String
.
Un'importante distinzione tra le due è che my_str
è memorizzata nello stack, mentre my_string
è allocata nella memoria heap. Ciò significa che il valore di my_str
non può cambiare e la sua dimensione è fissa, mentre my_string
può avere una dimensione sconosciuta al tempo di compilazione.
Una stringa letterale è anche conosciuta come string slice (letteralmente "fetta"), poiché &str
fa riferimento a parte di una stringa. In linea generale, ecco la somiglianza tra stringhe e array:
let my_string = String::from("The quick brown fox");
let my_str: &str = &my_string[4..9]; // "quick"
let my_arr: [usize; 5] = [1, 2, 3, 4, 5];
let my_arr_slice: &[usize] = &my_arr[0..3]; // [1, 2, 3]
La notazione [T; n]
viene usata per creare un array di n
elementi di tipo T
.
Il tipo char
in Rust
Un char
è un USV (Unicode Scalar Value), che è rappresentato in Unicode con valori come U+221E
– il codice Unicode per '∞'. Puoi pensare a un insieme o un array di char
come a una stringa:
let my_str: &str = "Hello, world!";
let collection_of_chars: &str = my_str.chars().as_str();
Tipi numerici in Rust
Esistono molti tipi di numeri in Rust:
- Interi senza segno:
u8
,u16
,u32
,u64
,u128
- Interi con segno:
i8
,i16
,i32
,i64
,i128
- Numeri in virgola mobile:
f32
,f64
Gli interi senza segno rappresentano solo gli interi positivi.
Gli interi con segno rappresentano gli interi sia positivi che negativi.
Mentre i numeri in virgola mobile rappresentano numeri frazionari positivi e negativi.
Struct in Rust
Una struct è un tipo di dato personalizzato usato per raggruppare dati correlati. Ti sei già imbattuto in una struct nella sezione Stringhe e slice:
struct String {
vec: Vec<u8>,
}
La struct String
consiste di un campo vec
, che è un Vec
di u8
. Vec
è un array con dimensione dinamica.
Un'istanza di una struct viene dichiarata dando valori ai campi:
struct MyStruct {
field_1: u8,
}
let my_struct = MyStruct { field_1: 0, };
Precedentemente, la struct String
è stata usata con la sua funzione from
per creare una String
da una &str
. Ciò è possibile in quanto la funzione from
è implementata per String
:
impl String {
fn from(s: &str) -> Self {
String {
vec: Vec::from(s.as_bytes()),
}
}
}
Si utilizza la parola chiave Self
al posto del tipo di struct.
Le struct possono anche prendere altre varianti:
struct MyUnitStruct;
struct MyTupleStruct(u8, u8);
Enum in Rust
Analogamente ad altri linguaggi, gli enum sono utili per agire come tipi e come valori.
enum MyErrors {
BrainTooTired,
TimeOfDay(String)
CoffeeCupEmpty,
}
fn work() -> Result<(), MyErrors> { // Result is also an enum
if state == "missing semi-colon" {
Err(MyErrors::BrainTooTired)
} else if state == "06:00" {
Err(MyErrors::TImeOfDay("It's too early to work".to_string()))
} else if state == "22:00" {
Err(MyErrors::TimeOfDay("It's too late to work".to_string()))
} else if state == "empty" {
Err(MyErrors::CoffeeCupEmpty)
} else {
Ok(())
}
}
Macro in Rust
Una macro è simile a una funzione, ma puoi pensarla come un pezzo di codice che scrive altro codice. Per ora, le differenze principali da tenere a mente tra una funzione e una macro sono:
- Le macro vengono chiamate usando un punto esclamativo (
!
, anche detto bang) - Le macro possono prendere un numero variabile di argomenti, mentre le funzioni non possono
Una delle macro più comuni è println!
, che stampa sulla console:
let my_str = "Hello, world!";
println!("{}", my_str);
Si utilizza la sintassi {}
per inserire una variabile in una stringa.
Un'altra macro comune è panic!
. Panic è il modo di Rust di dare errore. È saggio pensare a un panic in Rust come a un errore mal gestito. La macro accetta una stringa letterale e "va nel panico" dando il messaggio.
let am_i_an_error = true;
if (am_i_an_error) {
panic!("There was an error");
}
$ cargo run
Compiling fcc-rust-in-replit v0.1.0 (/home/runner/Rust-in-Replit)
Finished dev [unoptimized + debuginfo] target(s) in 1.66s
Running `target/debug/calculator`
thread 'main' panicked at 'There was an error', src/main.rs
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Proprietà in Rust
Un'importante concetto in Rust è la proprietà. Esistono tre regole principali:
- Ogni valore in Rust ha una variabile che è chiamata proprietario.
- Può esserci solo un proprietario alla volta.
- Quando il proprietario è fuori dalla visibilità, il valore viene eliminato.
(Fonte: The Rust Book)
Questo è il modo in cui Rust la fa franca senza avere un tipico garbage collector, mentre non richiede al programmatore di gestire in modo esplicito la memoria. Ecco un esempio di proprietà:
fn main() { // first_string non è stata ancora dichiarata -> non ha valore
let first_string = String::from("freeCodeCamp"); // first_string adesso è il proprietario del valore "freeCodeCamp"
let second_string = first_string; // second_string prende la proprietà del valore "freeCodeCamp"
println!("Hello, {}!", first_string); // first_string NON è valida, perché il valore è stato spostato a second_string
}
Dato che la macro println!
prova a fare riferimento a un valore non valido, il codice non viene compilato. Per porvi rimedio, invece di spostare il valore di first_string
in second_string
, second_string
può essere assegnata come riferimento a first_string
:
fn main() {
let first_string: String = String::from("freeCodeCamp");
let second_string: &String = &first_string; // first_string è ancora il proprietario del valore "freeCodeCamp"
println!("Hello, {}!", first_string);
}
La e commerciale (&
) indica che il valore è un riferimento, ovvero che second_string
non possiede la proprietà di "freeCodeCamp"
, ma invece punta alla stessa posizione in memoria di first_string
.
Progetto #1 – Costruire una calcolatrice per la riga di comando in Rust
Risultato del progetto
Alla fine di questo progetto, sarai in grado di eseguire delle operazioni aritmetiche di base su dei numeri usando la riga di comando.
Ecco degli esempi di input e output attesi:
$ calculator 1 + 1
$ 1 + 1 = 2
$ calculator 138 / 4
$ 138 / 4 = 34.5
Metodologia del progetto calcolatrice
Step 1 – Creare un nuovo progetto
Usa Cargo per chiamare un nuovo progetto chiamato calculator
:
$ cargo new calculator
Questo crea una nuova cartella chiamata calculator
, inizializzandola come un repository Git, e aggiunge il boilerplate per il tuo progetto.
Il boilerplate include:
Cargo.toml
– Il file manifest usato da Cargo per gestire i metadati del tuo progettosrc/
– La cartella in cui si trova il tuo progettosrc/main.rs
– Il file che Cargo usa di default come punto d'ingresso dell'applicazione
Step 2 – Capire la sintassi
Il file calculator/Cargo.toml
contiene:
[package]
name = "calculator"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[package]
denota i metadati del tuo progetto.
L'intestazione [dependencies]
denota i crate da cui dipende il tuo progetto. I crate sono come delle librerie esterne.
Il file calculator/src/main.rs
contiene:
fn main() {
println!("Hello, world!");
}
Questo file contiene una dichiarazione di funzione con l'handle main
. Di default, rustc chiama prima la funzione main
ogni volta che l'eseguibile viene eseguito.
println!
è una macro integrata che stampa sulla console.
Step 3 – Eseguire il progetto
Puoi usare Cargo per eseguire il codice del tuo progetto:
# All'interno della cartella calculator/
$ cargo run
Compiling fcc-rust-in-replit v0.1.0 (/home/runner/Rust-in-Replit-1)
Finished dev [unoptimized + debuginfo] target(s) in 0.80s
Running `target/debug/calculator`
Hello, world!
Oppure puoi usare rustc per compilare il tuo progetto e poi puoi eseguire il binario:
# All'interno della cartella calculator/
$ rustc src/main.rs
$ ./main
Hello, world!
Step 4 – Argomenti della riga di comando
La libreria standard Rust contiene un modulo env
, che permette di accedere agli argomenti della riga di comando passati chiamando il programma.
Gli export indispensabili dal modulo env
sono la funzione args
e la struct Args
. La funzione args
restituisce un'istanza della struct Args
ed è importata nell'ambito di visibilità del file con:
use std::env::{args, Args};
Per farti un'idea dell'aspetto della struct Args
, la variabile args
è stampata sulla console:
fn main() {
let args: Args = args();
println!("{:?}", args);
}
$ cargo run -- fCC
Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
Finished dev [unoptimized + debuginfo] target(s) in 1.71s
Running `target/debug/calculator`
Args { inner: ["target/debug/toto", "fCC"] }
Il codice qui sopra mostra che la struct Args
contiene un field
(campo) chiamato inner
che consiste nella posizione del binario compilato e gli argomenti da riga di comando passati al programma.
Per accedere ai valori degli argomenti, puoi usare il metodo nth
sulla variabile args
. Il metodo nth
prende un argomento index
e restituisce il valore a quell'indice racchiuso in un Option
. Quindi il valore deve essere spacchettato (unwrapped).
fn main() {
let mut args: Args = args();
let first: String = args.nth(1).unwrap();
}
La variabile args
deve essere dichiarata come mutabile, perché il metodo nth
itera mutabilmente sugli elementi e rimuove gli elementi consultati.
fn main() {
let mut args: Args = args();
// Il primo argomento è la posizione del binario compilato, quindi saltalo
let first: String = args.nth(1).unwrap();
// Dopo aver consultato il secondo argomento, l'elemento successivo dell'iteratore diventa il primo
let operator: String = args.nth(0).unwrap();
let second: String = args.nth(0).unwrap();
println!("{} {} {}", first, operator, second);
}
$ cargo run -- 1 + 1
Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
Finished dev [unoptimized + debuginfo] target(s) in 1.71s
Running `target/debug/calculator`
1 + 1
Step 5 – Interpretare le stringhe in numeri
Le variabili first
e second
sono stringhe e devi effettuarne il parsing in numeri. La struct String
implementa il metodo parse
, che prende un'annotazione di tipo e restituisce un Result
contenente il valore interpretato.
use std::env::{args, Args};
fn main() {
let mut args: Args = args();
let first: String = args.nth(1).unwrap();
let operator: String = args.nth(0).unwrap();
let second: String = args.nth(0).unwrap();
let first_number = first.parse::<f32>().unwrap();
let second_number = second.parse::<f32>().unwrap();
println!("{} {} {}", first_number, operator, second_number);
}
Il metodo parse
qui sopra fa uso della sintassi turbofish per specificare il tipo in cui provare a interpretare la stringa.
Step 6 – Eseguire operazioni aritmetiche di base
Rust utilizza gli operatori per addizione, sottrazione, moltiplicazione e divisione.
Per gestire le operazioni, definisci una funzione chiamata operate
che prende tre argomenti: l'operatore come char
e due numeri come f32
. La funzione dovrebbe restituire un f32
che rappresenta il risultato dell'operazione.
fn operate(operator: char, first_number: f32, second_number: f32) -> f32 {
match operator {
'+' => first_number + second_number,
'-' => first_number - second_number,
'/' => first_number / second_number,
'*' | 'X' | 'x' => first_number * second_number,
_ => panic!("Invalid operator used."),
}
}
L'espressione match
funziona in modo simile all'istruzione switch
in altri linguaggi. L'espressione match
prende un valore e una lista di braccia. Ogni braccio ha un pattern e un blocco. Il pattern è un valore con cui effettuare il confronto e il blocco è il codice da eseguire se il pattern corrisponde. Il pattern _
è un jolly e agisce come una clausola else
.
Il braccio della moltiplicazione include il confronto OR
per permettere la gestione dei casi per X
e x
.
Ora, per chiamare operate
con operator
, devi convertirla prima in un char
. Puoi farlo usando il metodo chars
sulla struct String
che restituisce un iteratore sui caratteri della stringa. Poi, il primo carattere viene spacchettato:
fn main() {
let mut args: Args = args();
let first: String = args.nth(1).unwrap();
let operator: char = args.nth(0).unwrap().chars().next().unwrap();
let second: String = args.nth(0).unwrap();
let first_number = first.parse::<f32>().unwrap();
let second_number = second.parse::<f32>().unwrap();
let result = operate(operator, first_number, second_number);
println!("{} {} {}", first_number, operator, second_number);
}
Il valore restituito da operate
è memorizzato nella variabile result
.
Step 7 – Formattare l'output
Per ottenere l'output desiderato, le variabili first_number
, second_number
, operator
e result
devono essere formattate. Puoi usare la macro format!
per creare una String
da una stringa formattata e una lista di argomenti:
To get the desired output, the first_number
, second_number
, operator
, and result
variables need to be formatted. You can use the format!
macro to create a String
from a format string and a list of arguments:
fn output(first_number: f32, operator: char, second_number: f32, result: f32) -> String {
format!(
"{} {} {} = {}",
first_number, operator, second_number, result
)
}
Step 8 – Mettere tutto insieme
use std::env::{args, Args};
fn main() {
let mut args: Args = args();
let first: String = args.nth(1).unwrap();
let operator: char = args.nth(0).unwrap().chars().next().unwrap();
let second: String = args.nth(0).unwrap();
let first_number = first.parse::<f32>().unwrap();
let second_number = second.parse::<f32>().unwrap();
let result = operate(operator, first_number, second_number);
println!("{}", output(first_number, operator, second_number, result));
}
fn output(first_number: f32, operator: char, second_number: f32, result: f32) -> String {
format!(
"{} {} {} = {}",
first_number, operator, second_number, result
)
}
fn operate(operator: char, first_number: f32, second_number: f32) -> f32 {
match operator {
'+' => first_number + second_number,
'-' => first_number - second_number,
'/' => first_number / second_number,
'*' | 'X' | 'x' => first_number * second_number,
_ => panic!("Invalid operator used."),
}
}
Far fare il build del codice in un binario eseguibile, esegui il seguente comando:
$ cargo build --release
Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
Finished release [optimized] target(s) in 3.26s
Il flag --release
comunica a Cargo di fare il build del binario in modalità di rilascio. Ciò ridurrà la dimensione del binario e rimuoverà anche qualsiasi informazione di debugging.
Il build del binario avviene nella cartella target/release
. Per eseguire il binario e testare la tua applicazione esegui il seguente comando:
$ target/release/calculator 1 + 1
1 + 1 = 2
Progetto #2 – Costruire un combinatore di immagini in Rust
Risultato del progetto
Alla fine di questo progetto, sarai in grado di combinare due immagini usando la riga di comando.
Ecco un esempio di un input atteso:
$ combiner ./image1.png ./image2.png ./output.png
Per un esempio dell'output, guarda la prima immagine in questo articolo ☝️
Metodologia del progetto combinatore di immagini
Step 1 - Creare un nuovo progetto
Usa Cargo per creare un nuovo progetto chiamato combiner
:
$ cargo new combiner
Step 2 - Aggiungere un nuovo modulo per args
Per evitare che il file main.rs
diventi troppo difficile da gestire, crea un nuovo file chiamato args.rs
nella cartella src
.
All'interno di args.rs
, crea una funzione chiamata get_nth_arg
che prende un n
usize
e restituisce una String
. Quindi, dal modulo std::env
, chiama la funzione args
e concatena il metodo nth
per ottenere l'n
-esimo argomento e spacchettare il valore:
fn get_nth_arg(n: usize) -> String {
std::env::args().nth(n).unwrap()
}
Definisci una struct pubblica chiamata Args
che consiste di tre campi pubblici di tipo String
: image_1
, image_2
e output
:
pub struct Args {
pub image_1: String,
pub image_2: String,
pub output: String,
}
Dichiara la struct e i suoi campi come pubblici con la parola chiave pub
, in modo da potervi accedere dall'esterno del file args.rs
.
Infine, puoi usare la funzione get_nth_arg
per creare una nuova struct Args
in una funzione new
:
impl Args {
pub fn new() -> Self {
Args {
image_1: get_nth_arg(1),
image_2: get_nth_arg(2),
output: get_nth_arg(3),
}
}
}
Nel complesso, il file args.rs
ha questo aspetto:
pub struct Args {
pub image_1: String,
pub image_2: String,
pub output: String,
}
impl Args {
pub fn new() -> Self {
Args {
image_1: get_nth_arg(1),
image_2: get_nth_arg(2),
output: get_nth_arg(3),
}
}
}
fn get_nth_arg(n: usize) -> String {
std::env::args().nth(n).unwrap()
}
Step 3 – Importare e usare il modulo args
All'interno di main.rs
, devi dichiarare il file args.rs
come modulo. Poi, per usare la struct Args
occorre importarla:
mod args;
use args::Args;
fn main() {
let args = Args::new();
println!("{:?}", args);
}
Ma testando il codice scoprirai un errore:
$ cargo run -- arg1 arg2 arg3
Compiling combiner v0.1.0 (/home/runner/Rust-in-Replit/combiner)
error[E0277]: `args::Args` doesn't implement `Debug`
--> src/main.rs:12:20
|
12 | println!("{:?}", args);
| ^^^^ `args::Args` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `args::Args`
= note: add `#[derive(Debug)]` or manually implement `Debug`
= note: required by `std::fmt::Debug::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `combiner`
To learn more, run the command again with --verbose.
Analogamente al modo in cui le funzioni sono implementate per le struct, i tratti possono essere implementati per le struct. Tuttavia, il tratto Debug
ha la particolarità di poter essere implementato usando attributi:
#[derive(Debug)]
pub struct Args {
pub image_1: String,
pub image_2: String,
pub output: String,
}
Il tratto Debug
è stato derivato dalla struct Args
. Ciò vuol dire che il tratto Debug
è implementato automaticamente per la struct, senza che tu debba farlo manualmente 🚀.
Se adesso esegui il codice, funzionerà:
$ cargo run -- arg1 arg2 arg3
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/combiner arg1 arg2 arg3`
Args { image_1: "arg1", image_2: "arg2", output: "arg3" }
Step 4 – Aggiungere un crate esterno
Nello stesso modo in cui altri linguaggi hanno librerie o pacchetti, Rust ha i crate. Per poter codificare e decodificare immagini, puoi usare il crate image
.
Aggiungi il crate image
versione 0.23.14
al file Cargo.toml
:
[package]
name = "combiner"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
image = "0.23.14"
Alla prossima chiamata cargo run
, Cargo recupererà e installerà il crate image
.
Step 5 – Leggere un file immagine
Il crate image
contiene un modulo io
che comprende una struct Reader
. Questa struct implementa una funzione open
che prende un percorso a un file immagine e restituisce un Result
contenete un lettore. Puoi formattare e decodificare questo lettore per ottenere il formato (ad esempio PNG, JGP e via dicendo) e i dati dell'immagine.
Crea una funzione chiamata find_image_from_path
per aprire il file immagine dall'argomento path
:
fn find_image_from_path(path: String) -> (DynamicImage, ImageFormat) {
let image_reader: Reader<BufReader<File>> = Reader::open(path).unwrap();
let image_format: ImageFormat = image_reader.format().unwrap();
let image: DynamicImage = image_reader.decode().unwrap();
(image, image_format)
}
Le variabili image
e image_format
vengono restituite come una tupla.
In cima al file, includi gli import necessari:
use image::{ io::Reader, DynamicImage, ImageFormat };
fn main() {
// ...
let (image_1, image_1_format) = find_image_from_path(args.image_1);
let (image_2, image_2_format) = find_image_from_path(args.image_2);
}
All'interno di main
, la tupla restituita può essere destrutturata in due nuove variabili per ogni percorso di immagine.
Step 6 – Gestire gli errori con Result
È importante essere in grado di gestire gli errori che possono presentarsi. Ad esempio, potresti avere un caso in cui due immagini di formato diverso vengono passate come argomenti da combinare.
Un modo semantico di gestire un errore del genere è restituire un Result
che può consistere in un Ok
o un Err
.
fn main() -> Result<(), ImageDataErrors> {
let args = Args::new();
println!("{:?}", args);
let (image_1, image_1_format) = find_image_from_path(args.image_1);
let (image_2, image_2_format) = find_image_from_path(args.image_2);
if image_1_format != image_2_format {
return Err(ImageDataErrors::DifferentImageFormats);
}
Ok(())
}
La funzione main
restituisce un Err
contenente un enum con un gruppo di varianti DifferentImageFormats
se le due immagini non sono uguali. Altrimenti restituisce Ok
con una tupla vuota.
L'enum è definito come:
enum ImageDataErrors {
DifferentImageFormats,
}
Step 7 – Ridimensionare le immagini
Per rendere più semplice la combinazione delle immagini, devi ridimensionare l'immagine più grande fino a farla combaciare con le dimensioni di quella più piccola.
Prima di tutto, puoi trovare l'immagine più piccola usando il metodo dimensions
che restituisce la larghezza e l'altezza dell'immagine come una tupla. Queste tuple possono essere confrontate e la più piccola restituita:
fn get_smallest_dimensions(dim_1: (u32, u32), dim_2: (u32, u32)) -> (u32, u32) {
let pix_1 = dim_1.0 * dim_1.1;
let pix_2 = dim_2.0 * dim_2.1;
return if pix_1 < pix_2 { dim_1 } else { dim_2 };
}
I valori della tupla vengono consultati usando la notazione a punto con indicizzazione a base zero.
Se image_2
è l'immagine più piccola, allora image_1
deve essere ridimensionata fino a combaciare con le dimensioni più piccole. Altrimenti, image_2
deve essere ridimensionata.
fn standardise_size(image_1: DynamicImage, image_2: DynamicImage) -> (DynamicImage, DynamicImage) {
let (width, height) = get_smallest_dimensions(image_1.dimensions(), image_2.dimensions());
println!("width: {}, height: {}\n", width, height);
if image_2.dimensions() == (width, height) {
(image_1.resize_exact(width, height, Triangle), image_2)
} else {
(image_1, image_2.resize_exact(width, height, Triangle))
}
}
Il metodo resize_exact
implementato sulla struct DynamicImage
importa mutabilmente l'immagine e usando gli argomenti width
, height
e FilterType
ridimensiona l'immagine.
Usando il valore di ritorno della funzione standardise_size
, puoi ridichiarare le variabili image_1
e image_2
:
use image::{ io::Reader, DynamicImage, ImageFormat, imageops::FilterType::Triangle };
fn main() -> Result<(), ImageDataErrors> {
// ...
let (image_1, image_2) = standardise_size(image_1, image_2);
Ok(())
}
Step 8 – Creare una FloatingImage
Per gestire l'output, crea una struct temporanea in cui conservare i metadati per l'immagine di output.
Definisci una struct chiamata FloatingImage
per memorizzare width
, height
e data
dell'immagine, così come il nome del file di output:
struct FloatingImage {
width: u32,
height: u32,
data: Vec<u8>,
name: String,
}
Poi, implementa una funzione new
per FloatingImage
che prende i valori width
, height
e name
dell'immagine di output:
impl FloatingImage {
fn new(width: u32, height: u32, name: String) -> Self {
let buffer_capacity = 3_655_744;
let buffer: Vec<u8> = Vec::with_capacity(buffer_capacity);
FloatingImage {
width,
height,
data: buffer,
name,
}
}
}
Dato che non hai ancora creato i dati per l'immagine, crea un buffer in forma di un Vec
di u8
con capacità di 3,655,744 (956 x 956 x 4). La sintassi <numeri>_<numeri>
è la numerazione di facile lettura di Rust, che separa i numeri in gruppi di tre cifre.
Usa i valori width
e height
della variabile image_1
per creare un'istanza di FloatingImage
e usa il terzo argomento memorizzato in args
per impostare il nome di FloatingImage
:
fn main() -> Result<(), ImageDataErrors> {
// ...
let mut output = FloatingImage::new(image_1.width(), image_1.height(), args.output);
Ok(())
}
Dichiara le variabili output
come mutabili in modo da poter modificare il campo data
in un secondo momento.
Step 9 – Creare i dati dell'immagine combinata
Per poter elaborare l'immagine, devi convertirla in un vettore di pixel RGBA. I pixel sono memorizzati come u8
, perché il loro valore è compreso tra 0 e 255.
La struct DynamicImage
implementa il metodo to_rgba8
, che restituisce un ImageBuffer
contenente un Vec<u8>
, e ImageBuffer
implementa il metodo into_vec
, che restituisce Vec<u8>
:
fn combine_images(image_1: DynamicImage, image_2: DynamicImage) -> Vec<u8> {
let vec_1 = image_1.to_rgba8().into_vec();
let vec_2 = image_2.to_rgba8().into_vec();
alternate_pixels(vec_1, vec_2)
}
Poi, le variabili vec_1
e vec_2
vengono passate alla funzione alternate_pixels
che restituisce i dati dell'immagine combinata alternando i set di pixel RGBA delle due immagini:
fn alternate_pixels(vec_1: Vec<u8>, vec_2: Vec<u8>) -> Vec<u8> {
// Un Vec<u8> viene creato con la stessa lunghezza di vec_1
let mut combined_data = vec![0u8; vec_1.len()];
let mut i = 0;
while i < vec_1.len() {
if i % 8 == 0 {
combined_data.splice(i..=i + 3, set_rgba(&vec_1, i, i + 3));
} else {
combined_data.splice(i..=i + 3, set_rgba(&vec_2, i, i + 3));
}
i += 4;
}
combined_data
}
La funzione set_rgba
prende un riferimento a Vec<u8>
e restituisce i set di pixel RGBA per quel Vec<u8>
iniziando e terminando a un dato indice:
fn set_rgba(vec: &Vec<u8>, start: usize, end: usize) -> Vec<u8> {
let mut rgba = Vec::new();
for i in start..=end {
let val = match vec.get(i) {
Some(d) => *d,
None => panic!("Index out of bounds"),
};
rgba.push(val);
}
rgba
}
La sintassi ..=
è la sintassi range di Rust, che fa sì che l'intervallo includa il valore finale. Il simbolo *
prima della variabile è l'operatore di de-referenziazione, che consente di accedere al valore della variabile.
Quindi, assegna il valore di ritorno di combine_images
alla variabile combined_data
:
fn main() -> Result<(), ImageDataErrors> {
// ...
let combined_data = combine_images(image_1, image_2);
Ok(())
}
Step 10 – Inserire i dati combinati in FloatingImage
Per impostare i dati di combined_data
nell'imagine output
, viene definito un metodo su FloatingImage
per impostare il campo data
di output
con il valore di combined_data
.
Finora, hai implementato delle funzioni solo su struct. I metodi sono definiti in un modo simile, ma prendono un'istanza della struct come primo argomento:
struct MyStruct {
name: String,
}
impl MyStruct {
fn change_name(&mut self, new_name: &str) {
self.name = new_name.to_string();
}
}
let mut my_struct = MyStruct { name: String::from("Shaun") };
// my_struct.name == "Shaun"
my_struct.change_name("Tom");
// my_struct.name == "Tom"
Dato che hai bisogno di cambiare il valore dell'istanza di FloatingImage
, il metodo set_data
prende un riferimento mutabile all'istanza come primo argomento.
impl FloatingImage {
// ...
fn set_data(&mut self, data: Vec<u8>) -> Result<(), ImageDataErrors> {
// If the previously assigned buffer is too small to hold the new data
if data.len() > self.data.capacity() {
return Err(ImageDataErrors::BufferTooSmall);
}
self.data = data;
Ok(())
}
}
L'enum deve essere esteso per includere il nuovo gruppo di varianti BufferTooSmall
:
enum ImageDataErrors {
// ...
BufferTooSmall,
}
Nota: il metodo è ancora chiamato con un solo argomento:
fn main() -> Result<(), ImageDataErrors> {
// ...
output.set_data(combined_data)?;
Ok(())
}
La sintassi ?
alla fine dell'espressione è una scorciatoia per gestire il risultato della chiamata di una funzione. Se la funzione restituisce un errore, l'operatore di propagazione dell'errore restituirà l'errore dalla chiamata della funzione.
Step 11 – Scrivere l'immagine in un file
Finalmente, salva la nuova immagine in un file. Il crate image
possiede una funzione save_buffer_with_format
che la seguente forma:
fn save_buffer_with_format(
path: AsRef<Path>,
buf: &[u8],
width: u32,
height: u32,
color: image::ColorType,
format: image::ImageFormat
) -> image::ImageResult<()>;
Vedendo il modo in cui AsRef
è implementato per String
, puoi usare un argomento di tipo String
per path
.
fn main() -> Result<(), ImageDataErrors> {
// ...
image::save_buffer_with_format(
output.name,
&output.data,
output.width,
output.height,
image::ColorType::Rgba8,
image_1_format,
)
.unwrap();
Ok(())
}
Step 12 – Mettere tutto insieme
Questo è il codice finale:
mod args;
use args::Args;
use image::{
imageops::FilterType::Triangle, io::Reader, DynamicImage, GenericImageView, ImageFormat,
};
fn main() -> Result<(), ImageDataErrors> {
let args = Args::new();
println!("{:?}", args);
let (image_1, image_1_format) = find_image_from_path(args.image_1);
let (image_2, image_2_format) = find_image_from_path(args.image_2);
if image_1_format != image_2_format {
return Err(ImageDataErrors::DifferentImageFormats);
}
let (image_1, image_2) = standardise_size(image_1, image_2);
let mut output = FloatingImage::new(image_1.width(), image_1.height(), args.output);
let combined_data = combine_images(image_1, image_2);
output.set_data(combined_data)?;
image::save_buffer_with_format(
output.name,
&output.data,
output.width,
output.height,
image::ColorType::Rgba8,
image_1_format,
)
.unwrap();
Ok(())
}
enum ImageDataErrors {
BufferTooSmall,
DifferentImageFormats,
}
struct FloatingImage {
width: u32,
height: u32,
data: Vec<u8>,
name: String,
}
impl FloatingImage {
fn new(width: u32, height: u32, name: String) -> Self {
let buffer_capacity = 3_655_744;
let buffer: Vec<u8> = Vec::with_capacity(buffer_capacity);
FloatingImage {
width,
height,
data: buffer,
name,
}
}
fn set_data(&mut self, data: Vec<u8>) -> Result<(), ImageDataErrors> {
if data.len() > self.data.capacity() {
return Err(ImageDataErrors::BufferTooSmall);
}
self.data = data;
Ok(())
}
}
fn find_image_from_path(path: String) -> (DynamicImage, ImageFormat) {
let image_reader = Reader::open(path).unwrap();
let image_format = image_reader.format().unwrap();
let image = image_reader.decode().unwrap();
(image, image_format)
}
fn standardise_size(image_1: DynamicImage, image_2: DynamicImage) -> (DynamicImage, DynamicImage) {
let (width, height) = get_smallest_dimensions(image_1.dimensions(), image_2.dimensions());
println!("width: {}, height: {}\n", width, height);
if image_2.dimensions() == (width, height) {
(image_1.resize_exact(width, height, Triangle), image_2)
} else {
(image_1, image_2.resize_exact(width, height, Triangle))
}
}
fn get_smallest_dimensions(dim_1: (u32, u32), dim_2: (u32, u32)) -> (u32, u32) {
let pix_1 = dim_1.0 * dim_1.1;
let pix_2 = dim_2.0 * dim_2.1;
return if pix_1 < pix_2 { dim_1 } else { dim_2 };
}
fn combine_images(image_1: DynamicImage, image_2: DynamicImage) -> Vec<u8> {
let vec_1 = image_1.to_rgba8().into_vec();
let vec_2 = image_2.to_rgba8().into_vec();
alternate_pixels(vec_1, vec_2)
}
fn alternate_pixels(vec_1: Vec<u8>, vec_2: Vec<u8>) -> Vec<u8> {
let mut combined_data = vec![0u8; vec_1.len()];
let mut i = 0;
while i < vec_1.len() {
if i % 8 == 0 {
combined_data.splice(i..=i + 3, set_rgba(&vec_1, i, i + 3));
} else {
combined_data.splice(i..=i + 3, set_rgba(&vec_2, i, i + 3));
}
i += 4;
}
combined_data
}
fn set_rgba(vec: &Vec<u8>, start: usize, end: usize) -> Vec<u8> {
let mut rgba = Vec::new();
for i in start..=end {
let val = match vec.get(i) {
Some(d) => *d,
None => panic!("Index out of bounds"),
};
rgba.push(val);
}
rgba
}
Fai il build del binario:
$ cargo build --release
E crea un'immagine combinata usando le immagini freeCodeCamp/Rust-In-Replit
:
$ ./target/release/combiner images/pro.png images/fcc_glyph.png images/output.png
Ed ecco il risultato in images/output.png
:
Conclusione
Con questo, conosci le basi di Rust, ma c'è ancora tanto da imparare.
Continua a seguirci per altri contenuti 😉.