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

Generics y traits en Rust

Los generics son un mecanismo indispensable en un lenguaje de programación.

C++ Los lenguajes utilizan "plantillas" para implementar generics, mientras que el lenguaje C no tiene un mecanismo de generics, lo que hace que sea difícil construir proyectos de tipo complejo en C.

El mecanismo de generics es una herramienta utilizada por los lenguajes de programación para expresar abstracciones de tipo, generalmente utilizado en clases con funcionalidad determinada y tipo de datos no determinado, como listas enlazadas, tablas de mapeo, etc.

Definir generics en la función

Esta es una forma de ordenamiento de selección para números enteros:

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));
}

Resultado de ejecución:

max = 6

Este es un programa simple para obtener el valor máximo, que se puede usar para procesar i32 Los datos de tipo numérico, pero no se puede usar en f64 Los datos de tipo. Al usar generics, podemos hacer que esta función sea utilizable para varios tipos. Sin embargo, no todos los tipos de datos pueden compararse entre sí, por lo que el siguiente código no se utiliza para ejecutarse, sino para describir la sintaxis del generics de la función:

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]
}

Generics en estructuras y enumeraciones

En los enumerados Option y Result que hemos aprendido anteriormente son genéricos.

Las estructuras y las clases enumeradas en Rust pueden implementar el mecanismo genérico.

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

Esta es una estructura de coordenadas de punto, T representa el tipo numérico que describe las coordenadas del punto. Podemos usarlo así:

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

Al usarlo no se declara el tipo, aquí se utiliza el mecanismo de tipo automático, pero no se permite que aparezcan situaciones de no coincidencia de tipo como:

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

x y 1 al enlazar ya se ha establecido T como i32por lo que no se permite que aparezca f64 el tipo. Si queremos que x y y se representen con tipos de datos diferentes, podemos usar dos identificadores genéricos:

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

En las clases enumeradas se representan métodos genéricos como Option y Result:

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

Ambos tipos de estructuras y enumeraciones pueden definir métodos, por lo tanto, los métodos también deben implementar el mecanismo genérico, de lo contrario, las clases genéricas no podrán ser operadas de manera efectiva por métodos.

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

Resultado de ejecución:

p.x = 1

Atención, debe haber <T> después del keyword impl, ya que T es el modelo para lo que sigue. Pero también podemos agregar métodos a uno de los genéricos:

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

La implementación genérica del bloque en sí no impide que sus métodos internos tengan capacidad genérica:

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

El método mixup fusiona el punto x de un Point<T, U> con el punto y de un Point<V, W> para formar un nuevo punto de tipo Point<T, W>.

Característica

El concepto de característica (trait) es similar al de interfaz en Java (Interface), pero no son completamente iguales. Las características y las interfaces tienen en común que son un tipo de especificación de comportamiento que se puede usar para identificar qué clases tienen qué métodos.

Las características en Rust se representan con trait:

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

Descriptive especifica que el implementador debe tener es describe(&self) -> Método String.

Lo usamos para implementar una estructura:

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

El formato es:

impl <Nombre de la característica> for <Nombre del tipo implementado>

En Rust, un mismo tipo puede implementar múltiples características, pero cada bloque impl solo puede implementar una.

Característica por defecto

Este es el punto de diferencia entre la característica y la interfaz: la interfaz solo puede especificar métodos y no puede definir métodos, pero la característica puede definir métodos como métodos por defecto, ya que son "por defecto", los objetos pueden redefinir métodos o usar métodos por defecto sin redefinirlos:

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 {
        nombre: String::from("Cali"),
        edad: 24
    };
    println!("{}", cali.describe());
}

Resultado de ejecución:

Cali 24

Si eliminamos el contenido del bloque impl Descriptive for Person, el resultado de la ejecución será:

[Object]

Característica como parámetro

En muchos casos, necesitamos pasar una función como parámetro, por ejemplo, funciones de devolución, eventos de botones, etc. En Java, la función debe ser pasada mediante una instancia de clase que implements la interfaz, en Rust se puede lograr mediante la transmisión de parámetros de características:

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

Cualquier objeto que haya implementado la característica Descriptive puede ser utilizado como parámetro de esta función, esta función no necesita saber si el objeto recibido tiene otras propiedades o métodos, solo necesita saber que tiene el método estándar de la característica Descriptive. Por supuesto, también no se pueden usar otras propiedades o métodos dentro de esta función.

Los parámetros de característica también pueden ser implementados usando esta gramática equivalente:

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

Esta es una sugerencia de gramática similar a la generica, que es muy útil cuando hay múltiples tipos de parámetros que son características:

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

Cuando una característica es usada como un tipo y implica múltiples características, se puede usar + Símbolos, por ejemplo:

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

Nota:Únicamente para representar tipos, no significa que pueda usarse en el bloque impl.

Las relaciones de implementación complejas pueden ser simplificadas usando la palabra clave where, por ejemplo:

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

Puede simplificarse a:

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

En conocimiento de esta gramática, el caso "tomar el máximo" de la sección de generics puede ser realizado realmente:

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!("el máximo de arr es {}", max(&arr));
}

Resultado de ejecución:

el máximo de arr es 5

Consejo: Debido a que debe declararse el segundo parámetro de la función compare como el tipo que ha implementado la característica, la palabra clave Self (tenga en cuenta la capitalización) representa el tipo actual (no el ejemplo) mismo.

Valor de retorno de característica

El formato de valor de retorno de característica es:

fn persona() -> impl Descriptive {
    Persona {
        nombre: String::from("Cali"),
        edad: 24
    }
}

Pero hay un punto, las características como valor de retorno solo aceptan objetos que han implementado la característica y todos los tipos posibles de retorno en la misma función deben ser completamente iguales. Por ejemplo, las estructuras A y B han implementado la característica Trait, el siguiente función es incorrecta:

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

Métodos de implementación condicional

La función impl es muy potente, podemos usarla para implementar métodos de clase. Pero para las clases genéricas, a veces necesitamos distinguir entre los métodos implementados por el tipo genérico para decidir cuál método implementar a continuación:

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

Este código declara que el tipo A<T> debe ser efectivo para impl este bloque impl previo a que T haya implementado las características B y C.