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 позволяет настраивать поведение оператора разыменования * (не путайте его с оператором умножения или glob-оператором). Реализовав Deref так, чтобы с умным указателем можно было обращаться как с обычной ссылкой, вы можете писать код, который работает со ссылками, и использовать этот код также с умными указателями.

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

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

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

Имя файла: 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.

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

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

Тип 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() {}
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.

Имя файла: 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>

Вот получившаяся ошибка компиляции:

$ 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);
}
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, в ссылку на другой тип. Например, deref-преобразование может преобразовать &String в &str, потому что String реализует трейт Deref так, что он возвращает &str. Deref-преобразование – это удобство, которое Rust выполняет для аргументов функций и методов, и оно работает только с типами, реализующими трейт Deref. Оно происходит автоматически, когда мы передаем ссылку на значение определенного типа как аргумент функции или метода, но этот тип не совпадает с типом параметра в определении функции или метода. Последовательность вызовов метода deref преобразует предоставленный нами тип в тип, который нужен параметру.

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

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

Имя файла: 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.

Имя файла: 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-преобразование, для вызова hello со значением типа &MyBox<String> нам пришлось бы написать код из листинга 15-13 вместо кода из листинга 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)[..]);
}
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 не может предполагать, что преобразование неизменяемой ссылки в изменяемую возможно.