Умные указатели как обычные ссылки
Реализация трейта Deref позволяет настроить поведение оператора
разыменования * (не путать с умножением и с оператором * в use). Если
реализовать Deref так, чтобы с умным указателем можно было обращаться как с
обычной ссылкой, то код, написанный для ссылок, будет работать и с умными
указателями.
Сначала посмотрим, как оператор разыменования работает с обычными ссылками.
Затем попробуем определить собственный тип, похожий на Box<T>, и увидим,
почему с ним оператор разыменования не работает так, как со ссылкой.
Разберёмся, как реализация трейта Deref позволяет умным указателям вести себя
похоже на ссылки. А потом посмотрим на неявное разыменование (deref
coercion) в Rust — и на то, как оно позволяет одинаково работать и со
ссылками, и с умными указателями.
Переход по ссылке к значению
Обычная ссылка — разновидность указателя, а указатель можно представлять как
стрелку, направленную на значение, которое хранится где-то ещё. В листинге
15-6 мы создаём ссылку на значение i32 и с помощью оператора разыменования
переходим по ссылке к значению.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
i32 с помощью оператора разыменованияПеременная x хранит значение 5 типа i32. В 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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f128
f16
f32
f64
i128
i16
i32
i64
and 8 others
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> как ссылка
Код из листинга 15-6 можно переписать, используя вместо ссылки Box<T>. В
листинге 15-7 оператор разыменования, применённый к Box<T>, работает так же,
как оператор разыменования для ссылки в листинге 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32>Главное отличие листинга 15-7 от листинга 15-6 в том, что y здесь —
экземпляр коробки, указывающей на копию значения x, а не ссылка на значение
x. В последней проверке оператором разыменования можно перейти по указателю
коробки так же, как когда y была ссылкой. Дальше мы определим собственный
тип коробки и разберёмся, что именно позволяет применять к Box<T> оператор
разыменования.
Собственный умный указатель
Сделаем тип-обёртку, похожий на Box<T> из стандартной библиотеки, и на
практике увидим, чем умные указатели по умолчанию отличаются от ссылок. А
потом посмотрим, как добавить возможность пользоваться оператором
разыменования.
Примечание: между типом MyBox<T>, который мы сейчас напишем, и настоящим
Box<T> есть одно большое различие: наша версия не хранит данные в куче.
Этот пример посвящён Deref, поэтому то, где на самом деле лежат данные,
менее важно, чем поведение, похожее на указатель.
В конечном счёте Box<T> определён как кортежная структура с одним
элементом, поэтому и MyBox<T> в листинге 15-8 определён так же. Ещё мы
определим функцию 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 — кортежная
структура с одним элементом типа T. Функция MyBox::new принимает один
параметр типа T и возвращает экземпляр MyBox с переданным значением.
Попробуем добавить в листинг 15-8 функцию main из листинга 15-7, заменив
Box<T> на наш тип MyBox<T>. Код из листинга 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);
}
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);
| ^^ 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 и возвращает ссылку на внутренние
данные. В листинге 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);
}
Deref для MyBox<T>Запись type Target = T; определяет ассоциированный тип, который использует
трейт Deref. Ассоциированные типы — немного другой способ объявить
обобщённый параметр; пока не беспокойтесь о них, подробнее мы разберём их в
главе 20.
В тело метода deref мы пишем &self.0, чтобы deref возвращал ссылку на то
значение, к которому мы хотим обращаться оператором *. Напомним из раздела
«Кортежные структуры: разные типы из похожих кортежей» главы 5, что .0 — обращение к первому значению кортежной
структуры. Теперь функция main из листинга 15-9, которая применяет * к
значению MyBox<T>, компилируется, и проверки проходят!
Без трейта Deref компилятор умеет разыменовывать только ссылки &. Метод
deref позволяет компилятору взять значение любого типа, реализующего
Deref, и вызвать у него deref, чтобы получить обычную ссылку & — а её
компилятор разыменовывать уже умеет.
Когда мы написали *y в листинге 15-9, за кулисами Rust на самом деле
выполнил такой код:
*(y.deref())
Rust заменяет оператор * вызовом метода deref и обычным разыменованием,
так что нам не нужно задумываться, вызывать deref или нет. Благодаря этому
можно писать код, который работает одинаково, будь у нас обычная ссылка или
тип, реализующий Deref.
Почему метод deref возвращает ссылку на значение и почему обычное
разыменование снаружи скобок в *(y.deref()) всё равно нужно? Дело в системе
владения. Если бы deref возвращал само значение, а не ссылку на него,
значение переместилось бы из self. А забирать владение внутренним значением
MyBox<T> ни здесь, ни в большинстве других случаев использования оператора
разыменования мы не хотим.
Учтите, что каждый * в нашем коде заменяется вызовом deref и
разыменованием * ровно один раз. Подстановка не повторяется бесконечно,
поэтому в итоге мы получаем данные типа i32, которые совпадают с 5 в
assert_eq! из листинга 15-9.
Неявное разыменование в функциях и методах
Неявное разыменование (deref coercion) превращает ссылку на тип,
реализующий трейт Deref, в ссылку на другой тип. Например, оно может
превратить &String в &str, потому что String реализует трейт Deref так,
что тот возвращает &str. Это удобство, которое Rust применяет к аргументам
функций и методов, и работает оно только с типами, реализующими Deref.
Происходит оно автоматически, когда мы передаём в функцию или метод ссылку на
значение какого-то типа, а тип параметра в определении функции или метода
другой. Цепочка вызовов deref превращает переданный тип в тот, который нужен
параметру.
Неявное разыменование добавили в Rust для того, чтобы при вызове функций и
методов не приходилось писать столько явных & и *. Заодно оно позволяет
писать больше кода, работающего и со ссылками, и с умными указателями.
Чтобы увидеть неявное разыменование в действии, возьмём тип MyBox<T> из
листинга 15-8 и реализацию Deref из листинга 15-10. В листинге 15-11 —
определение функции с параметром — строковым срезом.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
hello с параметром name типа &strФункцию hello можно вызвать со строковым срезом в качестве аргумента,
например hello("Rust");. А благодаря неявному разыменованию её можно вызвать
и со ссылкой на значение типа MyBox<String>, как в листинге 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 со ссылкой на значение MyBox<String> — это работает благодаря неявному разыменованиюЗдесь мы вызываем hello с аргументом &m — ссылкой на значение
MyBox<String>. Раз в листинге 15-10 мы реализовали трейт Deref для
MyBox<T>, Rust может превратить &MyBox<String> в &String, вызвав
deref. Стандартная библиотека реализует Deref для String так, что тот
возвращает строковый срез, — это указано в документации к API Deref. Rust
ещё раз вызывает deref и превращает &String в &str, что совпадает с
определением функции hello.
Если бы в Rust не было неявного разыменования, для вызова hello со значением
типа &MyBox<String> вместо кода из листинга 15-12 пришлось бы писать код из
листинга 15-13.
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 не может считать, что превратить неизменяемую ссылку в изменяемую возможно.