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

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

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

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

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

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

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Использование оператора разыменования для следования по ссылке к значению 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:

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Использование оператора разыменования на Box<i32>

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

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

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

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

Filename: src/main.rs
struct MyBox<T>(T);

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

fn main() {}
Listing 15-8: Определение типа MyBox<T>

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

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

Filename: 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);
}
Listing 15-9: Попытка использовать 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>.

Filename: 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);
}
Listing 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. Эта возможность 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 показывает определение функции, которая имеет параметр строкового среза.

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

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

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

Filename: 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);
}
Listing 15-12: Вызов 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>.

Filename: 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)[..]);
}
Listing 15-13: Код, который нам пришлось бы написать, если бы у Rust не было неявного преобразования через Deref

(*m) разыменовывает MyBox<String> в String. Затем & и [..] берут строковый срез из String, который равен всей строке, чтобы соответствовать сигнатуре hello. Этот код без неявных преобразований через Deref сложнее для чтения, написания и понимания из-за всех этих символов. Неявное преобразование через Deref позволяет Rust автоматически обрабатывать эти преобразования для нас.

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

Взаимодействие неявного преобразования через Deref с изменяемостью

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

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

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

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