Использование умных указателей как обычных ссылок с помощью Deref
Реализация типажа Deref позволяет вам настроить поведение
оператора разыменования * (не путать с оператором умножения или glob-оператором).
Реализовав Deref таким образом, чтобы умный указатель можно было использовать
как обычную ссылку, вы сможете писать код, работающий со ссылками, и применять
этот код также к умным указателям.
Сначала рассмотрим, как оператор разыменования работает с обычными ссылками.
Затем попробуем определить собственный тип, который ведёт себя как Box<T>,
и посмотрим, почему оператор разыменования не работает с нашим новым типом
как со ссылкой. Мы изучим, как реализация типажа Deref позволяет умным
указателям работать подобно ссылкам. Затем рассмотрим возможность Rust —
неявное преобразование через Deref (deref coercion) и то, как она позволяет
работать как со ссылками, так и с умными указателями.
Примечание: Существует одно большое различие между типом
MyBox<T>, который мы собираемся создать, и реальнымBox<T>: наша версия не будет хранить свои данные в куче. В этом примере мы сосредоточены наDeref, поэтому то, где на самом деле хранятся данные, менее важно, чем поведение, подобное указателю.
Следование по ссылке к значению
Обычная ссылка — это тип указателя, и один из способов мыслить об указателе —
это стрелка, указывающая на значение, хранящееся где-то ещё. В Листинге 15-6
мы создаём ссылку на значение i32, а затем используем оператор разыменования,
чтобы пройти по ссылке к значению.
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
i32Переменная 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> как ссылки
Мы можем переписать код из Листинга 15-6, чтобы использовать Box<T> вместо
ссылки; оператор разыменования, применённый к Box<T> в Листинге 15-7,
функционирует так же, как оператор разыменования, применённый к ссылке в
Листинге 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> из стандартной библиотеки,
чтобы на практике увидеть, как умные указатели по умолчанию ведут себя иначе,
чем ссылки. Затем мы посмотрим, как добавить возможность использования оператора
разыменования.
Тип Box<T> в конечном итоге определён как кортежный структ с одним элементом,
поэтому Листинг 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 —
это кортежный структ с одним элементом типа T. Функция MyBox::new принимает
один параметр типа T и возвращает экземпляр MyBox, который хранит переданное
значение.
Попробуем добавить функцию main из Листинга 15-7 в Листинг 15-8 и изменить её
для использования типа MyBox<T>, который мы определили, вместо Box<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>Вот resultant ошибка компиляции:
$ 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` (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.
Эта возможность Rust позволяет нам писать код, который функционирует одинаково,
у нас ли обычная ссылка или тип, реализующий Deref.
Причина, по которой метод deref возвращает ссылку на значение, и почему
обычное разыменование за скобками в *(y.deref()) всё ещё необходимо, связана
с системой владения. Если бы метод deref возвращал значение напрямую, а не
ссылку на значение, значение было бы перемещено из self. Мы не хотим брать
владение внутренним значением внутри MyBox<T> в этом случае или в большинстве
случаев, когда мы используем оператор разыменования.
Обратите внимание, что оператор * заменяется вызовом метода deref, а затем
вызовом оператора * только один раз каждый раз, когда мы используем * в нашем
коде. Поскольку подстановка оператора * не рекурсирует бесконечно, мы в итоге
получаем данные типа i32, что соответствует 5 в assert_eq! в Листинге 15-9.
Неявное преобразование через Deref в функциях и методах
Неявное преобразование через Deref (deref coercion) преобразует ссылку на тип,
реализующий типаж Deref, в ссылку на другой тип. Например, неявное преобразование
через Deref может преобразовать &String в &str, потому что String реализует
типаж Deref так, что возвращает &str. Неявное преобразование через Deref —
это удобство, которое Rust выполняет для аргументов функций и методов, и оно
работает только для типов, реализующих типаж Deref. Оно происходит автоматически,
когда мы передаём ссылку на значение определённого типа в качестве аргумента
функции или метода, который не соответствует типу параметра в определении функции
или метода. Последовательность вызовов метода deref преобразует предоставленный
тип в тип, необходимый параметру.
Неявное преобразование через Deref было добавлено в Rust, чтобы программисты,
пишущие вызовы функций и методов, не нужно было добавлять так много явных ссылок
и разыменований с помощью & и *. Возможность неявного преобразования через
Deref также позволяет нам писать больше кода, который может работать как со
ссылками, так и с умными указателями.
Чтобы увидеть неявное преобразование через Deref в действии, давайте используем
тип MyBox<T>, который мы определили в Листинге 15-8, а также реализацию Deref,
которую мы добавили в Листинге 15-10. Листинг 15-11 показывает определение функции,
которая имеет параметр строкового среза.
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
hello, которая имеет параметр name типа &strМы можем вызвать функцию hello со строковым срезом в качестве аргумента,
например hello("Rust");. Неявное преобразование через Deref делает возможным
вызов hello со ссылкой на значение типа 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>, что работает благодаря неявному преобразованию через DerefЗдесь мы вызываем функцию hello с аргументом &m, который является ссылкой
на значение MyBox<String>. Поскольку мы реализовали типаж Deref для MyBox<T>
в Листинге 15-10, Rust может преобразовать &MyBox<String> в &String,
вызвав deref. Стандартная библиотека предоставляет реализацию Deref для
String, которая возвращает строковый срез, и это указано в документации API
для Deref. Rust вызывает deref ещё раз, чтобы преобразовать &String в &str,
что соответствует определению функции hello.
Если бы Rust не реализовывал неявное преобразование через Deref, нам пришлось бы
написать код из Листинга 15-13 вместо кода из Листинга 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. Этот код без неявных преобразований через Deref сложнее для
чтения, написания и понимания из-за всех этих символов. Неявное преобразование
через Deref позволяет Rust автоматически обрабатывать эти преобразования для нас.
Когда типаж Deref определён для вовлечённых типов, Rust будет анализировать
типы и использовать Deref::deref столько раз, сколько необходимо, чтобы получить
ссылку, соответствующую типу параметра. Количество раз, которое нужно вставить
Deref::deref, разрешается во время компиляции, поэтому нет накладных расходов
во время выполнения за использование неявного преобразования через Deref!
Взаимодействие неявного преобразования через Deref с изменяемостью
Подобно тому, как вы используете типаж Deref для переопределения оператора *
на неизменяемых ссылках, вы можете использовать типаж DerefMut для переопределения
оператора * на изменяемых ссылках.
Rust выполняет неявное преобразование через 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 прозрачно. Второй случай гласит,
что то же неявное преобразование через Deref происходит для изменяемых ссылок.
Третий случай сложнее: Rust также будет преобразовывать изменяемую ссылку в неизменяемую. Но обратное не возможно: неизменяемые ссылки никогда не будут преобразовываться в изменяемые. Из-за правил заимствования, если у вас есть изменяемая ссылка, эта изменяемая ссылка должна быть единственной ссылкой на эти данные (в противном случае программа не скомпилируется). Преобразование одной изменяемой ссылки в одну неизменяемую ссылку никогда не нарушит правила заимствования. Преобразование неизменяемой ссылки в изменяемую потребовало бы, чтобы исходная неизменяемая ссылка была единственной неизменяемой ссылкой на эти данные, но правила заимствования не гарантируют этого. Поэтому Rust не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую возможно.