Поводження з розумними вказівниками як із звичайними посиланнями
Реалізація трейту Deref дає змогу вам налаштувати поведінку оператора розіменування * (не слід плутати з оператором множення або glob-оператором). Реалізувавши Deref так, щоб розумний вказівник можна було поводити як звичайне посилання, ви можете писати код, який працює з посиланнями, і використовувати цей код також із розумними вказівниками.
Спочатку давайте подивимося, як оператор розіменування працює зі звичайними посиланнями. Потім ми спробуємо визначити власний тип, який поводиться як Box<T>, і побачимо, чому оператор розіменування не працює як посилання на нашому щойно визначеному типі. Ми дослідимо, як реалізація трейту Deref робить можливим для розумних вказівників працювати способами, подібними до посилань. Потім ми розглянемо можливість coercion deref у Rust і те, як вона дає нам змогу працювати або з посиланнями, або з розумними вказівниками.
Слідування за посиланням до значення
Звичайне посилання — це тип вказівника, і один зі способів думати про вказівник — як про стрілку до значення, що зберігається десь іще. У Listing 15-6 ми створюємо посилання на значення i32, а потім використовуємо оператор розіменування, щоб слідувати за посиланням до значення.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
Змінна x містить значення i32 5. Ми встановлюємо y рівним посиланню на 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` (bin "deref-example") due to 1 previous error
Порівнювати число і посилання на число не дозволено, тому що це різні типи. Ми маємо використати оператор розіменування, щоб слідувати за посиланням до значення, на яке воно вказує.
Використання Box<T> як посилання
Ми можемо переписати код у Listing 15-6, щоб використовувати Box<T> замість посилання; оператор розіменування, використаний на Box<T> у Listing 15-7, працює так само, як оператор розіменування, використаний на посиланні в Listing 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Головна відмінність між Listing 15-7 і Listing 15-6 полягає в тому, що тут ми встановлюємо y як екземпляр box, який вказує на скопійоване значення x, а не як посилання, що вказує на значення x. В останньому твердженні ми можемо використати оператор розіменування, щоб слідувати за вказівником box так само, як ми робили це, коли y було посиланням. Далі ми дослідимо, що особливого в Box<T>, що дає нам змогу використовувати оператор розіменування, визначивши наш власний тип box.
Визначення власного розумного вказівника
Давайте побудуємо тип-обгортку, подібний до типу Box<T>, наданого стандартною бібліотекою, щоб побачити, як типи розумних вказівників поводяться інакше, ніж посилання, за замовчуванням. Потім ми подивимося, як додати можливість використовувати оператор розіменування.
Note: There’s one big difference between the
MyBox<T>type we’re about to build and the realBox<T>: Our version will not store its data on the heap. We are focusing this example onDeref, so where the data is actually stored is less important than the pointer-like behavior.
Тип Box<T> зрештою визначений як кортежна структура з одним елементом, тож Listing 15-8 визначає тип MyBox<T> таким самим чином. Ми також визначимо функцію new, щоб відповідати функції new, визначеній для Box<T>.
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 із Listing 15-7 до Listing 15-8 і змінити її, щоб використовувати тип MyBox<T>, який ми визначили, замість Box<T>. Код у Listing 15-9 не скомпілюється, тому що Rust не знає, як розіменувати MyBox.
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);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Наш тип MyBox<T> не можна розіменувати, тому що ми не реалізували цю можливість для нашого типу. Щоб увімкнути розіменування за допомогою оператора *, ми реалізуємо трейт Deref.
Реалізація трейту Deref
Як обговорювалося в «Реалізація трейту для типу» у розділі 10, щоб реалізувати трейт, нам потрібно надати реалізації для обов’язкових методів трейту. Трейт Deref, наданий стандартною бібліотекою, вимагає від нас реалізувати один метод під назвою deref, який запозичує self і повертає посилання на внутрішні дані. Listing 15-10 містить реалізацію Deref, яку потрібно додати до визначення MyBox<T>.
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. Асоційовані типи — це дещо інший спосіб оголошення узагальненого параметра, але вам не потрібно турбуватися про них зараз; ми розглянемо їх докладніше в розділі 20.
Ми заповнюємо тіло методу deref виразом &self.0, щоб deref повертав посилання на значення, до якого ми хочемо отримати доступ за допомогою оператора *; згадайте з «Створення різних типів за допомогою кортежних структур» у розділі 5, що .0 звертається до першого значення в кортежній структурі. Функція main у Listing 15-9, яка викликає * для значення MyBox<T>, тепер компілюється, і твердження виконуються успішно!
Без трейту Deref компілятор може розіменовувати лише &-посилання. Метод deref дає компілятору можливість взяти значення будь-якого типу, який реалізує Deref, і викликати метод deref, щоб отримати посилання, яке він знає, як розіменувати.
Коли ми ввели *y у Listing 15-9, за лаштунками Rust насправді виконав такий код:
*(y.deref())
Rust замінює оператор * викликом методу deref, а потім звичайним розіменуванням, щоб нам не доводилося думати про те, чи потрібно викликати метод deref. Ця можливість Rust дає нам змогу писати код, який працює однаково, чи маємо ми звичайне посилання, чи тип, що реалізує Deref.
Причина, чому метод deref повертає посилання на значення, а звичайне розіменування поза дужками в *(y.deref()) усе ще необхідне, пов’язана з системою власності. Якби метод deref повертав значення безпосередньо, а не посилання на значення, значення було б переміщене з self. Ми не хочемо забирати власність на внутрішнє значення всередині MyBox<T> у цьому випадку або в більшості випадків, коли ми використовуємо оператор розіменування.
Зверніть увагу, що оператор * замінюється викликом методу deref, а потім викликом оператора * лише один раз, кожного разу, коли ми використовуємо * у нашому коді. Оскільки підстановка оператора * не рекурсує нескінченно, ми зрештою отримуємо дані типу i32, що відповідає 5 в assert_eq! у Listing 15-9.
Використання coercion deref у функціях і методах
Deref coercion перетворює посилання на тип, який реалізує трейт Deref, на посилання на інший тип. Наприклад, coercion deref може перетворити &String на &str, тому що String реалізує трейт Deref так, що він повертає &str. Deref coercion — це зручність, яку Rust виконує для аргументів функцій і методів, і вона працює лише для типів, що реалізують трейт Deref. Це відбувається автоматично, коли ми передаємо посилання на значення певного типу як аргумент до функції або методу, який не збігається з типом параметра у визначенні функції або методу. Послідовність викликів методу deref перетворює тип, який ми надали, на тип, який потрібен параметру.
Coercion deref було додано до Rust, щоб програмістам, які пишуть виклики функцій і методів, не потрібно було додавати стільки явних посилань і розіменувань за допомогою & і *. Можливість coercion deref також дає нам змогу писати більше коду, який може працювати або з посиланнями, або з розумними вказівниками.
Щоб побачити coercion deref в дії, давайте скористаємося типом MyBox<T>, який ми визначили в Listing 15-8, а також реалізацією Deref, яку ми додали в Listing 15-10. Listing 15-11 показує визначення функції, яка має параметр типу рядкового зрізу.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
Ми можемо викликати функцію hello з рядковим зрізом як аргументом, наприклад hello("Rust");. Coercion deref дає змогу викликати hello із посиланням на значення типу MyBox<String>, як показано в Listing 15-12.
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> у Listing 15-10, Rust може перетворити &MyBox<String> на &String, викликавши deref. Стандартна бібліотека надає реалізацію Deref для String, яка повертає рядковий зріз, і це є в документації API для Deref. Rust викликає deref ще раз, щоб перетворити &String на &str, що відповідає визначенню функції hello.
Якби Rust не реалізовував coercion deref, нам довелося б написати код у Listing 15-13 замість коду в Listing 15-12, щоб викликати hello зі значенням типу &MyBox<String>.
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. Цей код без coercion deref важче читати, писати й розуміти з усіма цими символами. Coercion deref дає змогу Rust обробляти ці перетворення за нас автоматично.
Коли трейт Deref визначено для типів, що беруть участь, Rust аналізуватиме типи та використовуватиме Deref::deref стільки разів, скільки потрібно, щоб отримати посилання, яке відповідає типу параметра. Кількість разів, яку потрібно вставити Deref::deref, визначається під час компіляції, тож за використання coercion deref немає жодних витрат під час виконання!
Обробка coercion deref зі змінними посиланнями
Подібно до того, як ви використовуєте трейт Deref, щоб перевизначити оператор * для незмінних посилань, ви можете використовувати трейт DerefMut, щоб перевизначити оператор * для змінних посилань.
Rust виконує coercion deref, коли знаходить типи та реалізації трейтів у трьох випадках:
- З
&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. Другий випадок стверджує, що те саме coercion deref відбувається і для змінних посилань.
Третій випадок складніший: Rust також перетворюватиме змінне посилання на незмінне. Але зворотне неможливе: незмінні посилання ніколи не перетворюються на змінні посилання. Через правила запозичення, якщо у вас є змінне посилання, це змінне посилання має бути єдиним посиланням на ці дані (інакше програма не скомпілюється). Перетворення одного змінного посилання на одне незмінне посилання ніколи не порушить правила запозичення. Перетворення незмінного посилання на змінне посилання вимагало б, щоб початкове незмінне посилання було єдиним незмінним посиланням на ці дані, але правила запозичення цього не гарантують. Отже, Rust не може припускати, що перетворення незмінного посилання на змінне посилання можливе.