Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 034 – Tipi di dati generici

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Tipi di Dati Generici

Utilizziamo i generics per creare definizioni per elementi come firme di funzioni o strutture, che possiamo poi utilizzare con molti tipi di dati concreti diversi. Prima vediamo come definire funzioni, strutture, enumerazioni e metodi utilizzando i generics. Successivamente discuteremo di come i generics influenzano le prestazioni del codice.

Nelle Definizioni di Funzioni

Quando definiamo una funzione che utilizza i generics, inseriamo i generics nella firma della funzione dove di solito specifica i tipi di dati dei parametri e del valore di ritorno. Farlo rende il nostro codice più flessibile e fornisce più funzionalità ai chiamanti della nostra funzione, evitando la duplicazione del codice.

Continuando con la nostra funzione più grande, la Liste di numeri più grandi, vediamo due funzioni che trovano entrambe il valore più grande in una slice. Successivamente combineremo queste in una singola funzione che utilizza i generics.

rust

fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
} largest
} fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0]; for item in list {
if item > largest {
largest = item;
}
}

largest
}

La funzione largest_i32 è quella che abbiamo estratto nel precedente esempio, che trova il più grande i32 in una slice. La funzione largest_char trova il più grande char in una slice. I corpi delle funzioni hanno lo stesso codice, quindi eliminiamo la duplicazione introducendo un tipo di parametro generico in una singola funzione.

Per parametrizzare i tipi in una nuova funzione singola, dobbiamo nominare il parametro del tipo, proprio come facciamo per i parametri di valore di una funzione. È possibile utilizzare qualsiasi identificatore come nome del parametro del tipo. Ma useremo T perché, per convenzione, i nomi dei parametri del tipo in Rust sono brevi, spesso solo una lettera, e la convenzione di denominazione dei tipi di Rust è UpperCamelCase. Breve per “tipo”, T è la scelta predefinita della maggior parte dei programmatori Rust.

Quando utilizziamo un parametro nel corpo della funzione, dobbiamo dichiarare il nome del parametro nella firma in modo che il compilatore sappia cosa significa quel nome. Allo stesso modo, quando utilizziamo un nome di parametro di tipo in una firma di funzione, dobbiamo dichiarare il nome del parametro di tipo prima di usarlo. Per definire la funzione generica largest, inserisci le dichiarazioni del nome del tipo all’interno delle parentesi angolari, <> tra il nome della funzione e l’elenco dei parametri, in questo modo:

rust

fn largest<T>(list: &[T]) -> &T {

Leggiamo questa definizione come: la funzione largest è generica su qualche tipo T. Questa funzione ha un parametro chiamato list, che è una slice di valori di tipo T. La funzione largest restituirà un riferimento a un valore dello stesso tipo T.

rust

fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}

largest
}

Se compiliamo questo codice ora, otterremo questo errore:

go

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

Il testo di aiuto menziona std::cmp::PartialOrd, che è un trait, e parleremo di trait nella prossima sezione. Per ora, sappi che questo errore indica che il corpo di largest non funzionerà per tutti i possibili tipi che T potrebbe essere. Poiché vogliamo confrontare valori di tipo T nel corpo, possiamo utilizzare solo tipi i cui valori possono essere ordinati. Per abilitare i confronti, la libreria standard ha il trait std::cmp::PartialOrd che puoi implementare su tipi (vedi Appendice C per ulteriori informazioni su questo trait). Seguendo il suggerimento del testo di aiuto, limitiamo i tipi validi per T solo a quelli che implementano PartialOrd e questo esempio compilerà, poiché la libreria standard implementa PartialOrd sia su i32 che su char.

Definizioni di Struttura

Possiamo anche definire strutture per utilizzare un parametro di tipo generico in uno o più campi usando la sintassi <> . L’esempio 10-6 definisce una struttura Point<T> per contenere i valori delle coordinate x e y di qualsiasi tipo.

rust

struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

La sintassi per l’utilizzo dei generics nelle definizioni di struct è simile a quella usata nelle definizioni di funzioni. Innanzitutto, dichiariamo il nome del parametro del tipo all’interno delle parentesi angolari subito dopo il nome della struct. Quindi usiamo il tipo generico nella definizione della struct dove altrimenti specificheremmo tipi di dati concreti.

Nota che poiché abbiamo usato un solo tipo generico per definire Point<T>, questa definizione indica che la struct Point<T> è generica su qualche tipo T, e i campi x e y sono entrambi lo stesso tipo, qualunque sia quel tipo. Se creiamo un’istanza di un Point<T> che ha valori di tipi diversi, come nell’esempio 10-7, il nostro codice non verrà compilato.

rust

struct Point<T> {
x: T,
y: T,
}

fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}

In questo esempio, quando assegniamo il valore intero 5 a x, facciamo sapere al compilatore che il tipo generico T sarà un intero per questa istanza di Point<T>. Quindi, quando specifichiamo 4.0 per y, che abbiamo definito avere lo stesso tipo di x, otterremo un errore di tipo mismatch come questo:

vbnet

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error

Per definire una struct Point dove x e y sono entrambi generici ma potrebbero avere tipi diversi, possiamo usare più parametri di tipo generico. Ad esempio, nell’esempio 10-8, cambiamo la definizione di Point per essere generica su tipi T e U dove x è di tipo T e y è di tipo U.

rust

struct Point<T, U> {
x: T,
y: U,
}

fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}

Ora tutte le istanze di Point mostrate sono consentite! Puoi utilizzare qualsiasi numero di parametri di tipo generico in una definizione, ma usarne più di alcuni rende il tuo codice difficile da leggere. Se scopri di aver bisogno di molti tipi generici nel tuo codice, potrebbe indicare che il tuo codice ha bisogno di una ristrutturazione in pezzi più piccoli.

Definizioni di Enumerazioni

Come abbiamo fatto con le strutture, possiamo definire enumerazioni per contenere tipi di dati generici nelle loro varianti. Diamo un’occhiata più da vicino alla enum Option<T> che fornisce la libreria standard, che abbiamo usato nel Capitolo 6:

rust

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

Questa definizione ora dovrebbe avere più senso per te. Come puoi vedere, la enum Option<T> è generica su tipo T e ha due varianti: Some, che contiene un valore di tipo T, e una variante None che non contiene alcun valore. Utilizzando la enum Option<T>, possiamo esprimere il concetto astratto di un valore opzionale e poiché Option<T> è generica, possiamo usare questa astrazione indipendentemente dal tipo del valore opzionale.

Le enum possono utilizzare anche più tipi generici. La definizione della enum Result che abbiamo usato nel Capitolo 9 è un esempio:

rust

enum Result<T, E> {
Ok(T),
Err(E),
}

La enum Result è generica su due tipi, T ed E, e ha due varianti: Ok, che contiene un valore di tipo T, e Err, che contiene un valore di tipo E. Questa definizione rende comodo utilizzare la enum Result ovunque abbiamo un’operazione che potrebbe avere successo (restituire un valore di qualche tipo T) o fallire (restituire un errore di qualche tipo E). Infatti, questo è quello che abbiamo usato per aprire un file nell’esempio 9-3, dove T è stato compilato con il tipo std::fs::File quando il file è stato aperto con successo e E è stato compilato con il tipo std::io::Error quando ci sono stati problemi ad aprire il file.

Quando riconosci situazioni nel tuo codice con molte definizioni di struct o enum che differiscono solo nei tipi dei valori che contengono, puoi evitare la duplicazione utilizzando tipi generici al posto.

Definizioni di Metodo

Possiamo implementare metodi su strutture e enumerazioni (come abbiamo fatto nel Capitolo 5) e utilizzare tipi generici nelle loro definizioni, anche. Nell’Esempio 10-9 viene mostrata la struttura Point<T> che abbiamo definito nell’Esempio 10-6 con un metodo chiamato x implementato su di essa.

rust

struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
} fn main() {
let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());
}

Qui, abbiamo definito un metodo chiamato x su Point<T> che restituisce un riferimento ai dati nel campo x.

Nota che dobbiamo dichiarare T subito dopo impl in modo da poter utilizzare T per specificare che stiamo implementando metodi sul tipo Point<T>. Dichiarando T come un tipo generico dopo impl, Rust può identificare che il tipo tra parentesi angolari in Point è un tipo generico piuttosto che un tipo concreto. Avremmo potuto scegliere un nome diverso per questo parametro generico rispetto al parametro generico dichiarato nella definizione della struttura, ma usare lo stesso nome è convenzionale. I metodi scritti all’interno di un impl che dichiara il tipo generico saranno definiti su qualsiasi istanza del tipo, indipendentemente dal tipo concreto che viene sostituito al tipo generico.

Possiamo anche specificare vincoli sui tipi generici quando definiamo metodi sul tipo. Potremmo, ad esempio, implementare metodi solo su istanze di Point<f32> piuttosto che su istanze di Point<T> con qualsiasi tipo generico. Nell’Esempio 10-10 utilizziamo il tipo concreto f32, il che significa che non dichiariamo alcun tipo dopo impl.

rust

impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

Questo codice significa che il tipo Point<f32> avrà un metodo distance_from_origin; altre istanze di Point<T> dove T non è di tipo f32 non avranno questo metodo definito. Il metodo misura quanto il nostro punto sia lontano dal punto alle coordinate (0.0, 0.0) e utilizza operazioni matematiche disponibili solo per i tipi in virgola mobile.

I parametri di tipo generico in una definizione di struttura non sono sempre gli stessi che si utilizzano nelle firme dei metodi di quella stessa struttura. Nell’Esempio 10-11 vengono utilizzati i tipi generici X1 e Y1 per la struttura Point e X2 Y2 per la firma del metodo mixup per rendere l’esempio più chiaro. Il metodo crea una nuova istanza di Point con il valore x del punto stesso (di tipo X1) e il valore y del Point passato (di tipo Y2).

rust

struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
} fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2);

println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Nel main, abbiamo definito un Point che ha un i32 per x (con valore 5) e un f64 per y (con valore 10.4). La variabile p2 è una struttura Point che ha una stringa per x (con valore “Ciao”) e un char per y (con valore c). Chiamando mixup su p1 con l’argomento p2 ci dà p3, che avrà un i32 per x, perché x proviene da p1. La variabile p3 avrà un char per y, perché y proviene da p2. La chiamata macro println! stamperà p3.x = 5, p3.y = c.

Lo scopo di questo esempio è quello di dimostrare una situazione in cui alcuni parametri generici vengono dichiarati con impl e alcuni vengono dichiarati con la definizione del metodo. Qui, i parametri generici X1 e Y1 sono dichiarati dopo impl perché vanno con la definizione della struttura. I parametri generici X2 e Y2 sono dichiarati dopo fn mixup, perché sono rilevanti solo per il metodo.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *