Використання розумних вказівників як звичайних посилань за допомогою трейта Deref

Реалізація трейта Deref дозволяє вам налаштувати поведінку оператора розіменування * (не плутати з оператором множення чи глобальним оператором). Релізувавши Deref таким чином, щоб розумний вказівник міг використовуватися як звичайне посилання, ви зможете писати код, що працює з посиланнями і використовувати цей код також із розумними вказівниками.

Спочатку подивімося, як оператор розіменування працює зі звичайними посиланнями. Потім ми спробуємо визначити власний тип, що поводиться як Box<T>, і побачимо, чому оператор розіменування не працює, як посилання, для нашого щойно визначеного типу. Ми дослідимо, як реалізація трейта Deref дозволяє розумним вказівникам працювати у спосіб, схожий на посилання. Тоді ми розглянемо таку особливість Rust, як приведення при розіменуванні і як вона дозволяє нам працювати як із посиланнями, так і з розумними вказівниками.

Примітка: існує суттєва різниця між типом MyBox<T>, який ми збираємося описати, і справжнім Box<T>: наша версія не зберігатиме дані в купі. Ми зосередимося у цьому прикладі на Deref, тож нам не так важливо, де насправді зберігаються дані, ніж поведінка, подібна до вказівника.

Перехід за вказівником до значення

Звичайне посилання — це тип вказівника. Вказівник можна уявити як стрілку, що вказує на значення, розміщене деінде. У Блоці коду 15-6 ми створюємо посилання на значення i32, а потім використовуємо оператор розіменування, щоб перейти за посиланням до значення:

Файл: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Блок коду 15-6: використання оператор розіменування, щоб перейти за посиланням до значення i32

Змінна x має значення 5 типу i32. Ми встановили значення у рівним посиланню на x. Ми можемо стверджувати, що x дорівнює 5. Проте, якщо ми хочемо зробити твердження про значення в y, ми повинні виконати *y, щоб перейти за посиланням до значення, на яке воно вказує (тобто розіменувати), щоб компілятор міг порівняти фактичне значення. Розіменувавши y, ми отримуємо доступ до цілого значення, на яку y вказує, яке ми можемо порівняти з 5.

Якби ми спробували написати натомість assert_eq!(5, y);, то отримали б помилку компіляції:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` due to previous error

Порівняння числа і посилання на число не дозволене, оскільки це різні типи. Ми маємо використовувати оператор розіменування, щоб перейти за посиланням до значення, на яке воно вказує.

Використання Box<T> як посилання

Ми можемо переписати код у Блоці коду 15-6, щоб використовувати Box<T> замість посилання; оператор розіменування, застосований до Box<T> у Блоці коду 15-7 працює так само як і оператор розіменування, застосований до посилання у Блоці коду 15-6:

Файл: src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Блок коду 15-7: використання оператору розіменування на Box<i32>

Основна відмінність між Блоком коду 15-7 і Блоком коду 15-6 полягає в тому, що в першому ми робимо y екземпляром Box<T>, що вказує на скопійоване значення x, а не посиланням, що вказує на значення x. В останньому твердженні ми можемо використати оператор розіменування, щоб перейти за вказівником у Box<T> так само як ми робили, коли y був посиланням. Далі ми дослідимо, що ж такого в Box<T> дає нам змогу використовувати оператор розіменування, визначивши власний тип MyBox.

Визначення власного розумного вказівника

Створімо розумний вказівник, схожий на тип Box<T>, що надається стандартною бібліотекою, щоб побачити, у чому розумні вказівники поводяться інакше, ніж вказівники за замовчанням. Тоді ми розглянемо, як додати можливість використовувати оператор розіменування.

Тип Box<T> кінець-кінцем визначається як структура-кортеж з одним елементом, тож Блок коду 15-8 визначає тип MyBox<T> у той же спосіб. Ми також визначаємо функцію new, що відповідає функції new, визначеній для Box<T>.

Файл: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}

Блок коду 15-8: визначення типу MyBox<T>

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

Спробуймо додати функцію main з Блока коду 15-7 до Блоку коду 15-8 та змінити її, щоб використовувати визначений нами тип MyBox<T> замість Box<T>. Код у Блоці коду 15-9 не компілюється, оскільки Rust не знає, як розіменувати MyBox.

Файл: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Блок коду 15-9: спроба використовувати MyBox<T> тим самим способом, яким ми використовували посилання та Box<T>

Виходить ось така помилка компіляції:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error

Наш тип MyBox<T> не можна розіменовувати, оскільки ми не реалізували цю здатність для нашого типу. Щоб дозволити розіменування за допомогою оператора *, ми реалізуємо трейт Deref.

Реалізація трейту Deref для використання типу як посилання

Як обговорено в підрозділі "Реалізація трейту для типів" Розділу 10, щоб реалізувати трейт, ми маємо реалізувати методи, необхідні цьому трейту. Трейт

Deref, наданий стандартною бібліотекою, вимагає, щоб ми реалізувати один метод, що зветься deref, який позичає self і повертає посилання на внутрішні дані. Блок коду 15-10 містить реалізацію Deref, яку треба додати до визначення MyBox:

Файл: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Блок коду 15-10: реалізація Deref для MyBox<T>

Запис type Target = T; визначає асоційований тип для використання трейтом Deref. Асоційовані типи дещо відрізняються від оголошення узагальненого параметра, але вам поки що не потрібно турбуватися про них; ми розглянемо її детальніше у Розділі 19.

В тіло методу deref ми додаємо &self.0, тож Deref повертає посилання на значення, до якого ми хочемо отримати доступ за допомогою оператора *; згадайте з підрозділу "Структури-кортежі без іменованих полів для створення нових типів" Розділу 5, що .0 є способом доступу до першого значення у структурі-кортежі. Функція main у Блоці коду 15-9, яка викликає * для значення MyBox<T> тепер компілюється, і твердження виконуються!

Без трейту Deref компілятор може розіменовувати лише посилання &. Метод deref надає компілятору можливість взяти значення будь-якого типу, який реалізує Deref, і викликати метод deref, щоб отримати посилання &, яке він вміє розіменовувати.

Коли ми ввели *y у Блоці коду 15-9, за лаштунками Rust насправді запустився цей код:

*(y.deref())

Rust замінює оператор * викликом методу deref, а потім звичайне розіменування, тож нам не треба думати, треба чи не треба викликати метод deref. Це особливість Rust дозволяє нам писати код, що працює так само, використовуємо ми звичайні посилання чи тип, що реалізує Deref.

Причина, з якої метод deref повертає посилання на значення, і що за дужками *(y.deref()) все ще потрібне звичайне розіменування, стосується системи володіння. Якби метод deref повертав значення безпосередньо замість посилання на значення, значення було б переміщене з self. Ми не хочемо у цьому випадку брати володіння внутрішнім значенням у MyBox<T>, як і в більшості випадків, де ми використовуємо оператор розіменування.

Зверніть увагу, що оператор * замінюється на виклик метод deref, а потім виклик оператора * лише один раз, щоразу, коли ми використовуємо * у нашому коді. Оскільки підставляння оператора * не виконується рекурсивно до нескінченості, ми прийдемо до даних типу i32, що відповідають 5 у assert_eq! у Блоці коду 15-9.

Неявне приведення розіменування у функціях та методах

Приведення розіменування перетворює посилання на тип, що реалізує трейт Deref, до посилання на інший тип. Наприклад, приведення розіменування може перетворити &String на &str, тому що String реалізує трейт Deref, так, що він повертає &str. Приведення розіменування - це покращення для зручності, яке Rust застосовує до аргументів функцій та методів, і працює лише з типами, що реалізують трейт Deref. Воно застосовується автоматично, коли ми передаємо посилання на значення певного типу як аргумент функції чи метода, що не відповідає типу параметра у визначенні функції чи метода. Послідовність викликів методу deref перетворює тип, наданий нами, на тип, потрібний параметру.

Приведення розіменування було додане в Rust, щоб програмістам, що пишуть виклики функцій та методів, не було потрібно додавати стільки явних посилань і розіменувань за допомогою & і *. Приведення розіменування також дозволяє легше писати код, що працює як з посиланнями, так і з розумними вказівниками.

Щоб побачити, як працює приведення розіменування, застосуймо тип MyBox<T>, який ми визначили у Блоці коду 15-8, разом із реалізацією Deref, яку ми додали в Блоці коду 15-10. Блок коду 15-11 показує визначення функції, що має параметром стрічковий слайс:

Файл: src/main.rs

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}

Блок коду 15-11: Функція hello, що має параметр name типу &str

Ми можемо викликати функцію hello аргументом - стрічковим слайсом, наприклад hello("Rust");. Приведення розіменування уможливлює виклик hello з посиланням на значення типу MyBox<String>, як показано в Блоці коду 15-12:

Файл: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Блок коду 15-12: Виклик hello з посиланням на значення MyBox<String>, яке працює завдяки приведенню розіменування

Тут ми викликаємо функцію hello з аргументом &m, який є посиланням на значення типу MyBox<String>. Оскільки ми реалізували трейт Deref для MyBox<T> у Блоці коду 15-10, Rust може перетворити &MyBox<String> на &String викликавши deref. Стандартна бібліотека надає реалізацію Deref для String, що повертає стрічковий слайс, і про це сказано в документації API для Deref. Rust викликає deref знову, щоб перетворити &String на &str, який відповідає визначенню функції hello.

Якби Rust не мав приведення розіменування, нам довелося б писати код, як у Блоці коду 15-13 замість коду з Блоку коду 15-12, щоб викликати hello для значення типу &MyBox<String>.

Файл: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Блок коду 15-13: код, який довелося б писати, якби Rust не мав приведення розіменування

(*m) розіменовує MyBox<String> у String. Потім & і [..] беруть стрічковий слайс зі String, що дорівнює всій стрічці, щоб відповідати сигнатурі hello. Цей код без приведення розіменування складніше читати, писати і розуміти з усіма цими символами. Приведення розіменування дозволяє Rust обробляти для нас такі перетворення автоматично.

Коли трейт Deref визначений для залучених типів, Rust аналізує ці типи і використовує Deref::deref стільки разів, скільки треба, щоб отримати посилання, що відповідає типу параметра. Скільки разів треба додати Deref::deref визначається під час компіляції, тож немає ніяких втрат часу виконання за переваги приведення розіменування!

Як приведення розіменування взаємодіє з мутабельністю

Подібно до того, як ви використовуєте трейт Deref, щоб перевизначити оператор * для іммутабельних посилань, ви можете скористатися трейтом DerefMut, щоб перевизначити оператор * для мутабельних посилань.

Rust виконує приведення розіменування, коли виявляє типи і реалізації трейтів у трьох випадках:

  • З &T в &U, якщо T: Deref<Target=U>
  • З &mut T в &mut U, якщо T: DerefMut<Target=U>
  • З &mut T в &U, якщо T: Deref<Target=U>

Перші два випадки однакові, окрім того, що другий реалізує мутабельність. Перший випадок застосовується, що якщо є &T, і T реалізує Deref у якийсь тип U, то ми можете прозоро отримати &U. Другий випадок застосовується що таке саме приведення розіменування виконується для мутабельних посилань.

Третій випадок хитріший: Rust також приведе мутабельне посилання до немутабельного. Але зворотне неможливе: немутабельні посилання ніколи не приводяться до мутабельних посилань. Через правила позичання, якщо ви маєте мутабельне посилання, це мутабельне посилання має бути єдиним посиланням на ці дані (інакше програма не скомпілюється). Перетворення мутабельного посилання на немутабельне ніколи не порушить правила позичання. Перетворення немутабельного посилання на мутабельне посилання вимагало б, щоб початкове немутабельне посилання було єдиним немутабельним посиланням на ці дані, але правила позичання не гарантують цього. Тому Rust не може зробити припущення про те, чи перетворення немутабельного посилання на мутабельне посилання є можливим.