Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Умные указатели как обычные ссылки

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

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

Переход по ссылке к значению

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

Файл: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Листинг 15-7: Оператор разыменования для 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>.

Файл: src/main.rs
struct MyBox<T>(T);

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

fn main() {}
Листинг 15-8: Определяем тип MyBox<T>

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

Попробуем добавить в листинг 15-8 функцию main из листинга 15-7, заменив Box<T> на наш тип MyBox<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<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>.

Файл: 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<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 — определение функции с параметром — строковым срезом.

Файл: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Листинг 15-11: Функция hello с параметром name типа &str

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

Файл: 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 выполняет неявное разыменование, когда находит типы и реализации трейтов в трёх случаях:

  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, можно незаметно получить &U. Второй — что такое же неявное разыменование происходит и с изменяемыми ссылками.

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