Ставлення до Розумних Вказівників як до Звичайних Посилань з Трейтом 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); }
Змінна 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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f32
f64
i128
i16
i32
i64
i8
isize
and 6 others
= 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 і Блоком коду 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() {}
Ми визначаємо структуру з назвою 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);
}
Виходить ось така помилка компіляції:
$ 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); }
Запис 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() {}
Ми можемо викликати функцію 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); }
Тут ми викликаємо функцію 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)[..]); }
(*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 не може зробити припущення про те, чи перетворення немутабельного посилання на мутабельне посилання є можливим.