Traits

Links: https://rust.nizeclub.ru/_24.html

Статьи в разделе:

Trait

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

trait Printable { // объявление трейта
    fn print(&self) -> String; // определяем, что Trait делает. Трейт утверждает печать, значит, по контракту обязан быть метод печати.  
    fn into_tuple(self) -> (i32, i32); // модель владения &self или self
}

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

impl Printable for Point { // применяем Trait для типа данных 
    fn print(&self) -> String {
        format!("Point: ({}, {})", self.x, self.y) // пишем сам код
    }
    fn into_tuple(self) -> (i32, i32) { // self вместо &self = владение
    (self.x, self.y)  // Point поглощён, превращён tuple
    }
}

fn main() {
    let p = Point { x: 3, y: 4 };
    println!("{}", p.print()); // Вывод: Point: (3, 4)
}

По модели владения: обычно данными нужно пользоваться ещё после применения методов, поэтому всегда нужно объявлять &self и менять на self только в случае ошибок компилятора (в задачах трансформации данных, перемещения/отправки по сети, явного уничтожения).

Инициализация типажа

При объявлении типажа можно оставить обязательную реализацию на потом, либо вписать реализацию функций в типаже по-умолчанию:

// типаж задаёт метод и ограничения по входным/выходным типам
trait LandVehicle {  
    fn LandDrive(&self) -> String; }  

// типаж задаёт методы плюс их реализация по умолчанию
trait WaterVehicle {  
    fn WaterDrive(&self) { println!("Default float"); }  
}

Применение типажей к структурам данных

Во время применения, если реализация по умолчанию была задана, то можно её переделать под конкретную структуру, либо использовать эту реализацию:

struct Sedan {}  
struct Rocketship {}  

// типаж LandVehicle не имеет реализации по умолчанию, реализуем тут
impl LandVehicle for Sedan {  
    fn LandDrive(&self) -> String { format!("Car zoom-zoom!") } }

// типаж WaterVehicle имеет выше реализацию по умолчанию, используем её
impl WaterVehicle for Rocketship {}

Наследование и объединение типажей

Типажи могут наследовать другие типажи с помощью синтаксиса trait NewTrait: OldTrait:

trait Displayable: Printable {
    fn display(&self) -> String;
}

Любой тип, реализующий Displayable, обязан также реализовать Printable.

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

// создание ярлыка
trait AmphibiousVehicle: LandVehicle + WaterVehicle {}

// применение типажей к структуре
impl AmphibiousVehicle for Carrier {}  
impl LandVehicle for Carrier {  
    fn LandDrive(&self) -> String { format!("Use air thrust to travel on land") }  
}  
impl WaterVehicle for Carrier {}

Вызов методов экземпляра структуры определённого типажа

fn main() {  
    let toyota_camry = Sedan {};  
    println!("{}",toyota_camry.LandDrive());
  
    let rs = Rocketship {};  
    rs.WaterDrive();  
  
    let project_x = Carrier {};  
    println!("{}",project_x.LandDrive());  
    project_x.WaterDrive();  
}

Встроенные типажи для generic данных у функций

Типажи определяют, какие возможности есть у generic типа T:

fn my_function<T>(value: T) -> T 
where 
    T: SomeTrait + AnotherTrait 
{ /* function body */ } 

Eq (Equality)

  • Проверяет равенство == или неравенство !=;
  • Включает PartialEq (проверяет только равенство) + гарантирует тождество;
// Без Eq типажа:
// ❌ не будет компилироваться ==
fn find_value<T>(items: &[T], target: T) -> bool {
    items.iter().any(|x| x == &target) // Error
}

// Добавим Eq trait:
fn find_value<T>(items: &[T], target: T) -> bool 
where 
    T: Eq  // теперь можно использовать ==
{
    items.iter().any(|x| x == &target) // ✅ работает!
}

std::hash::Hash

  • Позволяет конвертировать значения в хэш;
  • Требуется для хранения в HashMapHashSet, или других коллекций с хэшом;
  • Тип с Hash может быть ключом или значением словаря:
use std::collections::HashSet;
// Без Hash:
// ❌ не будет компилироваться - HashSet требует Hash
fn create_set<T>(items: Vec<T>) -> HashSet<T> {
    items.into_iter().collect() // Error: T doesn't implement Hash
}

// С Hash:
fn create_set<T>(items: Vec<T>) -> HashSet<T> 
where 
    T: Eq + std::hash::Hash  // требуется для HashSet
{
    items.into_iter().collect() // ✅ работает!
}

Clone

  • Позволяет дублировать значение;
  • Разрешает метод .clone();
  • Нужно для копирования значений без взятия владения.
// Без Clone:
fn duplicate_first<T>(items: &[T]) -> Option<T> {
    items.first().map(|x| x) // Error: can't return T from &T
}

// Включаем Clone:
fn duplicate_first<T>(items: &[T]) -> Option<T> 
where 
    T: Clone  // можно клонировать
{
    items.first().cloned() // ✅ работает! - создаёт копию
}

Типовые комбинации Trait

НазначениеТиповые TraitsПример
Hash collectionsEq + HashHashMap<K, V>HashSet<T>
Sorting/orderingOrd or PartialOrdSorting vectors, BTreeMap
Display/printingDebug or Displayprintln!format!
Copying valuesClone or CopyDuplicating data

Subsections of Traits

From Conversion

From

From trait объясняет, как провести конверсию из типа A в тип B:

// Возьмём 2 типа - банан и смузи:
struct Banana {
    ripeness: u8,
}

struct Smoothie {
    flavor: String,
}

// Сообщаем как превратить банан в смузи:
impl From<Banana> for Smoothie {
    fn from(banana: Banana) -> Self {
        Smoothie {
            flavor: format!("banana ripeness level {}", banana.ripeness),
        }
    }
}

// Получаем:
let my_banana = Banana { ripeness: 8 };
let my_smoothie: Smoothie = my_banana.into();  // авто конвертация!
// ИЛИ
let my_smoothie = Smoothie::from(my_banana);    // явная конвертация!

Встроенные конверсии

String

let my_string: String = "hello".into();  // &str → String
let owned_string = String::from("hello");

File Paths

use std::path::PathBuf;
let path: PathBuf = "/home/user/docs".into();  // &str → PathBuf

Errors

// Converting different errors into one type
impl From<NetworkError> for AppError {
    fn from(error: NetworkError) -> Self {
        AppError::Network(error.to_string())
    }
}

// Now functions can use ? operator automatically:
fn do_stuff() -> Result<(), AppError> {
    let data = connect_to_server()?;  // NetworkError automatically becomes AppError!
    Ok(())
}

Traits Turbofish

Turbofish в типажах

Пример типажа

Обозначим типаж “летать” для супергероя и ведьмы:

// Anyone with the "Flying" badge must have a method called fly
trait Flying {
    fn fly(&self); // Takes a reference to itself, returns nothing
}

// Trait implementation
// Our hero
struct SuperHero {
    name: String,
}

// Our enemy
struct WickedWitch {
    name: String,
}

// We give both the Flying badge, but they implement it differently
impl Flying for SuperHero {
    fn fly(&self) {
        println!("{} soars through the sky majestically!", self.name);
    }
}

impl Flying for WickedWitch {
    fn fly(&self) {
        println!("{} cackles loudly as her broomstick rattles into the air!", self.name);
    }
}

// ONE function, that works on ANYTHING with the Flying badge:

// This function doesn't care if it's a hero or a witch.
// It just says: "Bring me anything with the 'Flying' badge."
fn race_to_the_castle(flyer: &impl Flying) {
    println!("The race starts!");
    flyer.fly(); // We know it can fly, because it has the badge!
    println!("We made it to the castle!");
}

fn main() {
    let hero = SuperHero { name: String::from("Captain Soar") };
    let witch = WickedWitch { name: String::from("Elvira") };

    // Both work perfectly!
    race_to_the_castle(&hero);
    race_to_the_castle(&witch);
}

Turbofish

При этом можно написать функцию race_to_the_castle двумя способами:

// короткий способ, использовался ранее:
fn race_to_the_castle(flyer: &impl Flying) {
    flyer.fly();
}

// Способ turbofish way (полный generic syntax):
fn race_to_the_castle<F: Flying>(flyer: &F) {
    flyer.fly();
}

Короткий способ подходит для простых случаев, но ломается на сложных. Примеры, когда нужно переходить на turbofish:

Несколько параметров должны быть ОДИНАКОВОГО типа

Нужны два животных для битвы, и они обязаны быть одинакового типа:

// SHORTHAND: This allows a Cat and a Dog to be shuffled around
fn shuffle_pair(a: &impl Animal, b: &impl Animal) {
    // 'a' could be a Cat, 'b' could be a Dog
}

// TURBOFISH: This FORCES both to be the same type
fn shuffle_pair<T: Animal>(a: &T, b: &T) {
    // a and b MUST be the same kind of animal
}

fn main() {
    let cat = Cat { name: String::from("Whiskers") };
    let dog = Dog { name: String::from("Rover") };
    
    // Works with shorthand (different types allowed)
    shuffle_pair(&cat, &dog); // OK
    
    // ERROR with turbofish (cat and dog are different types!)
    // shuffle_pair::<Cat>(&cat, &dog); // 💥 Dog ≠ Cat
}

Тип данных на выходе равен типу на входе

// You put in a specific animal, you get THAT same type back
fn clone_animal<A: Animal + Clone>(original: &A) -> A {
    original.clone() // Returns the same type that went in
}

fn main() {
    let cat = Cat { name: String::from("Whiskers") };
    let cloned_cat = clone_animal(&cat); // We know cloned_cat is a Cat
}

Нужно НЕСКОЛЬКО типажей одновременно

// This thing must be Flying AND have a BattleCry
fn dramatic_entrance<F: Flying + BattleCry>(fighter: &F) {
    fighter.battle_cry();
    fighter.fly();
}

Type Params

Параметризованные типажи

Подготовим пример: есть рыцарь и маг, оба могут держать оружие, но оружие у каждого своё.

// Define a weapon type
struct Sword {
    damage: u32,
}

struct Wand {
    magic_power: u32,
}

// This trait says "I can hold some type of item W"
// The <W> is a placeholder waiting to be filled
trait CanHold<W> {
    fn hold(&self, item: W);
    fn describe_held_item(&self) -> String;
}

В данном случае c параметром типаж как пустой стикер или пустой знак отличия, где поле ещё не заполнено конкретикой. Заполним конкретику:

struct Knight {
    name: String,
}

struct Wizard {
    name: String,
}

// The knight can hold a Sword specifically
impl CanHold<Sword> for Knight {
    fn hold(&self, item: Sword) {
        println!("{} grips the sword firmly! Damage: {}", self.name, item.damage);
    }
    
    fn describe_held_item(&self) -> String {
        String::from("A heavy blade")
    }
}

// The wizard can hold a Wand specifically
impl CanHold<Wand> for Wizard {
    fn hold(&self, item: Wand) {
        println!("{} twirls the wand gracefully! Power: {}", self.name, item.magic_power);
    }
    
    fn describe_held_item(&self) -> String {
        String::from("A mystical wand")
    }
}

Теперь добавим, что рыцарь может держать в руках ещё щит - и это тоже попадает в типаж, остаётся лишь +1 параметр добавить:

struct Shield {
    defense: u32,
}

// The knight can ALSO hold a Shield!
impl CanHold<Shield> for Knight {
    fn hold(&self, item: Shield) {
        println!("{} raises the shield! Defense: {}", self.name, item.defense);
    }
    
    fn describe_held_item(&self) -> String {
        String::from("A sturdy shield")
    }
}

fn main() {
    let knight = Knight { name: String::from("Arthur") };
    
    // The same knight can hold different items!
    knight.hold(Sword { damage: 50 });   // Uses CanHold<Sword>
    knight.hold(Shield { defense: 30 }); // Uses CanHold<Shield> - same trait, different weapon!
}

Без параметра типаж работает НЕ БУДЕТ: потому что нельзя тот же типаж CanHold несколько раз применять просто так. А с параметром - МОЖНО, главное, чтобы параметр W отличался. Тогда один рыцарь может иметь несколько знаков из одной семьи знаков.

Пример на базе AsRef

Рассмотрим пример работы со строками из статьи

// AsRef is defined like this (simplified):
trait AsRef<T> {
    fn as_ref(&self) -> &T;  // "Turn myself into a reference to T"
}

// String implements AsRef<str> - "I can act as a string slice reference"
impl AsRef<str> for String {
    fn as_ref(&self) -> &str {
        // String can easily give a &str view of its contents
        &self[..]
    }
}

// &str also implements AsRef<str> - "I'm already a str reference!"
impl AsRef<str> for &str {
    fn as_ref(&self) -> &str {
        self  // Just return myself, I'm already what you want
    }
}

// PathBuf (a file path type) implements AsRef<Path>
impl AsRef<Path> for PathBuf {
    fn as_ref(&self) -> &Path {
        // Give a Path view of the PathBuf
        self.as_path()
    }
}

Теперь когда мы пишем:

fn hello<N: AsRef<str>>(name: N) {
    println!("Hello, {}!", name.as_ref());
}

то это значит “приму любой тип N, если у N есть знак/стикер AsRef”, и это означает, что N может превратиться в &str.

Пример с превращениями

Представим игру, где персонажи могут превращаться в драконов или зайцев:

struct Player {
    name: String,
}

struct Dragon {
    fire_power: u32,
}

struct Rabbit {
    speed: u32,
}

// The polymorph trait: "I can transform into T"
trait Polymorph<T> {
    fn transform(&self) -> T;
}

// Player can polymorph into a Dragon
impl Polymorph<Dragon> for Player {
    fn transform(&self) -> Dragon {
        println!("{} grows scales and wings!", self.name);
        Dragon { fire_power: 100 }
    }
}

// Player can ALSO polymorph into a Rabbit
impl Polymorph<Rabbit> for Player {
    fn transform(&self) -> Rabbit {
        println!("{} shrinks and grows fluffy ears!", self.name);
        Rabbit { speed: 200 }
    }
}

fn main() {
    let player = Player { name: String::from("Alex") };
    
    // Same player, same trait name, completely different transformations!
    let dragon_form = player.transform(); // Rust infers we want Dragon here
    let rabbit_form: Rabbit = player.transform(); // Explicitly ask for Rabbit
}