Обращение с умными указателями как с обычными ссылками
Реализация трейта Deref позволяет настраивать поведение оператора
разыменования * (не путайте его с оператором умножения или glob-оператором).
Реализовав Deref так, чтобы с умным указателем можно было обращаться как с
обычной ссылкой, вы можете писать код, который работает со ссылками, и
использовать этот код также с умными указателями.
Сначала посмотрим, как оператор разыменования работает с обычными ссылками.
Затем попробуем определить собственный тип, который ведет себя как Box<T>, и
увидим, почему оператор разыменования не работает как ссылка для нашего нового
типа. Мы изучим, как реализация трейта Deref позволяет умным указателям
работать аналогично ссылкам. Затем рассмотрим возможность Rust,
называемую 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> из стандартной библиотеки, чтобы
увидеть, как типы умных указателей по умолчанию ведут себя иначе, чем ссылки.
Затем посмотрим, как добавить возможность использовать оператор
разыменования.
Примечание: между типом
MyBox<T>, который мы сейчас создадим, и настоящимBox<T>есть одно важное различие: наша версия не будет хранить данные в куче. В этом примере мы сосредоточены наDeref, поэтому то, где именно хранятся данные, менее важно, чем поведение, похожее на указатель.
Тип 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>Вот получившаяся ошибка компиляции:
$ 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. Эта возможность Rust позволяет писать код, который работает одинаково
независимо от того, есть ли у нас обычная ссылка или тип, реализующий Deref.
Причина, по которой метод deref возвращает ссылку на значение, а обычное
разыменование за скобками в *(y.deref()) все еще необходимо, связана с
системой владения. Если бы метод deref возвращал значение напрямую, а не
ссылку на значение, значение было бы перемещено из self. В этом случае, как
и в большинстве случаев использования оператора разыменования, мы не хотим
забирать владение внутренним значением внутри MyBox<T>.
Обратите внимание, что оператор * заменяется вызовом метода deref, а затем
вызовом оператора * только один раз при каждом использовании * в коде.
Поскольку подстановка оператора * не рекурсирует бесконечно, в итоге мы
получаем данные типа i32, что соответствует 5 в assert_eq! из листинга
15-9.
Использование deref-преобразования в функциях и методах
Deref-преобразование преобразует ссылку на тип, реализующий трейт 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-преобразование, для вызова hello со
значением типа &MyBox<String> нам пришлось бы написать код из листинга 15-13
вместо кода из листинга 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)[..]);
}
(*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 не может предполагать, что преобразование неизменяемой ссылки в изменяемую возможно.