Перейти к содержанию

Заметки о Rust - введение и примеры по trait

Posted on:30 июня 2023 г. at 13:12

Example Dynamic OG Image link

Что такое трейты?

В Rust трейт - это языковая особенность, позволяющая определять абстрактное поведение и методы, которые могут реализовывать другие типы, позволяя абстрагироваться над поведением. Трейты определяют общее поведение, которое могут иметь различные типы.

Для определения трейта используется ключевое слово trait, за которым следует его имя и набор сигнатур метода, определенных в фигурных скобках {}.

Рассмотрим пример:

trait Speak {
    fn speak(&self);
}

В приведенном выше примере мы определили трейт Speak, который имеет один метод speak. Любой тип, реализующий этот трейт, обязан определять этот метод.

Реализация трейтов

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

Давайте напишем наш трейт Speak для Dog и Human структур.

struct Dog {
    name: String,
}

struct Human {
    name: String,
}

impl Speak for Dog {
    fn speak(&self) {
        println!("{} says: Woof!", self.name);
    }
}

impl Speak for Human {
    fn speak(&self) {
        println!("{} says: Hello!", self.name);
    }
}

Здесь мы определили две структуры Dog и Human, оба имеют поле имени. Затем мы внедрили трейт Speak для обеих структур с их версиями метода speak.

Использование трейтов

Теперь мы можем использовать эти трейты в наших функциях. Вот пример:

fn make_speak<T: Speak>(t: T) {
    t.speak();
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

make_speak(dog);  // выведет "Fido says: Woof!"
make_speak(human); // выведет "Alice says: Hello!"

В приведенном выше коде make_speak является обобщенной функцией, которая принимает любой тип T, реализующий трейт Speak. Теперь мы можем передать любой тип, который реализует Speak этой функции.

Реализации по умолчанию

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

trait Speak {
    fn speak(&self) {
        println!("Hello, I can't specify my species yet!");
    }
}

impl Speak for Dog {
    // Мы не предоставляем метод 'speak' здесь, поэтому Dog использует значение по умолчанию.
}

impl Speak for Human {
    fn speak(&self) {
        println!("{} says: Hello, I am a human!", self.name);
    }
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

make_speak(dog);  // выведет "Hello, I can't specify my species yet!"
make_speak(human); // выведет "Alice says: Hello, I am a human!"

В этом примере Dog использует метод speak по умолчанию из трейта Speak, но Human обеспечивает его реализацию сам.

Границы трейтов

Границы трейтов могут ограничивать типы, используемые в базовой функции. Например, можно указать, что параметр функции должен реализовывать определенный трейт.

fn make_speak<T: Speak>(t: T) {
    t.speak();
}

В этой сигнатуре функции T: Speak является привязкой трейта, которая означает «любой тип T, реализующий трейт Speak».

Трейты как параметры

Одно из наиболее распространенных применений трейтов - в параметрах функций и методов. Они позволяют функциям и методам принимать параметры различных типов. Если у вас есть функция, которая принимает трейт вместо типа, она может получить любой тип, который его реализует. Это фундаментальный способ достижения полиморфизма у Rust.

Рассмотрим, как использовать трейт в качестве параметра:

fn say_hello(speaker: &dyn Speak) {
    speaker.speak();
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

say_hello(&dog);  // выведет "Fido says: Woof!"
say_hello(&human); // выведет "Alice says: Hello!"

Здесь say_hello принимает ссылку на любой тип, реализующий трейт Speak.

Трейты для перегрузки операторов

Трейты также могут использоваться для перегрузки определенных операторов у ваших типов. Rust обладает уникальными трейтами в стандартной библиотеке для перегрузки операторов. Например, функция std::ops::Add позволяет перегрузить оператор +:

use std::ops::Add;

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

let p1 = Point { x: 1, y: 0 };
let p2 = Point { x: 2, y: 3 };

let p3 = p1 + p2;

println!("Point: ({}, {})", p3.x, p3.y); // выведет "Point: (3, 3)"

Здесь мы внедрили трейт Add для нашей структуры Point, позволяющий использовать оператор + для сложения двух точек вместе.

Трейты и наследование

Rust не имеет классического наследования, в отличие от объектно-ориентированных языков, но можно определить трейт в терминах другого трейта. Это способ составления моделей поведения. Эта функция позволяет трейту опираться на функциональность другого трейта.

Вот пример:

trait Animal {
    fn name(&self) -> String;
}

trait Speak: Animal {
    fn speak(&self) {
        println!("{} can't speak", self.name());
    }
}

impl Animal for Dog {
    fn name(&self) -> String {
        self.name.clone()
    }
}

impl Speak for Dog {}

let dog = Dog { name: String::from("Fido") };
dog.speak();  // выведет "Fido can't speak"

Здесь мы определяем базовый трейт Animal и другой трейт Speak, который зависит от Animal. Следовательно, реализация Speak требует реализации Animal.

В заключение

Трейты Rust мощны и гибки. Они позволяют нам абстрагировать поведение различных типов, обеспечивая полиморфизм и делая наш код более универсальным и повторно используемым. Понимая и используя трейты, мы можем использовать систему типов Rust для написания безопасного и эффективного кода. В то время как мы рассмотрели основные темы в этой статье, остается ещё много чего, что изучить о трейтах, таких как объекты трейтов, срок службы спецификации в методах трейтов, супертрейты и т.д. Но с этим фундаментом вы готовы продолжать свой путь в изучении Rust.