Обращение с умными указателями как с обычными ссылками с помощью Deref типажа

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

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

Примечание: есть одна большая разница между типом MyBox<T>, который мы собираемся создать и реальным Box<T>: наша версия не будет хранить свои данные в куче. В примере мы сосредоточимся на типаже Deref, поэтому менее важно то, где данные хранятся, чем поведение подобное указателю.

Следование по указателю к значению с помощью оператора разыменования

Обычная ссылка является типом указателя и один из способов думать про указатель, как будто это стрела в направлении к значению, хранящемуся где-то ещё. В листинге 15-6 мы создаём ссылку на значение i32 и затем используем оператор разыменования, чтобы следовать по ссылке к данным:

Файл: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Листинг 15-6: Использование оператора разыменования для следования по ссылке к значению i32

Переменная x содержит тип i32 со значением 5. Мы устанавливаем y равным ссылке на x. Мы можем утверждать, что x равно 5. Тем не менее, если мы хотим сделать утверждение о значении y то, мы должны использовать оператор *y, чтобы проследовать по ссылке к значению, на которое она указывает (следовательно, разыменовывает (dereference)). Как только мы разыменовываем 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 a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example`

To learn more, run the command again with --verbose.

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

Использование Box<T> как ссылку

Можно переписать код в листинге 15-6, чтобы использовать Box<T> вместо ссылки; оператор разыменования будет работать, как показано в листинге 15-7:

Файл: src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Листинг 15-7: Использование оператора разыменования с типом Box

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

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

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

Тип Box<T> в конечном итоге определяется как структура кортежа с одним элементом, поэтому в листинге 15-8 аналогичным образом определяется MyBox<T>. Мы также определим функцию new, чтобы она соответствовала функции new, определённой в Box<T>.

Файл: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}

Листинг 15-8: Определение типа MyBox

Мы определяем структуру с именем MyBox и объявляем обобщённый параметр T, потому что мы хотим, чтобы наш тип хранил значения любого типа. Тип MyBox является структурой кортежа с одним элементом типа T. Функция MyBox::new принимает один параметр типа T и возвращает экземпляр MyBox, который содержит переданное значение.

Давайте попробуем добавить функцию main из листинга 15-7 в листинг 15-8 и изменим её на использование типа MyBox<T>, который мы определили вместо Box<T>. Код в листинге 15-9 не будет компилироваться, потому что Rust не знает, как разыменовывать MyBox.

Файл: src/main.rs

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

Листинг 15-9: Попытка использовать MyBox таким же образом, как мы использовали ссылки и библиотечный Box

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

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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example`

To learn more, run the command again with --verbose.

Наш тип MyBox<T> не может быть разыменован, потому что мы не реализовали эту возможность. Чтобы включить разыменование с помощью оператора *, мы реализуем типаж Deref.

Трактование типа как ссылки реализуя типаж Deref

Как обсуждалось в главе 10, для реализации типажа необходимо предоставить реализации требуемых методов типажа. Типаж Deref, предоставляемый стандартной библиотекой, требует от нас реализации одного метода с именем deref, который заимствует self и возвращает ссылку на внутренние данные. Листинг 15-10 содержит реализацию Deref, который мы добавляем в определение MyBox:

Файл: src/main.rs

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

Листинг 15-10: Реализация Deref для типа MyBox

Синтаксис type Target = T; определяет связанный тип для использования у типажа Deref. Связанные типы - это немного другой способ объявления обобщённого параметра, но пока вам не нужно о них беспокоиться; мы рассмотрим их более подробно в главе 19.

В тело метода deref мы размещаем &self.0, так что метод deref возвращает ссылку на значение, к которому мы хотим получить доступ с помощью оператора *. Функция 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 coercion) - это удобство, которое Rust выполняет над аргументами функций и методов. Разыменованное приведение преобразует ссылку на исходный тип, реализующий типаж Deref, в ссылку на целевой тип, в который Deref может преобразовать исходный тип. Разыменованное приведение происходит автоматически, когда мы передаём ссылку на значение определённого типа в качестве аргумента функции или метода, который не соответствует типу параметра в определении функции или метода. Последовательность вызовов метода deref преобразует предоставленный исходный тип, в целевой тип необходимый параметру.

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

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

Файл: src/main.rs

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

fn main() {}

Листинг 15-11: Функция hello имеющая параметр name типа &str

Можно вызвать функцию hello со срезом строки в качестве аргумента, например hello("Rust");. Разыменованное приведение делает возможным вызов hello со ссылкой на значение типа MyBox<String>, как показано в листинге 15-12.

Файл: src/main.rs

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

Листинг 15-12: Вызов hello со ссылкой на значение MyBox, которое работает из-за разыменованного приведения

Здесь мы вызываем функцию 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>.

Файл: src/main.rs

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)[..]);
}

Листинг 15-13: Код, который мы должны были бы написать, если бы в Rust не было разыменованного приведения.

Код (*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 не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую ссылку возможно.