Deref

Обращение с умными указателями как с обычными ссылками

Реализация трейта Deref позволяет вам настроить поведение оператора разыменования * (не путать с оператором умножения или glob). Реализуя Deref таким образом, что с умным указателем можно обращаться как с обычной ссылкой, вы можете писать код, который работает со ссылками, и использовать этот код также с умными указателями.

Давайте сначала посмотрим, как оператор разыменования работает с обычными ссылками. Затем мы попытаемся определить пользовательский тип, который ведёт себя как Box<T>, и посмотрим, почему оператор разыменования не работает как ссылка на нашем newly определённом типе. Мы исследуем, как реализация трейта Deref делает возможным для умных указателей работать способами, similar ссылкам. Затем мы посмотрим на функцию приведения разыменования (deref coercion) в Rust и как она позволяет нам работать как со ссылками, так и с умными указателями.

Следование по ссылке к значению

Обычная ссылка — это тип указателя, и один из способов думать об указателе — как о стрелке, указывающей на значение, хранящееся где-то ещё. В Листинге 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

Сравнение числа и ссылки на число не allowed, потому что они different типы. Мы должны использовать оператор разыменования, чтобы следовать по ссылке к значению, на которое она указывает.

Использование 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);
}

Основное различие между Листингом 15-7 и Листингом 15-6 заключается в том, что здесь мы устанавливаем y как экземпляр бокса, указывающего на скопированное значение x, а не как ссылку, указывающую на значение x. В последнем утверждении мы можем использовать оператор разыменования, чтобы следовать по указателю бокса так же, как мы делали, когда y была ссылкой. Далее мы исследуем, что особенного в Box<T>, что позволяет нам использовать оператор разыменования, определив наш собственный тип бокса.

Определение нашего собственного умного указателя

Давайте создадим тип-обёртку, similar типу Box<T>, предоставляемому стандартной библиотекой, чтобы испытать, как типы умных указателей ведут себя иначе, чем ссылки, по умолчанию. Затем мы посмотрим, как добавить возможность использовать оператор разыменования.

Примечание: Есть одно большое различие между типом MyBox<T>, который мы собираемся построить, и настоящим Box<T>: наша версия не будет хранить свои данные в куче. Мы сосредотачиваем этот пример на Deref, поэтому то, где данные фактически хранятся, менее важно, чем pointer-like поведение.

Тип 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::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);
}

Вот результирующая ошибка компиляции:

$ 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);
}

Синтаксис type Target = T; определяет ассоциированный тип для использования трейтом Deref. Ассоциированные типы — это slightly different способ объявления обобщённого параметра, но вам не нужно беспокоиться о них сейчас; мы рассмотрим их более подробно в Главе 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 возвращал значение directly вместо ссылки на значение, значение было бы перемещено из self. Мы не хотим забирать владение внутренним значением внутри MyBox<T> в этом случае или в большинстве случаев, когда мы используем оператор разыменования.

Обратите внимание, что оператор * заменяется вызовом метода deref, а затем вызовом оператора * только один раз, каждый раз, когда мы используем * в нашем коде. Поскольку замена оператора * не повторяется бесконечно, мы получаем данные типа i32, которые соответствуют 5 в assert_eq! в Листинге 15-9.

Использование приведения разыменования в функциях и методах

Приведение разыменования (Deref coercion) преобразует ссылку на тип, который реализует трейт Deref, в ссылку на другой тип. Например, приведение разыменования может преобразовать &String в &str, потому что String реализует трейт Deref таким образом, что возвращает &str. Приведение разыменования — это удобство, которое Rust выполняет для аргументов функций и методов, и оно работает только для типов, которые реализуют трейт Deref. Это происходит automatically, когда мы передаём ссылку на значение определённого типа в качестве аргумента функции или метода, который не соответствует типу параметра в определении функции или метода. Последовательность вызовов метода deref преобразует предоставленный нами тип в тип, необходимый параметру.

Приведение разыменования было добавлено в Rust, чтобы программистам, пишущим вызовы функций и методов, не нужно было добавлять столько явных ссылок и разыменований с помощью & и *. Функция приведения разыменования также позволяет нам писать больше кода, который может работать как для ссылок, так и для умных указателей.

Чтобы увидеть приведение разыменования в действии, давайте используем тип MyBox<T>, который мы определили в Листинге 15-8, а также реализацию Deref, которую мы добавили в Листинге 15-10. Листинг 15-11 показывает определение функции, которая имеет параметр среза строки.

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}

Мы можем вызвать функцию hello со срезом строки в качестве аргумента, например, hello("Rust");. Приведение разыменования делает возможным вызов 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 с аргументом &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>.

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 обрабатывать эти преобразования для нас automatically.

Когда трейт Deref определён для involved типов, Rust будет анализировать типы и использовать Deref::deref столько раз, сколько необходимо, чтобы получить ссылку, соответствующую типу параметра. Количество раз, которое Deref::deref нужно вставить, разрешается во время компиляции, поэтому нет штрафа за производительность во время выполнения за использование приведения разыменования!

Обработка приведения разыменования с изменяемыми ссылками

Подобно тому, как вы используете трейт Deref для переопределения оператора * на неизменяемых ссылках, вы можете использовать трейт DerefMut для переопределения оператора * на изменяемых ссылках.

Rust выполняет приведение разыменования, когда находит типы и реализации трейтов в трёх случаях:

  1. Из &T в &U, когда T: Deref<Target=U>
  2. Из &mut T в &mut U, когда T: DerefMut<Target=U>
  3. Из &mut T в &U, когда T: Deref<Target=U>

Первые два случая одинаковы, за исключением того, что второй реализует изменяемость. Первый случай утверждает, что если у вас есть &T, и T реализует Deref для некоторого типа U, вы можете transparently получить &U. Второй случай утверждает, что то же самое приведение разыменования происходит для изменяемых ссылок.

Третий случай более сложный: Rust также будет приводить изменяемую ссылку к неизменяемой. Но обратное не возможно: неизменяемые ссылки никогда не будут приводиться к изменяемым ссылкам. Из-за правил заимствования, если у вас есть изменяемая ссылка, эта изменяемая ссылка должна быть единственной ссылкой на эти данные (иначе программа не скомпилируется). Преобразование одной изменяемой ссылки в одну неизменяемую ссылку никогда не нарушит правила заимствования. Преобразование неизменяемой ссылки в изменяемую потребовало бы, чтобы initial неизменяемая ссылка была единственной неизменяемой ссылкой на эти данные, но правила заимствования не гарантируют этого. Следовательно, Rust не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую возможно.