Обобщения и трейты в Rust

Обобщения — незаменимый механизм в языках программирования.

C++ использует "шаблоны" для реализации обобщений, тогда как в C нет механизма обобщений, что затрудняет создание проектов со сложными типами на C.

Механизм обобщений — это механизм, используемый языками программирования для выражения абстракции типов, обычно применяемый для классов с определённой функциональностью, но неуказанными типами данных, таких как связные списки и словари.

Определение обобщений в функциях

Это метод сортировки целых чисел выбором:

Пример

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

Вывод:

max = 6

Это простая программа для получения максимального значения, которая может обрабатывать данные числового типа i32, но не может обрабатывать данные типа f64. Используя обобщения, мы можем сделать эту функцию пригодной для различных типов. Однако в реальности не все типы данных можно сравнивать по величине, поэтому следующий фрагмент кода не предназначен для запуска, а описывает синтаксис обобщений функций:

Пример

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

Обобщения в структурах и перечислениях

Перечисления Option и Result, которые мы изучили ранее, являются обобщёнными.

И структуры, и перечисления в Rust могут реализовывать механизм обобщений.

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

Это структура координат точки, где T представляет числовой тип, описывающий координаты точки. Мы можем использовать её так:

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

При использовании тип не объявляется; здесь используется механизм автоматического вывода типов, но несоответствия типов не допускаются, как показано ниже:

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

Когда x привязан к 1, T уже установлен в i32, поэтому тип f64 не допускается. Если мы хотим, чтобы x и y были представлены разными типами данных, мы можем использовать два идентификатора обобщения:

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

В перечислениях обобщения выражаются такими способами, как Option и Result:

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

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

И структуры, и перечисления могут определять методы, поэтому методы также должны реализовывать механизм обобщений; иначе обобщённые типы не смогут эффективно обрабатываться методами.

Пример

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

Вывод:

p.x = 1

Обратите внимание, что за ключевым словом impl должно следовать<T><T>, поскольку T после него задаётся по его образцу.

impl Point<f64> {
    fn x(&self) -> f64 {
        self.x
    }
}
Обобщения самого блока impl не препятствуют тому, чтобы его внутренние методы обладали возможностями обобщений:
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,
        }
    }
}

Метод mixup объединяет x точки Point<T, U> с y точки Point<V, W> в новую точку типа Point<T, W>.


Трейты

Концепция трейтов близка к интерфейсам в Java, но эти два понятия не совсем одинаковы. Общее между трейтами и интерфейсами состоит в том, что они оба являются спецификациями поведения, используемыми для определения того, какие классы имеют какие методы.

Трейты представлены в 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 {
        name: String::from("Cali"),
        age: 24
    };
    println!("{}", cali.describe());
}

Вывод:

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

Это синтаксический сахар в стиле, похожем на обобщения. Этот синтаксический сахар очень полезен, когда несколько типов параметров являются трейтами:

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

Когда трейты используются как представления типов и задействовано несколько трейтов, можно использовать++ символ для обозначения, например:

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

Примечание:Оно используется только при представлении типов; это не означает, что его можно использовать в блоках impl.

Сложные отношения реализаций можно упростить с помощью ключевого слова where, например:

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

можно упростить до:

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

После понимания этого синтаксиса пример «получение максимального значения» из главы об обобщениях можно действительно реализовать:

Пример

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!("максимум arr равен {}", max(&arr));
}

Вывод:

maximum of arr is 5

Совет:Поскольку необходимо объявить, что второй параметр функции сравнения должен совпадать с типом, реализующим трейт, ключевое слово Self (обратите внимание на регистр) представляет сам текущий тип (а не экземпляр).

Трейты как возвращаемые значения

Формат для трейтов в качестве возвращаемых значений следующий:

Пример

fn person() -> impl Descriptive {
    Person {
        name: String::from("Cali"),
        age: 24
    }
}

Но есть один момент: когда трейт используется как возвращаемое значение, в качестве возвращаемых значений принимаются только объекты, реализующие этот трейт, и все возможные возвращаемые типы в одной и той же функции должны быть в точности одинаковыми. Например, если и структура A, и структура B реализуют трейт Trait, следующая функция является ошибочной:

Пример

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

Условная реализация методов

Функциональность impl очень мощная; мы можем использовать её для реализации методов для типа. Но для обобщённых типов иногда нужно различать, какие методы уже реализованы у соответствующего обобщённого типа, чтобы решить, какие методы следует реализовать далее:

struct A<T> {}

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

Этот код объявляет, что тип A<T> может фактически реализовать этот блок impl только в том случае, если T уже реализует трейты B и C.

Другие расширения