Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Розширені трейти

Спочатку ми розглядали трейти в розділі “Визначення спільної поведінки за допомогою трейтів” у розділі 10, але ми не обговорювали більш просунуті деталі. Тепер, коли ви знаєте більше про Rust, ми можемо перейти до суті.

Визначення трейтів з асоційованими типами

Асоційовані типи пов’язують заповнювач типу з трейтом так, що визначення методів трейту можуть використовувати ці типи-заповнювачі у своїх сигнатурах. Той, хто реалізує трейт, вкаже конкретний тип, який буде використаний замість типу-заповнювача для конкретної реалізації. Таким чином, ми можемо визначити трейт, який використовує деякі типи, не потребуючи знати точно, що це за типи, доки трейт не буде реалізовано.

Ми описували більшість просунутих можливостей у цьому розділі як такі, що потрібні рідко. Асоційовані типи перебувають десь посередині: їх використовують рідше, ніж можливості, пояснені в решті книги, але частіше, ніж багато інших можливостей, обговорюваних у цьому розділі.

Одним із прикладів трейту з асоційованим типом є трейт Iterator, який надає стандартна бібліотека. Асоційований тип називається Item і позначає тип значень, по яких ітерується тип, що реалізує трейт Iterator. Визначення трейт-а Iterator показано у Лістингу 20-13.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Тип Item є заповнювачем, а визначення методу next показує, що він повертатиме значення типу Option<Self::Item>. Ті, хто реалізує трейт Iterator, вкажуть конкретний тип для Item, а метод next повертатиме Option, що містить значення цього конкретного типу.

Асоційовані типи можуть здаватися подібною концепцією до узагальнених типів, у тому сенсі, що останні дозволяють нам визначити функцію без зазначення того, які типи вона може обробляти. Щоб дослідити різницю між цими двома концепціями, ми розглянемо реалізацію трейт-а Iterator для типу на ім’я Counter, яка вказує, що тип Item — це u32:

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Цей синтаксис здається подібним до синтаксису узагальнених типів. То чому б просто не визначити трейт Iterator з узагальненими типами, як показано у Лістингу 20-14?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

Різниця полягає в тому, що під час використання узагальнених типів, як у Лістингу 20-14, ми мусимо анотувати типи в кожній реалізації; оскільки ми також можемо реалізувати Iterator<String> for Counter або будь-який інший тип, ми могли б мати кілька реалізацій Iterator для Counter. Іншими словами, коли трейт має узагальнений параметр, його можна реалізувати для типу кілька разів, змінюючи конкретні типи узагальнених параметрів типу щоразу. Коли ми використовуємо метод next на Counter, нам довелося б надавати анотації типів, щоб указати, яку саме реалізацію Iterator ми хочемо використовувати.

З асоційованими типами нам не потрібно анотувати типи, тому що ми не можемо реалізувати трейт для типу кілька разів. У Лістингу 20-13 з визначенням, що використовує асоційовані типи, ми можемо вибрати, яким буде тип Item, лише один раз, тому що може існувати лише один impl Iterator for Counter. Нам не потрібно вказувати, що ми хочемо ітератор значень u32, всюди, де ми викликаємо next на Counter.

Асоційовані типи також стають частиною контракту трейту: ті, хто реалізує трейт, мають надати тип, який замінить заповнювач асоційованого типу. Асоційовані типи часто мають назву, що описує, як буде використовуватися тип, і документування асоційованого типу в документації API — це добра практика.

Використання типових параметрів узагальнених типів і перевантаження операторів

Коли ми використовуємо параметри узагальнених типів, ми можемо вказати типовий конкретний тип для узагальненого типу. Це усуває потребу для тих, хто реалізує трейт, указувати конкретний тип, якщо типовий тип підходить. Ви вказуєте типовий тип під час оголошення узагальненого типу за допомогою синтаксису <PlaceholderType=ConcreteType>.

Чудовим прикладом ситуації, де ця техніка корисна, є перевантаження операторів, у якому ви налаштовуєте поведінку оператора (наприклад, +) у певних ситуаціях.

Rust не дозволяє вам створювати власні оператори або перевантажувати довільні оператори. Але ви можете перевантажувати операції та відповідні трейти, перелічені в std::ops, реалізувавши трейти, пов’язані з оператором. Наприклад, у Лістингу 20-15 ми перевантажуємо оператор +, щоб додавати два екземпляри Point один до одного. Ми робимо це, реалізуючи трейт Add для структури Point.

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
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,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

Метод add додає значення x двох екземплярів Point і значення y двох екземплярів Point, щоб створити новий Point. Трейт Add має асоційований тип на ім’я Output, який визначає тип, що повертається з методу add.

Типовий параметр узагальненого типу в цьому коді міститься в трейт-і Add. Ось його визначення:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Цей код загалом має вигляд, який вам уже знайомий: трейт з одним методом і асоційованим типом. Нова частина — Rhs=Self: цей синтаксис називається типовими параметрами типу. Параметр узагальненого типу Rhs (скорочено від “right-hand side”) визначає тип параметра rhs у методі add. Якщо ми не вкажемо конкретний тип для Rhs, коли реалізуємо трейт Add, тип Rhs за замовчуванням буде Self, тобто типом, для якого ми реалізуємо Add.

Коли ми реалізовували Add для Point, ми використали типовий параметр для Rhs, тому що хотіли додати два екземпляри Point. Розгляньмо приклад реалізації трейт-а Add, де ми хочемо налаштувати тип Rhs, а не використовувати типовий.

У нас є дві структури, Millimeters і Meters, які зберігають значення в різних одиницях. Це тонке обгортання наявного типу в іншу структуру відоме як патерн newtype, який ми докладніше описуємо в розділі “Реалізація зовнішніх трейтів за допомогою патерну newtype” . Ми хочемо додавати значення в міліметрах до значень у метрах і щоб реалізація Add правильно виконувала перетворення. Ми можемо реалізувати Add для Millimeters з Meters як Rhs, як показано в Лістингу 20-16.

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Щоб додати Millimeters і Meters, ми вказуємо impl Add<Meters>, щоб задати значення параметра типу Rhs замість використання типового значення Self.

Ви використовуватимете типові параметри типу двома основними способами:

  1. Щоб розширити тип, не ламаючи наявний код
  2. Щоб дозволити налаштування в конкретних випадках, які більшості користувачів не потрібні

Трейт Add зі стандартної бібліотеки — приклад другої мети: зазвичай ви додаєте два типи одного виду, але трейт Add надає можливість налаштувати поведінку ширше за це. Використання типового параметра типу у визначенні трейт-а Add означає, що вам не потрібно вказувати додатковий параметр більшу частину часу. Іншими словами, не потрібен деякий шаблонний код реалізації, що робить трейт простішим у використанні.

Перша мета подібна до другої, але навпаки: якщо ви хочете додати параметр типу до наявного трейт-а, ви можете надати йому типове значення, щоб дозволити розширення функціональності трейт-а без ламання наявного коду реалізації.

Усунення неоднозначності між однаково названими методами

Ніщо в Rust не заважає трейт-у мати метод з тією самою назвою, що й метод іншого трейт-а, і Rust також не забороняє вам реалізувати обидва трейти для одного типу. Також можливо реалізувати метод безпосередньо на типі з тією ж назвою, що й методи з трейтів.

Під час виклику методів з однаковою назвою вам потрібно буде сказати Rust, який саме ви хочете використовувати. Розгляньте код у Лістингу 20-17, де ми визначили два трейти, Pilot і Wizard, які обидва мають метод під назвою fly. Потім ми реалізуємо обидва трейти для типу Human, який уже має метод під назвою fly, реалізований безпосередньо на ньому. Кожен метод fly робить щось інше.

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}

Коли ми викликаємо fly на екземплярі Human, компілятор за замовчуванням викликає метод, який реалізовано безпосередньо на типі, як показано в Лістингу 20-18.

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}

Запуск цього коду виведе *waving arms furiously*, показуючи, що Rust викликав метод fly, реалізований безпосередньо на Human.

Щоб викликати методи fly з трейт-а Pilot або трейт-а Wizard, нам потрібно використати більш явний синтаксис, щоб указати, який саме метод fly ми маємо на увазі. Лістинг 20-19 демонструє цей синтаксис.

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Указання імені трейт-а перед іменем методу робить для Rust зрозумілим, яку саме реалізацію fly ми хочемо викликати. Ми також могли б написати Human::fly(&person), що еквівалентно person.fly(), який ми використали в Лістингу 20-19, але це трохи довше писати, якщо нам не потрібно усувати неоднозначність.

Запуск цього коду виводить таке:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Оскільки метод fly приймає параметр self, якщо б у нас було два типи, які обидва реалізують один трейт, Rust міг би визначити, яку реалізацію трейт-а використовувати, на основі типу self.

Однак асоційовані функції, які не є методами, не мають параметра self. Коли є кілька типів або трейтів, які визначають не-методні функції з однаковою назвою функції, Rust не завжди знає, який тип ви маєте на увазі, якщо тільки ви не використовуєте повністю кваліфікований синтаксис. Наприклад, у Лістингу 20-20 ми створюємо трейт для притулку для тварин, який хоче називати всіх цуценят Spot. Ми створюємо трейт Animal з асоційованою не-методною функцією baby_name. Трейт Animal реалізовано для структури Dog, для якої ми також безпосередньо надаємо асоційовану не-методну функцію baby_name.

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}

Ми реалізуємо код для називання всіх цуценят Spot в асоційованій функції baby_name, визначеній для Dog. Тип Dog також реалізує трейт Animal, який описує характеристики, спільні для всіх тварин. Цуценят собак називають puppies, і це виражено в реалізації трейт-а Animal для Dog у функції baby_name, пов’язаній з трейт-ом Animal.

У main ми викликаємо функцію Dog::baby_name, яка безпосередньо викликає асоційовану функцію, визначену для Dog. Цей код виводить таке:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

Цей вивід не той, який ми хотіли. Ми хочемо викликати функцію baby_name, яка є частиною трейт-а Animal, який ми реалізували для Dog, щоб код вивів A baby dog is called a puppy. Техніка вказання імені трейт-а, яку ми використали в Лістингу 20-19, тут не допомагає; якщо ми змінимо main на код з Лістингу 20-21, ми отримаємо помилку компіляції.

{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-21-impl-animal-for-dog/src/main.rs:here}}

Оскільки Animal::baby_name не має параметра self, і можуть існувати інші типи, які реалізують трейт Animal, Rust не може визначити, яку саме реалізацію Animal::baby_name ми хочемо. Ми отримаємо цю помилку компілятора:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error

Щоб усунути неоднозначність і сказати Rust, що ми хочемо використовувати реалізацію Animal для Dog, а не реалізацію Animal для якогось іншого типу, нам потрібно використовувати повністю кваліфікований синтаксис. Лістинг 20-22 демонструє, як використовувати повністю кваліфікований синтаксис.

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

Ми надаємо Rust анотацію типу всередині кутових дужок, яка вказує, що ми хочемо викликати метод baby_name з трейт-а Animal, реалізованого для Dog, кажучи, що ми хочемо розглядати тип Dog як Animal для цього виклику функції. Тепер цей код виведе те, що ми хочемо:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

Загалом повністю кваліфікований синтаксис визначається так:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Для асоційованих функцій, які не є методами, не було б receiver: був би лише список інших аргументів. Ви можете використовувати повністю кваліфікований синтаксис усюди, де ви викликаєте функції або методи. Однак вам дозволено опускати будь-яку частину цього синтаксису, яку Rust може визначити з іншої інформації в програмі. Вам потрібно використовувати цей більш багатослівний синтаксис лише в тих випадках, коли існує кілька реалізацій, що використовують однакову назву, і Rust потрібна допомога, щоб визначити, яку саме реалізацію ви хочете викликати.

Використання супертрейтів

Іноді ви можете написати визначення трейт-а, яке залежить від іншого трейт-а: щоб тип реалізовував перший трейт, ви хочете вимагати, щоб цей тип також реалізовував другий трейт. Ви робите це для того, щоб ваше визначення трейт-а могло використовувати асоційовані елементи другого трейт-а. Трейт, на який спирається ваше визначення трейт-а, називається супертрейт вашого трейт-а.

Наприклад, припустімо, ми хочемо створити трейт OutlinePrint з методом outline_print, який друкуватиме задане значення, відформатоване так, щоб воно було обрамлене зірочками. Тобто, якщо задано структуру Point, яка реалізує стандартний трейт Display так, що результатом є (x, y), коли ми викликаємо outline_print на екземплярі Point, який має 1 для x і 3 для y, він має вивести таке:

**********
*        *
* (1, 3) *
*        *
**********

У реалізації методу outline_print ми хочемо використовувати функціональність трейт-а Display. Отже, нам потрібно вказати, що трейт OutlinePrint працюватиме лише для типів, які також реалізують Display, і надають функціональність, потрібну OutlinePrint. Ми можемо зробити це у визначенні трейт-а, вказавши OutlinePrint: Display. Ця техніка подібна до додавання обмеження трейт-а до трейт-а. Лістинг 20-23 показує реалізацію трейт-а OutlinePrint.

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}

Оскільки ми вказали, що OutlinePrint вимагає трейт Display, ми можемо використати функцію to_string, яка автоматично реалізується для будь-якого типу, що реалізує Display. Якби ми спробували використати to_string без додавання двокрапки та без указання трейт-а Display після імені трейт-а, ми отримали б помилку, що не знайдено метод to_string для типу &Self в поточній області видимості.

Подивімося, що станеться, коли ми спробуємо реалізувати OutlinePrint для типу, який не реалізує Display, наприклад структури Point:

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

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

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Ми отримуємо помилку, що Display потрібен, але не реалізований:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
 4 |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors

Щоб виправити це, ми реалізуємо Display для Point і задовольнимо обмеження, яке вимагає OutlinePrint, ось так:

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

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

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Тоді реалізація трейт-а OutlinePrint для Point успішно скомпілюється, і ми зможемо викликати outline_print на екземплярі Point, щоб показати його в обрамленні із зірочок.

Реалізація зовнішніх трейтів за допомогою патерну newtype

У розділі “Реалізація трейт-а для типу” у розділі 10 ми згадували правило сироти, яке стверджує, що нам дозволено реалізувати трейт для типу лише якщо або трейт, або тип, або обидва, є локальними для нашого крейту. Обійти це обмеження можна за допомогою патерну newtype, який полягає у створенні нового типу в кортежній структурі. (Ми розглядали кортежні структури в розділі “Створення різних типів за допомогою кортежних структур” у розділі 5.) Кортежна структура матиме одне поле і буде тонкою обгорткою навколо типу, для якого ми хочемо реалізувати трейт. Тоді тип обгортки є локальним для нашого крейту, і ми можемо реалізувати трейт для обгортки. Newtype — це термін, що походить із мови програмування Haskell. Використання цього патерну не створює штрафу для продуктивності під час виконання, а тип обгортки усувається під час компіляції.

Як приклад, припустімо, ми хочемо реалізувати Display для Vec<T>, що правило сироти не дозволяє нам робити безпосередньо, тому що трейт Display і тип Vec<T> визначені поза нашим крейтом. Ми можемо створити структуру Wrapper, яка зберігає екземпляр Vec<T>; тоді ми можемо реалізувати Display для Wrapper і використовувати значення Vec<T>, як показано в Лістингу 20-24.

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}

Реалізація Display використовує self.0, щоб отримати доступ до внутрішнього Vec<T>, тому що Wrapper є кортежною структурою, а Vec<T> є елементом з індексом 0 у кортежі. Потім ми можемо використовувати функціональність трейт-а Display для Wrapper.

Недолік використання цієї техніки полягає в тому, що Wrapper — це новий тип, тому він не має методів значення, яке він зберігає. Нам довелося б реалізувати всі методи Vec<T> безпосередньо для Wrapper так, щоб методи делегували self.0, що дозволило б нам поводитися з Wrapper точно як з Vec<T>. Якби ми хотіли, щоб новий тип мав кожен метод, який має внутрішній тип, реалізація трейт-а Deref для Wrapper, щоб повертати внутрішній тип, була б розв’язком (ми обговорювали реалізацію трейт-а Deref у розділі “Поводження з розумними вказівниками як зі звичайними посиланнями” у розділі 15). Якби ми не хотіли, щоб тип Wrapper мав усі методи внутрішнього типу — наприклад, щоб обмежити поведінку типу Wrapper, — нам довелося б реалізувати вручну лише ті методи, які ми хочемо.

Цей патерн newtype також корисний навіть тоді, коли трейти не залучені. Тепер перемкнімо увагу й розгляньмо деякі просунуті способи взаємодії з системою типів Rust.