English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Generici e caratteristiche di Rust

I generics sono un meccanismo indispensabile per un linguaggio di programmazione.

Nel linguaggio C++, i generics vengono implementati tramite "template", mentre nel linguaggio C non esiste un meccanismo di generics, il che rende difficile costruire progetti complessi di tipo.

Il meccanismo generics è un meccanismo utilizzato dai linguaggi di programmazione per esprimere astrazioni di tipo, generalmente utilizzato per classi con funzionalità determinate e tipi di dati indeterminati, come elenchi, mappe, ecc.

Definire generics nel contesto della funzione

Questo è un metodo di ordinamento per numeri interi.

fn max(array: &[i32]) -> i32 {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}
fn main() {
    let a = [2, 4, 6, 3, 1];
    println!("max = {}", max(&a));
}

Risultato dell'esecuzione:

max = 6

Questo è un programma semplice per trovare il valore massimo, che può essere utilizzato per trattare dati di tipo i32, ma non può essere utilizzato per dati di tipo f64. Utilizzando i generici, possiamo rendere questa funzione utilizzabile per vari tipi di dati. Tuttavia, non tutti i tipi di dati possono essere confrontati, quindi il seguente blocco di codice non è destinato ad essere eseguito, ma serve a descrivere la sintassi del generics della funzione:

fn max<T>(array: &[T]) -> T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}

Generici nella struttura e nelle classi Enumerazione

Negli studi precedenti abbiamo imparato che le classi Enumerazione Option e Result sono generiche.

Le strutture e le classi enumerative in Rust possono implementare il meccanismo generico.

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

Questo è un tipo di struttura di coordinate di punto, T rappresenta il tipo numerico che descrive le coordinate del punto. Possiamo usarlo così:

let p1 = Point { x: 1, y: 2};
let p2 = Point { x: 1.0, y: 2.0};

Quando si utilizza, non è necessario dichiarare il tipo, qui viene utilizzato il meccanismo di tipo automatico, ma non è permesso che ci siano casi di non corrispondenza di tipo come:

let p = Point { x: 1, y: 2.0};

Quando x è vincolato a 1, T è già stato impostato come i32, quindi non è permesso che compaia il tipo f64. Se vogliamo che x e y siano rappresentati da tipi di dati diversi, possiamo usare due identificatori generici:

struct Point<T1, T2> {
    x: T1,
    y: T2
}

Nella classe enumerativa, i metodi generici come Option e Result:

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

Sia i tipi di struttura che i tipi enumerativi possono definire metodi, quindi i metodi devono implementare il meccanismo generico, altrimenti le classi generiche non possono essere operati efficacemente dai metodi.

struct Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}
fn main() {
    let p = Point { x: 1, y: 2 };
    println!("p.x = {}", p.x());
}

Risultato dell'esecuzione:

p.x = 1

Attenzione, dopo il keyword impl deve esserci <T>, poiché T è il modello per il quale è stato definito. Ma possiamo anche aggiungere un metodo a uno dei generici:

impl Point<f64> {
    fn x(&self) -> f64 {
        self.x
    }
}

Il blocco impl non impedisce alla sua funzionalità generica interna di avere capacità generiche:

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

Il metodo mixup fonde l'asse x di un punto Point<T, U> con l'asse y di un punto Point<V, W> in un nuovo punto di tipo Point<T, W>.

特性

特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。

特性在 Rust 中用 trait 表示:

trait Descriptive {
    fn describe(&self) -> String;
}

Descriptive 指定了实现者必须有 describe(&self) -> String 方法。

我们用它实现一个结构体:

struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}

格式是:

impl <特性名> for <所实现的类型名>

Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。

默认特性

这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:

trait Descriptive {
    fn describe(&self) -> String {
        String::from("[Object]")
    }
}
struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}
fn main() {
    let cali = Person {
        nome: String::from("Cali"),
        età: 24
    };
    println!("{}", cali.describe());
}

Risultato dell'esecuzione:

Cali 24

如果我们将 impl Descriptive for Person 块中的内容去掉,那么运行结果就是:

[Object]

特性做参数

很多情况下我们需要传递一个函数作为参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类示例来传递,在 Rust 中可以通过传递特性参数来实现:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没有必要了解传入对象是否有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内部也无法使用其他的属性与方法。

特性参数还可以用这种等效语法实现:

fn output<T: Descriptive>(object: T) {
    println!("{}", object.describe());
}

Questo è uno zucchero sintattico simile ai Generici, che è molto utile quando ci sono più tipi di parametri che sono caratteristiche:

fn output_two<T: Descriptive>(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}

Quando le caratteristiche sono rappresentate come tipi e coinvolgono più caratteristiche, può essere utilizzato il simbolo + per rappresentare, ad esempio:

fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)

Attenzione:Quando viene utilizzato solo per rappresentare tipi, non significa che può essere utilizzato nel blocco impl.

Relazioni di implementazione complesse possono essere semplificate utilizzando la chiave where, ad esempio:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)

Può essere semplificato in:

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug

Dopo aver compreso questa sintassi, il caso di esempio "Estrazione del Massimo" nella sezione dei Generici può essere effettivamente implementato:

trait Comparable {
    fn compare(&self, object: &Self) -> i8;
}
fn max<T: Comparable>(array: &[T]) -> &T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i].compare(&array[max_index]) > 0 {
            max_index = i;
        }
        i += 1;
    }
    &array[max_index]
}
impl Comparable for f64 {
    fn compare(&self, object: &f64) -> i8 {
        if &self > &object { 1 }
        else if &self == &object { 0 }
        else { -1 }
    }
}
fn main() {
    let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
    println!("Il massimo di arr è {}", max(&arr));
}

Risultato dell'esecuzione:

Il massimo di arr è 5

Suggerimento: Poiché è necessario dichiarare che il secondo parametro della funzione compare deve essere dello stesso tipo che ha implementato la caratteristica, la parola chiave Self (attenzione alle maiuscole) rappresenta il tipo corrente (non l'esempio) stesso.

Valore di ritorno delle caratteristiche

Formato del valore di ritorno delle caratteristiche

fn person() -> impl Descriptive {
    Person {
        nome: String::from("Cali"),
        età: 24
    }
}

Ma c'è un punto, le caratteristiche come valore di ritorno accettano solo oggetti che hanno implementato la caratteristica e tutti i tipi possibili di ritorno all'interno della stessa funzione devono essere completamente identici. Ad esempio, le strutture A e B hanno entrambe implementato la caratteristica Trait, la seguente funzione è errata:

fn some_function(bool bl) -> impl Descriptive {
    if bl {
        return A {};
    } else {
        return B {};
    }
}

Metodi di implementazione condizionale

L'implementazione di funzionalità è molto potente, possiamo usarla per implementare i metodi delle classi. Ma per le classi generiche, a volte dobbiamo distinguere i metodi implementati dal tipo generico per determinare quali metodi implementare di seguito:

struct A<T> {}
impl<T: B + C> A<T> {
    fn d(&self) {}
}

Questo codice dichiara che il tipo A<T> deve essere implementato efficacemente solo se T ha già implementato le caratteristiche B e C.