ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1167
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 15-2: Первая попытка определить перечисление в качестве структуры данных cons list, состоящей
из
i32
значений.
Примечание: В данном примере мы реализуем cons list, который содержит только значения i32
. Мы могли бы реализовать его с помощью generics, о которых мы говорили в главе 10, чтобы определить тип cons list, который мог бы хранить значения любого типа.
Использование типа
List для хранения списка
1, 2, 3
будет выглядеть как код в листинге 15-3:
Файл: src/main.rs
Листинг 15-3: Использование перечисления
List
для хранения списка
1, 2, 3
enum
List
{
Cons(
i32
, List),
Nil,
} use crate::List::{Cons, Nil}; fn main
() { let list = Cons(
1
, Cons(
2
, Cons(
3
, Nil)));
}
Первое значение
Cons содержит
1
и другой
List
. Это значение
List является следующим значением
Cons
, которое содержит
2
и другой
List
. Это значение
List является ещё один значением
Cons
, которое содержит
3
и значение
List
, которое наконец является
Nil
, не рекурсивным вариантом, сигнализирующим об окончании списка.
Если мы попытаемся скомпилировать код в листинге 15-3, мы получим ошибку,
показанную в листинге 15-4:
Листинг 15-4: Ошибка, которую мы получаем при попытке определить рекурсивное перечисление
Ошибка говорит о том, что этот тип "имеет бесконечный размер". Причина в том, что мы определили
List в форме, которая является рекурсивной: она непосредственно хранит другое значение своего собственного типа. В результате Rust не может определить,
сколько места ему нужно для хранения значения
List
. Давайте разберёмся, почему мы получаем эту ошибку. Сначала мы рассмотрим, как Rust решает, сколько места ему нужно для хранения значения нерекурсивного типа.
Вычисление размера нерекурсивного типа
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0072]: recursive type `List` has infinite size
-->
src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
| help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box
Вспомните перечисление
Message определённое в листинге 6-2, когда обсуждали объявление enum в главе 6:
Чтобы определить, сколько памяти выделять под значение
Message
, Rust проходит каждый из вариантов, чтобы увидеть, какой вариант требует наибольшее количество памяти. Rust видит, что для
Message::Quit не требуется места,
Message::Move хватает места для хранения двух значений i32
и т.д. Так как будет использоваться только один вариант, то наибольшее пространство, которое потребуется для значения
Message
, это пространство, которое потребуется для хранения самого большого из вариантов перечисления.
Сравните это с тем, что происходит, когда Rust пытается определить, сколько места необходимо рекурсивному типу, такому как перечисление
List в листинге 15-2.
Компилятор смотрит на вариант
Cons
, который содержит значение типа i32
и значение типа
List
. Следовательно,
Cons нужно пространство, равное размеру i32
плюс размер
List
. Чтобы выяснить, сколько памяти необходимо типу
List
, компилятор смотрит на варианты, начиная с
Cons
. Вариант
Cons содержит значение типа i32
и значение типа
List
, и этот процесс продолжается бесконечно, как показано на рисунке 15-1.
Cons i32
Cons i32
Cons i32
Cons i32
Cons i32 ∞
Рисунок 15-1: Бесконечный
List
, состоящий из нескончаемого числа вариантов
Cons
Использование Box для получения рекурсивного типа с известным размером
Поскольку Rust не может определить, сколько места нужно выделить для типов с рекурсивным определением, компилятор выдаёт ошибку с этим полезным предложением:
enum
Message
{
Quit,
Move { x: i32
, y: i32
},
Write(
String
),
ChangeColor(
i32
, i32
, i32
),
}
В данном предложении "перенаправление" означает, что вместо того, чтобы непосредственно хранить само значение, мы должны изменить структуру данных, так чтобы хранить его косвенно - хранить указатель на это значение.
Поскольку
Box
является указателем, Rust всегда знает, сколько места нужно
Box
:
размер указателя не меняется в зависимости от объёма данных, на которые он указывает. Это означает, что мы можем поместить
Box
внутрь экземпляра
Cons вместо значения
List напрямую.
Box
будет указывать на значение очередного
List
, который будет находиться в куче, а не внутри экземпляра
Cons
. Концептуально у нас все ещё есть список, созданный из списков, содержащих другие списки, но эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга.
Мы можем изменить определение перечисления
List в листинге 15-2 и использование
List в листинге 15-3 на код из листинга 15-5, который будет компилироваться:
Файл: src/main.rs
Листинг 15-5: Определение
List
, которое использует
Box
для того, чтобы иметь вычисляемый размер
Cons требуется объём i32
плюс место для хранения данных указателя box.
Nil не хранит никаких значений, поэтому ему нужно меньше места, чем
Cons
. Теперь мы знаем, что любое значение
List займёт размер i32
плюс размер данных указателя box.
Используя box, мы разорвали бесконечную рекурсивную цепочку, поэтому компилятор может определить размер, необходимый для хранения значения
List
. На рисунке 15-2
показано, как теперь выглядит
Cons help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box
Cons i32
Box usize
Рисунок 15-2:
List
, который не является бесконечно большим, потому что
Cons
хранит
Box
.
Box-ы обеспечивают только перенаправление и выделение в куче; у них нет никаких других специальных возможностей, подобных тем, которые мы увидим у других типов умных указателей. У них также нет накладных расходов на производительность, которые несут эти специальные возможности, поэтому они могут быть полезны в таких случаях,
как cons list, где перенаправление - единственная функция, которая нам нужна. В главе
17 мы также рассмотрим другие случаи использования box.
Тип
Box
является умным указателем, поскольку он реализует трейт
Deref
, который позволяет обрабатывать значения
Box
как ссылки. Когда значение
Box
выходит из области видимости, данные кучи, на которые указывает box, также очищаются благодаря реализации типажа
Drop
. Эти два трейта будут ещё более значимыми для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в оставшейся части этой главы. Давайте рассмотрим эти два типажа более подробно.
Обращение с умными указателями как с обычными
ссылками с помощью Deref типажа
Используя трейт
Deref
, вы можете изменить поведение оператора разыменования
*
(не путать с операторами умножения или глобального подключения). Реализовав
Deref таким образом, что умный указатель может рассматриваться как обычная ссылка, вы можете писать код, оперирующий ссылками, а также использовать этот код с умными указателями.
Давайте сначала посмотрим, как работает оператор разыменования с обычными ссылками. Затем мы попытаемся определить пользовательский тип, который ведёт себя как
Box
и посмотрим, почему оператор разыменования не работает как ссылка для нового объявленного типа. Мы рассмотрим, как реализация типажа
Deref делает возможным работу умных указателей аналогично ссылкам. Затем посмотрим на
разыменованное приведение (deref coercion) в Rust и как оно позволяет работать с любыми ссылками или умными указателями.
Примечание: есть одна большая разница между типом
MyBox
, который мы собираемся создать и реальным
Box
: наша версия не будет хранить свои данные в куче. В примере мы сосредоточимся на типаже
Deref
, поэтому менее важно то, где данные хранятся, чем поведение подобное указателю.
Следуя за указателем на значение
Обычная ссылка - это разновидность указателя, а указатель можно рассматривать как своеобразную стрелочку направляющую к значению, хранящемуся в другом месте. В
листинге 15-6 мы создаём ссылку на значение i32
, а затем используем оператор разыменования для перехода от ссылки к значению:
Файл: src/main.rs
Листинг 15-6: Использование оператора разыменования для следования по ссылке к значению
i32
Переменной x
присвоено значение
5
типа i32
. Мы установили в качестве значения y
ссылку на x
. Мы можем утверждать, что значение x
равно
5
. Однако, если мы хотим сделать утверждение о значении в y
, мы должны использовать
*y
, чтобы перейти по fn main
() { let x =
5
; let y = &x; assert_eq!
(
5
, x); assert_eq!
(
5
, *y);
}
Листинг 15-7: Использование оператора разыменования с типом
Box
Разница между листингом 15-7 и листингом 15-6 состоит в том, что здесь мы устанавливаем y
на экземпляр box, указывающий на значение x
, а не ссылкой,
указывающей на значение x
. В последнем утверждении мы можем использовать оператор разыменования, чтобы проследовать за указателем box-а так же, как мы это делали когда y
была ссылкой. Далее мы рассмотрим, что особенного у типа
Box
, что позволяет нам использовать оператор разыменования, определяя наш собственный тип
Box
Определение собственного умного указателя
Давайте создадим умный указатель, похожий на тип
Box
предоставляемый стандартной библиотекой, чтобы понять как поведение умных указателей отличается от поведения обычной ссылки. Затем мы рассмотрим вопрос, как добавить возможность использовать оператор разыменования.
Тип
Box
в конечном итоге определяется как структура кортежа с одним элементом,
поэтому в листинге 15-8 аналогичным образом определяется
MyBox
. Мы также определим функцию new
, чтобы она соответствовала функции new
, определённой в
Box
Файл: src/main.rs
Листинг 15-8: Определение типа
MyBox
Мы определяем структуру с именем
MyBox и объявляем обобщённый параметр
T
,
потому что мы хотим, чтобы наш тип хранил значения любого типа. Тип
MyBox является структурой кортежа с одним элементом типа
T
. Функция
MyBox::new принимает один параметр типа
T
и возвращает экземпляр
MyBox
, который содержит переданное значение.
Давайте попробуем добавить функцию main из листинга 15-7 в листинг 15-8 и изменим её на использование типа
MyBox
, который мы определили вместо
Box
. Код в листинге 15-9 не будет компилироваться, потому что Rust не знает, как разыменовывать
MyBox
Файл: src/main.rs struct
MyBox
(T); impl
MyBox { fn new
(x: T) -> MyBox {
MyBox(x)
}
}
Листинг 15-9. Попытка использовать
MyBox
таким же образом, как мы использовали ссылки и
Box
Вот результат ошибки компиляции:
Наш тип
MyBox
не может быть разыменован, потому что мы не реализовали эту возможность. Чтобы включить разыменование с помощью оператора
*
, мы реализуем типаж
Deref
Трактование типа как ссылки реализуя типаж Deref
Как обсуждалось в разделе
“Реализация трейта для типа”
Главы 10, для реализации типажа нужно предоставить реализации требуемых методов типажа. Типаж
Deref
,
предоставляемый стандартной библиотекой требует от нас реализации одного метода с именем deref
, который заимствует self и возвращает ссылку на внутренние данные.
Листинг 15-10 содержит реализацию
Deref добавленную к определению
MyBox
:
Файл: src/main.rs
Листинг 15-10: Реализация
Deref
для типа
MyBox
fn main
() { let x =
5
; let y = MyBox::new(x); assert_eq!
(
5
, x); assert_eq!
(
5
, *y);
}
$
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` due to previous error use std::ops::Deref; impl
Deref for
MyBox { type
Target
= T; fn deref
(&
self
) -> &Self::Target {
&
self
0
}
}
Синтаксис type Target = T;
определяет связанный тип для использования у типажа
Deref
. Связанные типы - это немного другой способ объявления обобщённого параметра, но пока вам не нужно о них беспокоиться; мы рассмотрим их более подробно в главе 19.
Мы заполним тело метода deref оператором
&self.0
, чтобы deref вернул ссылку на значение, к которому мы хотим получить доступ с помощью оператора
*
; вспомним из раздела "Using Tuple Structs without Named Fields to Create Different Types"
главы 5, что
.0
получает доступ к первому значению в кортежной структуре. Функция main в листинге
15-9, которая вызывает
*
для значения
MyBox
, теперь компилируется, и проверки проходят!
Без типажа
Deref компилятор может только разыменовывать
&
ссылки. Метод deref даёт компилятору возможность принимать значение любого типа, реализующего
Deref и вызывать метод deref чтобы получить ссылку
&
, которую он знает, как разыменовывать.
Когда мы ввели
*y в листинге 15-9, Rust фактически выполнил за кулисами такой код:
Rust заменяет оператор
*
вызовом метода deref и затем простое разыменование,
поэтому нам не нужно думать о том, нужно ли нам вызывать метод deref
. Эта функция
Rust позволяет писать код, который функционирует одинаково, независимо от того, есть ли у нас обычная ссылка или тип, реализующий типаж
Deref
Причина, по которой метод deref возвращает ссылку на значение, и что простое разыменование вне круглых скобок в
*(y.deref())
все ещё необходимо, связана с системой владения. Если бы метод deref возвращал значение напрямую, а не ссылку на него, значение переместилось бы из self
. Мы не хотим передавать владение внутренним значением внутри
MyBox
в этом случае и в большинстве случаев, когда мы используем оператор разыменования.
Обратите внимание, что оператор
*
заменён вызовом метода deref
, а затем вызовом оператора
*
только один раз, каждый раз, когда мы используем
*
в коде. Поскольку замена оператора
*
не повторяется бесконечно, мы получаем данные типа i32
,
которые соответствуют
5
в assert_eq!
листинга 15-9.
Неявные разыменованные приведения с функциями и методами
Разыменованное приведение преобразует ссылку на тип, который реализует признак
Deref
, в ссылку на другой тип. Например, deref coercion может преобразовать
&String в
&str
, потому что
String реализует признак
Deref
, который возвращает
&str
. Deref coercion - это удобный механизм, который Rust использует для аргументов функций и
*(y.deref())
из
i32
значений.
Примечание: В данном примере мы реализуем cons list, который содержит только значения i32
. Мы могли бы реализовать его с помощью generics, о которых мы говорили в главе 10, чтобы определить тип cons list, который мог бы хранить значения любого типа.
Использование типа
List для хранения списка
1, 2, 3
будет выглядеть как код в листинге 15-3:
Файл: src/main.rs
Листинг 15-3: Использование перечисления
List
для хранения списка
1, 2, 3
enum
List
{
Cons(
i32
, List),
Nil,
} use crate::List::{Cons, Nil}; fn main
() { let list = Cons(
1
, Cons(
2
, Cons(
3
, Nil)));
}
Первое значение
Cons содержит
1
и другой
List
. Это значение
List является следующим значением
Cons
, которое содержит
2
и другой
List
. Это значение
List является ещё один значением
Cons
, которое содержит
3
и значение
List
, которое наконец является
Nil
, не рекурсивным вариантом, сигнализирующим об окончании списка.
Если мы попытаемся скомпилировать код в листинге 15-3, мы получим ошибку,
показанную в листинге 15-4:
Листинг 15-4: Ошибка, которую мы получаем при попытке определить рекурсивное перечисление
Ошибка говорит о том, что этот тип "имеет бесконечный размер". Причина в том, что мы определили
List в форме, которая является рекурсивной: она непосредственно хранит другое значение своего собственного типа. В результате Rust не может определить,
сколько места ему нужно для хранения значения
List
. Давайте разберёмся, почему мы получаем эту ошибку. Сначала мы рассмотрим, как Rust решает, сколько места ему нужно для хранения значения нерекурсивного типа.
Вычисление размера нерекурсивного типа
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0072]: recursive type `List` has infinite size
-->
src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
| help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box
- ),
| ++++ + error[E0391]: cycle detected when computing drop-check constraints for `List`
-->
src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing drop-check constraints for
`List` again
= note: cycle used when computing dropck types for `Canonical { max_universe:
U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, constness: NotConst }, value: List } }`
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`. error: could not compile `cons-list` due to 2 previous errors
Вспомните перечисление
Message определённое в листинге 6-2, когда обсуждали объявление enum в главе 6:
Чтобы определить, сколько памяти выделять под значение
Message
, Rust проходит каждый из вариантов, чтобы увидеть, какой вариант требует наибольшее количество памяти. Rust видит, что для
Message::Quit не требуется места,
Message::Move хватает места для хранения двух значений i32
и т.д. Так как будет использоваться только один вариант, то наибольшее пространство, которое потребуется для значения
Message
, это пространство, которое потребуется для хранения самого большого из вариантов перечисления.
Сравните это с тем, что происходит, когда Rust пытается определить, сколько места необходимо рекурсивному типу, такому как перечисление
List в листинге 15-2.
Компилятор смотрит на вариант
Cons
, который содержит значение типа i32
и значение типа
List
. Следовательно,
Cons нужно пространство, равное размеру i32
плюс размер
List
. Чтобы выяснить, сколько памяти необходимо типу
List
, компилятор смотрит на варианты, начиная с
Cons
. Вариант
Cons содержит значение типа i32
и значение типа
List
, и этот процесс продолжается бесконечно, как показано на рисунке 15-1.
Cons i32
Cons i32
Cons i32
Cons i32
Cons i32 ∞
Рисунок 15-1: Бесконечный
List
, состоящий из нескончаемого числа вариантов
Cons
Использование Box
Поскольку Rust не может определить, сколько места нужно выделить для типов с рекурсивным определением, компилятор выдаёт ошибку с этим полезным предложением:
enum
Message
{
Quit,
Move { x: i32
, y: i32
},
Write(
String
),
ChangeColor(
i32
, i32
, i32
),
}
В данном предложении "перенаправление" означает, что вместо того, чтобы непосредственно хранить само значение, мы должны изменить структуру данных, так чтобы хранить его косвенно - хранить указатель на это значение.
Поскольку
Box
является указателем, Rust всегда знает, сколько места нужно
Box
:
размер указателя не меняется в зависимости от объёма данных, на которые он указывает. Это означает, что мы можем поместить
Box
внутрь экземпляра
Cons вместо значения
List напрямую.
Box
будет указывать на значение очередного
List
, который будет находиться в куче, а не внутри экземпляра
Cons
. Концептуально у нас все ещё есть список, созданный из списков, содержащих другие списки, но эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга.
Мы можем изменить определение перечисления
List в листинге 15-2 и использование
List в листинге 15-3 на код из листинга 15-5, который будет компилироваться:
Файл: src/main.rs
Листинг 15-5: Определение
List
, которое использует
Box
для того, чтобы иметь вычисляемый размер
Cons требуется объём i32
плюс место для хранения данных указателя box.
Nil не хранит никаких значений, поэтому ему нужно меньше места, чем
Cons
. Теперь мы знаем, что любое значение
List займёт размер i32
плюс размер данных указателя box.
Используя box, мы разорвали бесконечную рекурсивную цепочку, поэтому компилятор может определить размер, необходимый для хранения значения
List
. На рисунке 15-2
показано, как теперь выглядит
Cons help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box
- ),
| ^^^^ ^ enum
List
{
Cons(
i32
,
Box
- ),
Nil,
} use crate::List::{Cons, Nil}; fn main
() { let list = Cons(
1
,
Box
::new(Cons(
2
,
Box
::new(Cons(
3
,
Box
::new(Nil))))));
}
Cons i32
Box usize
Рисунок 15-2:
List
, который не является бесконечно большим, потому что
Cons
хранит
Box
.
Box-ы обеспечивают только перенаправление и выделение в куче; у них нет никаких других специальных возможностей, подобных тем, которые мы увидим у других типов умных указателей. У них также нет накладных расходов на производительность, которые несут эти специальные возможности, поэтому они могут быть полезны в таких случаях,
как cons list, где перенаправление - единственная функция, которая нам нужна. В главе
17 мы также рассмотрим другие случаи использования box.
Тип
Box
является умным указателем, поскольку он реализует трейт
Deref
, который позволяет обрабатывать значения
Box
как ссылки. Когда значение
Box
выходит из области видимости, данные кучи, на которые указывает box, также очищаются благодаря реализации типажа
Drop
. Эти два трейта будут ещё более значимыми для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в оставшейся части этой главы. Давайте рассмотрим эти два типажа более подробно.
Обращение с умными указателями как с обычными
ссылками с помощью Deref типажа
Используя трейт
Deref
, вы можете изменить поведение оператора разыменования
*
(не путать с операторами умножения или глобального подключения). Реализовав
Deref таким образом, что умный указатель может рассматриваться как обычная ссылка, вы можете писать код, оперирующий ссылками, а также использовать этот код с умными указателями.
Давайте сначала посмотрим, как работает оператор разыменования с обычными ссылками. Затем мы попытаемся определить пользовательский тип, который ведёт себя как
Box
и посмотрим, почему оператор разыменования не работает как ссылка для нового объявленного типа. Мы рассмотрим, как реализация типажа
Deref делает возможным работу умных указателей аналогично ссылкам. Затем посмотрим на
разыменованное приведение (deref coercion) в Rust и как оно позволяет работать с любыми ссылками или умными указателями.
Примечание: есть одна большая разница между типом
MyBox
, который мы собираемся создать и реальным
Box
: наша версия не будет хранить свои данные в куче. В примере мы сосредоточимся на типаже
Deref
, поэтому менее важно то, где данные хранятся, чем поведение подобное указателю.
Следуя за указателем на значение
Обычная ссылка - это разновидность указателя, а указатель можно рассматривать как своеобразную стрелочку направляющую к значению, хранящемуся в другом месте. В
листинге 15-6 мы создаём ссылку на значение i32
, а затем используем оператор разыменования для перехода от ссылки к значению:
Файл: src/main.rs
Листинг 15-6: Использование оператора разыменования для следования по ссылке к значению
i32
Переменной x
присвоено значение
5
типа i32
. Мы установили в качестве значения y
ссылку на x
. Мы можем утверждать, что значение x
равно
5
. Однако, если мы хотим сделать утверждение о значении в y
, мы должны использовать
*y
, чтобы перейти по fn main
() { let x =
5
; let y = &x; assert_eq!
(
5
, x); assert_eq!
(
5
, *y);
}
ссылке к значению, на которое она указывает (таким образом, происходит
разыменование), для того чтобы компилятор при сравнении мог использовать фактическое значение. Как только мы разыменуем y
, мы получим доступ к целочисленному значению, на которое указывает y
, которое и будем сравнивать с
5
Если бы мы попытались написать assert_eq!(5, y);
, то получили ошибку компиляции:
Сравнение числа и ссылки на число не допускается, потому что они различных типов.
Мы должны использовать оператор разыменования, чтобы перейти по ссылке на значение, на которое она указывает.
Использование Box как ссылку
Мы можем переписать код в листинге 15-6, чтобы использовать
Box
вместо ссылки;
оператор разыменования, используемый для
Box
в листинге 15-7, работает так же,
как оператор разыменования, используемый для ссылки в листинге 15-6:
Файл: src/main.rs
$
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`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others
= 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` due to previous error fn main
() { let x =
5
; let y =
Box
::new(x); assert_eq!
(
5
, x); assert_eq!
(
5
, *y);
}
разыменование), для того чтобы компилятор при сравнении мог использовать фактическое значение. Как только мы разыменуем y
, мы получим доступ к целочисленному значению, на которое указывает y
, которое и будем сравнивать с
5
Если бы мы попытались написать assert_eq!(5, y);
, то получили ошибку компиляции:
Сравнение числа и ссылки на число не допускается, потому что они различных типов.
Мы должны использовать оператор разыменования, чтобы перейти по ссылке на значение, на которое она указывает.
Использование Box
Мы можем переписать код в листинге 15-6, чтобы использовать
Box
вместо ссылки;
оператор разыменования, используемый для
Box
в листинге 15-7, работает так же,
как оператор разыменования, используемый для ссылки в листинге 15-6:
Файл: src/main.rs
$
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
= 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` due to previous error 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
, что позволяет нам использовать оператор разыменования, определяя наш собственный тип
Box
Определение собственного умного указателя
Давайте создадим умный указатель, похожий на тип
Box
предоставляемый стандартной библиотекой, чтобы понять как поведение умных указателей отличается от поведения обычной ссылки. Затем мы рассмотрим вопрос, как добавить возможность использовать оператор разыменования.
Тип
Box
в конечном итоге определяется как структура кортежа с одним элементом,
поэтому в листинге 15-8 аналогичным образом определяется
MyBox
. Мы также определим функцию new
, чтобы она соответствовала функции new
, определённой в
Box
Файл: src/main.rs
Листинг 15-8: Определение типа
MyBox
Мы определяем структуру с именем
MyBox и объявляем обобщённый параметр
T
,
потому что мы хотим, чтобы наш тип хранил значения любого типа. Тип
MyBox является структурой кортежа с одним элементом типа
T
. Функция
MyBox::new принимает один параметр типа
T
и возвращает экземпляр
MyBox
, который содержит переданное значение.
Давайте попробуем добавить функцию main из листинга 15-7 в листинг 15-8 и изменим её на использование типа
MyBox
, который мы определили вместо
Box
. Код в листинге 15-9 не будет компилироваться, потому что Rust не знает, как разыменовывать
MyBox
Файл: src/main.rs struct
MyBox
(x: T) -> MyBox
MyBox(x)
}
}
Листинг 15-9. Попытка использовать
MyBox
таким же образом, как мы использовали ссылки и
Box
Вот результат ошибки компиляции:
Наш тип
MyBox
не может быть разыменован, потому что мы не реализовали эту возможность. Чтобы включить разыменование с помощью оператора
*
, мы реализуем типаж
Deref
Трактование типа как ссылки реализуя типаж Deref
Как обсуждалось в разделе
“Реализация трейта для типа”
Главы 10, для реализации типажа нужно предоставить реализации требуемых методов типажа. Типаж
Deref
,
предоставляемый стандартной библиотекой требует от нас реализации одного метода с именем deref
, который заимствует self и возвращает ссылку на внутренние данные.
Листинг 15-10 содержит реализацию
Deref добавленную к определению
MyBox
:
Файл: src/main.rs
Листинг 15-10: Реализация
Deref
для типа
MyBox
fn main
() { let x =
5
; let y = MyBox::new(x); assert_eq!
(
5
, x); assert_eq!
(
5
, *y);
}
$
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` due to previous error use std::ops::Deref; impl
MyBox
Target
= T; fn deref
(&
self
) -> &Self::Target {
&
self
0
}
}
Синтаксис type Target = T;
определяет связанный тип для использования у типажа
Deref
. Связанные типы - это немного другой способ объявления обобщённого параметра, но пока вам не нужно о них беспокоиться; мы рассмотрим их более подробно в главе 19.
Мы заполним тело метода deref оператором
&self.0
, чтобы deref вернул ссылку на значение, к которому мы хотим получить доступ с помощью оператора
*
; вспомним из раздела "Using Tuple Structs without Named Fields to Create Different Types"
главы 5, что
.0
получает доступ к первому значению в кортежной структуре. Функция main в листинге
15-9, которая вызывает
*
для значения
MyBox
, теперь компилируется, и проверки проходят!
Без типажа
Deref компилятор может только разыменовывать
&
ссылки. Метод deref даёт компилятору возможность принимать значение любого типа, реализующего
Deref и вызывать метод deref чтобы получить ссылку
&
, которую он знает, как разыменовывать.
Когда мы ввели
*y в листинге 15-9, Rust фактически выполнил за кулисами такой код:
Rust заменяет оператор
*
вызовом метода deref и затем простое разыменование,
поэтому нам не нужно думать о том, нужно ли нам вызывать метод deref
. Эта функция
Rust позволяет писать код, который функционирует одинаково, независимо от того, есть ли у нас обычная ссылка или тип, реализующий типаж
Deref
Причина, по которой метод deref возвращает ссылку на значение, и что простое разыменование вне круглых скобок в
*(y.deref())
все ещё необходимо, связана с системой владения. Если бы метод deref возвращал значение напрямую, а не ссылку на него, значение переместилось бы из self
. Мы не хотим передавать владение внутренним значением внутри
MyBox
в этом случае и в большинстве случаев, когда мы используем оператор разыменования.
Обратите внимание, что оператор
*
заменён вызовом метода deref
, а затем вызовом оператора
*
только один раз, каждый раз, когда мы используем
*
в коде. Поскольку замена оператора
*
не повторяется бесконечно, мы получаем данные типа i32
,
которые соответствуют
5
в assert_eq!
листинга 15-9.
Неявные разыменованные приведения с функциями и методами
Разыменованное приведение преобразует ссылку на тип, который реализует признак
Deref
, в ссылку на другой тип. Например, deref coercion может преобразовать
&String в
&str
, потому что
String реализует признак
Deref
, который возвращает
&str
. Deref coercion - это удобный механизм, который Rust использует для аргументов функций и
*(y.deref())