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

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Работу с умными указателями как с обычными ссылками с помощью Deref. Эта лабораторная работа является частью Книги по Rust. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе мы изучим, как реализация трейта Deref позволяет работать с умными указателями как с обычными ссылками, и как функция неявного приведения типов Rust позволяет работать с ссылками или умными указателями.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} rust/string_type -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} rust/function_syntax -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} rust/expressions_statements -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} rust/method_syntax -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} rust/operator_overloading -.-> lab-100432{{"Работа с умными указателями как с обычными ссылками"}} end

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

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

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

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

Следующая за указателем до значения

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

Filename: src/main.rs

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

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

Listing 15-6: Использование оператора дереференцирования для следования за ссылкой на значение i32

Переменная x хранит значение i32 равное 5 [1]. Мы присваиваем y ссылку на x [2]. Мы можем утверждать, что x равно 5 [3]. Однако, если мы хотим сделать утверждение о значении в y, мы должны использовать *y, чтобы следовать за ссылкой до значения, на которое она указывает (отсюда и дереференцирование), так что компилятор мог сравнить фактическое значение [4]. После того, как мы дереференцируем y, мы получаем доступ к целому значению, на которое указывает y, которое мы можем сравнить с 5.

Если бы мы попытались написать assert_eq!(5, y); вместо этого, мы бы получили следующую ошибку компиляции:

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}`

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

Использование Box<T>{=html} как ссылки

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

Filename: src/main.rs

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

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

Listing 15-7: Использование оператора дереференцирования для Box<i32>

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

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

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

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

Filename: src/main.rs

 1 struct MyBox<T>(T);

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

Listing 15-8: Определение типа MyBox<T>

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

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

Filename: src/main.rs

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>

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

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

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

Реализация трейта Deref

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

Filename: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
  1 type Target = T;

    fn deref(&self) -> &Self::Target {
      2 &self.0
    }
}

Listing 15-10: Реализация Deref для MyBox<T>

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

Мы заполняем тело метода deref значением &self.0, чтобы метод deref возвращал ссылку на значение, которое мы хотим получить с помощью оператора * [2]; вспомните из раздела "Использование кортежных struct без именованных полей для создания разных типов", что .0 позволяет получить доступ к первому значению в кортежном struct. Функция main в Listing 15-9, которая вызывает * для значения MyBox<T>, теперь компилируется, и утверждения проходят!

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

Когда мы ввели *y в Listing 15-9, на самом деле Rust под капотом запустил этот код:

*(y.deref())

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

Причина того, что метод deref возвращает ссылку на значение, а обычная операция дереференцирования за скобками в *(y.deref()) по-прежнему необходима, связана с системой владения. Если метод deref возвращал значение напрямую, а не ссылку на значение, то значение было бы перемещено из self. Мы не хотим получать владение внутренним значением внутри MyBox<T> в этом случае или в большинстве случаев, когда мы используем оператор дереференцирования.

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

Неявные преобразования Deref для функций и методов

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

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

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

Filename: src/main.rs

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

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

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

Filename: src/main.rs

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> в Listing 15-10, Rust может преобразовать &MyBox<String> в &String, вызвав deref. Стандартная библиотека предоставляет реализацию Deref для String, которая возвращает строковый срез, и это описано в документации API для Deref. Rust снова вызывает deref, чтобы преобразовать &String в &str, что соответствует определению функции hello.

Если Rust не реализовывал преобразование Deref, мы бы不得不 написать код из Listing 15-13 вместо кода из Listing 15-12, чтобы вызвать hello с значением типа &MyBox<String>.

Filename: src/main.rs

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

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

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

Резюме

Поздравляем! Вы завершили лабораторную работу "Работа с умными указателями как с обычными ссылками с использованием Deref". Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.