Материал: Язык программирования Rust

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

Константам, с другой стороны, разрешено дублировать свои данные с помощью компилятора при любом их использовании.
Другое отличие констант от статических переменных в том, что последние могут быть изменяемыми. Чтение и изменение статических переменных является небезопасным.
Листинг 19-10 показывает как объявлять, получать доступ и изменять изменяемую статическую переменную с именем
COUNTER
:
Имя файла: src/main.rs
Листинг 19-10: Чтение и запись изменяемой статической переменной является небезопасным
Как и с обычными переменными, мы определяем изменяемость с помощью ключевого слова mut
. Любой код, который читает из или пишет в переменную
COUNTER
должен находиться в unsafe блоке. Этот код компилируется и печатает
COUNTER: 3
, как и следовало ожидать, потому что выполняется в одном потоке. Наличие нескольких потоков с доступом к
COUNTER
приведёт к ситуации гонки данных.
Наличие изменяемых данных, которые доступны глобально, делает трудным реализацию гарантии отсутствия гонок данных, поэтому Rust считает изменяемые статические переменные небезопасными. Там, где это возможно, предпочтительно использовать техники многопоточности и умные указатели, ориентированные на многопоточное исполнение, которые мы обсуждали в главе 16. Таким образом, компилятор сможет проверить, что обращение к данным, доступным из разных потоков, выполняется безопасно.
Реализация небезопасных типажей
Ещё один случай, в котором требуется unsafe
, это реализация небезопасного типажа.
Типаж небезопасен, если хотя бы один из его методов имеет некоторый инвариант,
который компилятор не может проверить. Мы можем объявить типаж как unsafe
,
static mut
COUNTER: u32
=
0
; fn add_to_count
(inc: u32
) { unsafe
{
COUNTER += inc;
}
} fn main
() { add_to_count(
3
); unsafe
{ println!
(
"COUNTER: {}"
, COUNTER);
}
}
добавив ключевое слово unsafe перед trait
, а также пометив реализацию типажа как unsafe
, как показано в листинге 19-11.
Листинг 19-11: Объявление и реализация небезопасного типажа
Используя unsafe impl
, мы даём обещание поддерживать инварианты, которые компилятор не может проверить.
Для примера вспомним маркерные типажи
Sync и
Send
, которые мы обсуждали в разделе "Расширяемый параллелизм с помощью типажей
Sync и
Send
"
главы 16:
компилятор реализует эти типажи автоматически, если наши типы полностью состоят из типов
Send и
Sync
. Если мы создадим тип, который содержит тип, не являющийся
Send или
Sync
, такой, как сырой указатель, и мы хотим пометить этот тип как
Send или
Sync
,
мы должны использовать unsafe блок. Rust не может проверить, что наш тип поддерживает гарантии того, что он может быть безопасно передан между потоками или доступен из нескольких потоков; поэтому нам нужно добавить эти проверки вручную и указать это с помощью unsafe
Доступ к полям объединений (union)
Последнее действие, которое работает только с unsafe
, это доступ к полям объединений,
union. union похож на struct
, но только одно объявленное поле используется в конкретном экземпляре в один момент времени. Объединения в основном используются для взаимодействия с объединениями в коде C. Доступ к полям объединения небезопасен, потому что Rust не может гарантировать тип данных, хранящихся в данный момент в экземпляре объединения. Вы можете узнать больше об объединениях в справочнике
Когда использовать небезопасный код
Использование unsafe для выполнения одного из пяти действий (супер способностей),
которые только что обсуждались, не является ошибочным или не одобренным. Но получить корректный unsafe код сложнее, потому что компилятор не может помочь в обеспечении безопасности памяти. Если у вас есть причина использовать unsafe код, вы unsafe trait
Foo
{
// methods go here
} unsafe impl
Foo for i32
{
// method implementations go here
} fn main
() {}
можете делать это, а наличие явной unsafe аннотации облегчает отслеживание источника проблем, если они возникают.

Продвинутые типажи
Сначала мы рассмотрели типажи в разделе "Типажи: Определение общего поведения"
главы 10, но как и со временами жизни, мы не обсудили более сложные детали. Сейчас что вы знаете о Rust больше и мы можем двинуться дальше.
Указание заполнителей типов в определениях типажей с
ассоциированными типами
Ассоциированные типы (Associated types) связывают заполнитель типа с типажом, таким образом что объявления методов типажа могут использовать эти заполнители типов в своих сигнатурах. Реализация типажа будет указывать конкретный, используемый тип на месте заполнителя типа, при конкретной реализации. Таким образом, мы можем определить типаж пока он не реализован, который использует какие-то типы без необходимости знать, какими точно типами они будут.
Мы описали большинство расширенных возможностей в этой главе, как редко необходимые. Ассоциированные типы находятся где-то посередине: они используются реже чем возможности описанные в остальной части книги, но чаще чем многие другие возможности обсуждаемые в этой главе.
Одним из примеров типажа с ассоциированным типом является типаж
Iterator
,
который предоставляет стандартная библиотека. Ассоциированный тип называется
Item и представляет тип для значений, которые перебирает тип реализующий типаж
Iterator
. В разделе "Типаж
Iterator и метод next
"
главы 13, мы упоминали определение типажа
Iterator показанное в листинге 19-12.
Листинг 19-12: Определение типажа
Iterator
, который имеет ассоциированный тип
Item
Тип
Item является заполнителем и определение метода next показывает, что он будет возвращать значения типа
Option<:item>
. Разработчики типажа
Iterator определит конкретный тип для
Item
, а метод next вернёт
Option содержащий значение этого конкретного типа.
Ассоциированные типы могли бы показаться концепцией похожей на обобщённые типы,
в том смысле, что последние позволяют определить функцию, не указывая, какие типы она может обрабатывать. Так зачем использовать ассоциированные типы?
Давайте рассмотрим разницу между этими двумя понятиями на примере из главы 13,
которая реализует типаж
Iterator у структуры
Counter
. В листинге 13-21 мы указали,
pub trait
Iterator
{ type
Item
; fn next
(&
mut self
) ->
Option
<:item>;
}
что тип для
Item был u32
:
Файл: src/lib.rs
Этот синтаксис весьма напоминает обобщённые типы. Так почему же типаж
Iterator не определён обобщённым типом, как показано в листинге 19-13?
Листинг 19-13: Гипотетическое определение типажа
Iterator
используя обобщённые типы
Разница в том, что при использовании обобщений, как показано в листинге 19-13, мы должны аннотировать типы в каждой реализации; потому что мы также можем реализовать
Iterator for Counter или любого другого типа, мы могли бы иметь несколько реализации
Iterator для
Counter
. Другими словами, когда типаж имеет обобщённый параметр, он может быть реализован для типа несколько раз, каждый раз меняя конкретные типы параметров обобщённого типа. Когда мы используем метод next у
Counter
, нам пришлось бы предоставить аннотации типа, указывая какую реализацию
Iterator мы хотим использовать.
С ассоциированными типами не нужно аннотировать типы, потому что мы не можем реализовать типаж у типа несколько раз. В листинге 19-12 с определением,
использующим ассоциированные типы можно выбрать только один тип
Item
, потому что может быть только одно объявление impl Iterator for Counter
. Нам не нужно указывать, что нужен итератор значений типа u32
везде, где мы вызываем next у
Counter
Параметры обобщённого типа по умолчанию и перегрузка операторов
Когда мы используем параметры обобщённого типа, мы можем указать конкретный тип по умолчанию для обобщённого типа. Это устраняет необходимость разработчикам указывать конкретный тип, если работает тип по умолчанию. Синтаксис для указания типа по умолчанию в случае обобщённого типа выглядит как
Отличным примером ситуации, где этот подход полезен, является перегрузка оператора.
Перегрузка оператора (Operator overloading) реализует пользовательское поведение некоторого оператора (например,
+
) в конкретных ситуациях.
impl
Iterator for
Counter { type
Item
= u32
; fn next
(&
mut self
) ->
Option
<:item> {
// --snip-- pub trait
Iterator
{ fn next
(&
mut self
) ->
Option
;
}

Rust не позволяет создавать собственные операторы или перегружать произвольные операторы. Но можно перегрузить перечисленные операции и соответствующие им типажи из std::ops путём реализации типажей, связанных с этими операторами.
Например, в листинге 19-14 мы перегружаем оператор
+
, чтобы складывать два экземпляра
Point
. Мы делаем это реализуя типаж
Add для структуры
Point
:
Файл: src/main.rs
Листинг 19-14: Реализация типажа
Add
для перезагрузки оператора
+
у структуры
Point
Метод add складывает значения x
двух экземпляров
Point и значения y
у
Point для создания нового экземпляра
Point
. Типаж
Add имеет ассоциированный тип с именем
Output
, который определяет тип, возвращаемый из метода add
Обобщённый тип по умолчанию в этом коде находится в типаже
Add
. Вот его определение:
Этот код должен выглядеть знакомым: типаж с одним методом и ассоциированным типом. Новый синтаксис это
RHS=Self
. Такой синтаксис называется параметры типа по
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct
Point
{ x: i32
, y: i32
,
} impl
Add for
Point { type
Output
= Point; fn add
(
self
, other: Point) -> Point {
Point { x: self
.x + other.x, y: self
.y + other.y,
}
}
} fn main
() { assert_eq!
(
Point { x:
1
, y:
0
} + Point { x:
2
, y:
3
},
Point { x:
3
, y:
3
}
);
} trait
Add
Self
> { type
Output
; fn add
(
self
, rhs: Rhs) -> Self::Output;
}

умолчанию (default type parameters). Параметр обобщённого типа
RHS
(сокращённо “right hand side”) определяет тип параметра rhs в методе add
. Если мы не укажем конкретный тип для
RHS
при реализации типажа
Add
, то типом для
RHS
по умолчанию будет
Self
,
который будет типом для которого реализуется типаж
Add
Когда мы реализовали
Add для структуры
Point
, мы использовали стандартное значение для
RHS
, потому что хотели сложить два экземпляра
Point
. Давайте посмотрим на пример реализации типажа
Add
, где мы хотим пользовательский тип
RHS
вместо использования типа по умолчанию.
У нас есть две разные структуры
Millimeters и
Meters
, хранящие значения в разных единицах измерения. Мы хотим добавить значения в миллиметрах к значениям в метрах и хотим иметь реализацию типажа
Add
, которая делает правильное преобразование единиц. Можно реализовать
Add для
Millimeters с типом
Meters в качестве
Rhs
, как показано в листинге 19-15.
Файл: src/lib.rs
1   ...   49   50   51   52   53   54   55   56   ...   62
Листинг 19-15: Реализация типажа Add для структуры Millimeters, чтобы складывать Millimeters и MetersЧтобы сложить Millimeters и Meters, мы указываем impl Add
, чтобы указать значение параметра типа
RHS
(Meters) вместо использования значения по умолчанию
Self
(Millimeters).
Параметры типа по умолчанию используются в двух основных случаях:
Чтобы расширить тип без внесения изменений ломающих существующий код
Чтобы позволить пользовательское поведение в специальных случаях, которые не нужны большинству пользователей
Типаж
Add из стандартной библиотеки является примером второй цели: обычно вы складываете два одинаковых типа, но типаж
Add позволяет сделать больше.
Использование параметра типа по умолчанию в объявлении типажа
Add означает, что не нужно указывать дополнительный параметр большую часть времени. Другими словами, большая часть кода реализации не нужна, что делает использование типажа проще.
use std::ops::Add; struct
Millimeters
(
u32
); struct
Meters
(
u32
); impl
Add for
Millimeters { type
Output
= Millimeters; fn add
(
self
, other: Meters) -> Millimeters {
Millimeters(
self
0
+ (other.
0
*
1000
))
}
}

Первая цель похожа на вторую, но используется наоборот: если вы хотите добавить параметр типа к существующему типажу, можно дать ему значение по умолчанию, чтобы разрешить расширение функциональности типажа без нарушения кода существующей реализации.
Полностью квалифицированный синтаксис для устранения
неоднозначности: вызов методов с одинаковым именем
В Rust ничего не мешает типажу иметь метод с одинаковым именем, таким же как метод другого типажа и Rust не мешает реализовывать оба таких типажа у одного типа. Также возможно реализовать метод с таким же именем непосредственно у типа, такой как и методы у типажей.
При вызове методов с одинаковыми именами в Rust нужно указать, какой из трёх возможных вы хотите использовать. Рассмотрим код в листинге 19-16, где мы определили два типажа:
Pilot и
Wizard
, у обоих есть метод fly
. Затем мы реализуем оба типажа у типа
Human в котором уже реализован метод с именем fly
. Каждый метод fly делает что-то своё.
Файл: src/main.rs trait
Pilot
{ fn fly
(&
self
);
} trait
Wizard
{ fn fly
(&
self
);
} struct
Human
; impl
Pilot for
Human { fn fly
(&
self
) { println!
(
"This is your captain speaking."
);
}
} impl
Wizard for
Human { fn fly
(&
self
) { println!
(
"Up!"
);
}
} impl
Human { fn fly
(&
self
) { println!
(
"*waving arms furiously*"
);
}
}

Листинг 19-16: Два типажа определены с методом
fly
и реализованы у типа
Human
, а также метод
fly
реализован непосредственно у
Human
Когда мы вызываем fly у экземпляра
Human
, то компилятор по умолчанию вызывает метод, который непосредственно реализован для типа, как показано в листинге 19-17.
Файл: src/main.rs
Листинг 19-17: Вызов
fly
у экземпляра
Human
Запуск этого кода напечатает
*waving arms furiously*
, показывая, что Rust называется метод fly реализованный непосредственно у
Human
Чтобы вызвать методы fly у типажа
Pilot или типажа
Wizard нужно использовать более явный синтаксис, указывая какой метод fly мы имеем в виду. Листинг 19-18
демонстрирует такой синтаксис.
Файл: src/main.rs
Листинг 19-18: Указание какой метода
fly
мы хотим вызвать
Указание имени типажа перед именем метода проясняет компилятору Rust, какую именно реализацию fly мы хотим вызвать. Мы могли бы также написать
Human::fly(&person)
, что эквивалентно используемому нами person.fly()
в листинге
19-18, но это писание немного длиннее, когда нужна неоднозначность.
Выполнение этого кода выводит следующее:
Поскольку метод fly принимает параметр self
, если у нас было два типа оба реализующих один типаж, то Rust может понять, какую реализацию типажа fn main
() { let person = Human; person.fly();
} fn main
() { let person = Human;
Pilot::fly(&person);
Wizard::fly(&person); person.fly();
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
использовать в зависимости от типа self
Однако ассоциированные функции являющиеся частью типажей не имеют self параметра. Когда два типа в одной области видимости реализуют такой типаж, Rust не может выяснить, какой тип вы имеете в виду если вы не используете полностью
квалифицированный синтаксис (fully qualified). Например, типаж
Animal в листинге 19-19
имеет: ассоциированную функцию baby_name
, реализацию типажа
Animal для структуры
Dog и ассоциированную функцию baby_name
, объявленную напрямую у структуры
Dog
Файл: src/main.rs
Листинг 19-19: Типаж с ассоциированной функцией и тип с ассоциированной функцией с тем же именем,
которая тоже реализует типаж
Этот код для приюта для животных, который хочет назвать всех щенков именем Spot, что реализовано в ассоциированной функции baby_name
, которая определена для
Dog
. Тип
Dog также реализует типаж
Animal
, который описывает характеристики, которые есть у всех животных. Маленьких собак называют щенками, и это выражается в реализации
Animal у
Dog в функции baby_name ассоциированной с типажом
Animal
В main мы вызываем функцию
Dog::baby_name
, которая вызывает ассоциированную функцию определённую напрямую у
Dog
. Этот код печатает следующее:
trait
Animal
{ fn baby_name
() ->
String
;
} struct
Dog
; impl
Dog { fn baby_name
() ->
String
{
String
::from(
"Spot"
)
}
} impl
Animal for
Dog { fn baby_name
() ->
String
{
String
::from(
"puppy"
)
}
} fn main
() { println!
(
"A baby dog is called a {}"
, Dog::baby_name());
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot

Этот вывод является не тем, что мы хотели получить. Мы хотим вызвать функцию baby_name
, которая является частью типажа
Animal реализованного у
Dog
, так чтобы код печатал
A baby dog is called a puppy
. Техника указания имени типажа использованная в листинге 19-18 здесь не помогает; если мы изменим main код как в листинге 19-20, мы получим ошибку компиляции.
Файл: src/main.rs
Листинг 19-20. Попытка вызвать функцию
baby_name
из типажа
Animal
, но Rust не знает какую реализацию
использовать
Так как
Animal::baby_name является ассоциированной функцией не имеющей self параметра в сигнатуре, а не методом, то Rust не может понять, какую реализацию
Animal::baby_name мы хотим вызвать. Мы получим эту ошибку компилятора:
Чтобы устранить неоднозначность и сказать Rust, что мы хотим использовать реализацию
Animal для
Dog
, нужно использовать полный синтаксис. Листинг 19-21
демонстрирует, как использовать полный синтаксис.
Файл: src/main.rs
Листинг 19-21: Использование полностью квалифицированного синтаксиса для указания, что мы мы хотим
вызвать функцию
baby_name
у типажа
Animal
реализованную в
Dog
Мы указываем аннотацию типа в угловых скобках, которая указывает на то что мы хотим вызвать метод baby_name из типажа
Animal реализованный в
Dog
, также указывая что мы хотим рассматривать тип
Dog в качестве
Animal для вызова этой функции. Этот код теперь напечатает то, что мы хотим:
fn main
() { println!
(
"A baby dog is called a {}"
, Animal::baby_name());
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0283]: type annotations needed
-->
src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer type
|
= note: cannot satisfy `_: Animal`
For more information about this error, try `rustc --explain E0283`. error: could not compile `traits-example` due to previous error fn main
() { println!
(
"A baby dog is called a {}"
, Animal>::baby_name());
}

В общем, полностью квалифицированный синтаксис определяется следующим образом:
Для ассоциированных функций при их вызове не будет receiver
(объекта приёмника), а будет только список аргументов. Вы можете использовать полностью квалифицированный синтаксис везде, где вызываете функции или методы. Тем не менее,
разрешается опустить любую часть этого синтаксиса, которую Rust может понять из другой информации в программе. Необходимость использования этого наиболее подробного синтаксиса возникает только в тех случаях, когда есть несколько реализаций, которые используют одинаковое имя и Rust нуждается в помощи для определения, какой вариант реализации вы хотите вызвать.
Использование супер типажей для требования функциональности
одного типажа в рамках другого типажа
Иногда вам может понадобиться, чтобы один типаж использовал функциональность другого типажа. В этом случае нужно полагаться на зависимый типаж, который также реализуется. Типаж на который вы полагаетесь, является супер типажом типажа,
который реализуете вы.
Например, мы хотим создать типаж
OutlinePrint с методом outline_print
, который будет печатать значение обрамлённое звёздочками. Мы хотим чтобы структура
Point реализующая типаж
Display вывела на печать
(x, y)
при вызове outline_print у
экземпляра
Point
, который имеет значение
1
для x
и значение
3
для y
. Она должна напечатать следующее:
В реализации outline_print мы хотим использовать функциональность типажа
Display
. Поэтому нам нужно указать, что типаж
OutlinePrint будет работать только для типов, которые также реализуют
Display и предоставляют функциональность, которая нужна в
OutlinePrint
. Мы можем сделать это в объявлении типажа, указав
OutlinePrint: Display
. Этот метод похож на добавление ограничения в типаж. В
листинге 19-22 показана реализация типажа
OutlinePrint
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Trait>::function(receiver_if_method, next_arg, ...);
**********
* *
* (1, 3) *
* *
**********

Файл: src/main.rs
Листинг 19-22: Реализация типажа
OutlinePrint
которая требует функциональности типажа
Display
Поскольку мы указали, что типаж
OutlinePrint требует типажа
Display
, мы можем использовать функцию to_string
, которая автоматически реализована для любого типа реализующего
Display
. Если бы мы попытались использовать to_string не добавляя двоеточие и не указывая типаж
Display после имени типажа, мы получили бы сообщение о том, что метод с именем to_string не был найден у типа
&Self в текущей области видимости.
Давайте посмотрим что происходит, если мы пытаемся реализовать типаж
OutlinePrint для типа, который не реализует
Display
, например структура
Point
:
Файл: src/main.rs
Мы получаем сообщение о том, что требуется реализация
Display
, но её нет:
use std::fmt; trait
OutlinePrint
: fmt::Display { fn outline_print
(&
self
) { let output = self
.to_string(); let len = output.len(); println!
(
"{}"
,
"*"
.repeat(len +
4
)); println!
(
"*{}*"
,
" "
.repeat(len +
2
)); println!
(
"* {} *"
, output); println!
(
"*{}*"
,
" "
.repeat(len +
2
)); println!
(
"{}"
,
"*"
.repeat(len +
4
));
}
} struct
Point
{ x: i32
, y: i32
,
} impl
OutlinePrint for
Point {}

Чтобы исправить, мы реализуем
Display у структуры
Point и выполняем требуемое ограничение
OutlinePrint
, вот так:
Файл: src/main.rs
Тогда реализация типажа
OutlinePrint для структуры
Point будет скомпилирована успешно и мы можем вызвать outline_print у экземпляра
Point для отображения значения обрамлённое звёздочками.
Шаблон Newtype для реализация внешних типажей у внешних типов
В разделе "Реализация типажа у типа"
главы 10, мы упоминали "правило сироты" (orphan rule), которое гласит, что разрешается реализовать типаж у типа, если либо типаж, либо тип являются локальными для нашего крейта. Можно обойти это ограничение,
используя шаблон нового типа (newtype pattern), который включает в себя создание нового типа в кортежной структуре. (Мы рассмотрели кортежные структуры в разделе "Использование структур кортежей без именованных полей для создания различных типов"
главы 5.) Структура кортежа будет иметь одно поле и будет тонкой оболочкой для типа которому мы хотим реализовать типаж. Тогда тип оболочки является локальным для нашего крейта и мы можем реализовать типаж для локальной обёртки. Newtype это термин, который происходит от языка программирования Haskell. В нем нет ухудшения
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0277]: `Point` doesn't implement `std::fmt::Display`
-->
src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty- print) instead note: required by a bound in `OutlinePrint`
-->
src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`. error: could not compile `traits-example` due to previous error use std::fmt; impl fmt::Display for
Point { fn fmt
(&
self
, f: &
mut fmt::Formatter) -> fmt::
Result
{ write!
(f,
"({}, {})"
, self
.x, self
.y)
}
}
производительности времени выполнения при использовании этого шаблона и тип оболочки исключается во время компиляции.
В качестве примера, мы хотим реализовать типаж
Display для типа
Vec
, где "правило сироты" (orphan rule) не позволяет нам этого делать напрямую, потому что типаж
Display и тип
Vec
объявлены вне нашего крейта. Мы можем сделать структуру
Wrapper
, которая содержит экземпляр
Vec
; тогда мы можем реализовать
Display у структуры
Wrapper и использовать значение
Vec
как показано в листинге
19-23.
Файл: src/main.rs
Листинг 19-23. Создание типа
Wrapper
Vec
для реализации
Display
Реализация
Display использует self.0
для доступа к внутреннему
Vec
, потому что
Wrapper это структура кортежа, а
Vec
это элемент с индексом 0 в кортеже. Затем мы можем использовать функциональные возможности типа
Display у
Wrapper
Недостатком использования этой техники является то, что
Wrapper является новым типом, поэтому он не имеет методов для значения, которое он держит в себе. Мы должны были бы реализовать все методы для
Vec
непосредственно во
Wrapper
, так чтобы эти методы делегировались внутреннему self.0
, что позволило бы нам обращаться с
Wrapper точно так же, как с
Vec
. Если бы мы хотели, чтобы новый тип имел каждый метод имеющийся у внутреннего типа, реализуя типаж
Deref
(обсуждается в разделе "Работа с умными указателями как с обычными ссылками с помощью
Deref типажа"
главы 15) у
Wrapper для возвращения внутреннего типа, то это было бы решением. Если мы не хотим, чтобы тип
Wrapper имел все методы внутреннего типа,
например, для ограничения поведения типа
Wrapper
, то пришлось бы вручную реализовать только те методы, которые нам нужны.
Теперь вы знаете, как используется newtype шаблон по отношению к типажам; это также полезный шаблон, даже когда типажи не используются. Давайте переключимся и посмотрим на некоторые продвинутые способы взаимодействия с системой типов Rust.
use std::fmt; struct
Wrapper
(
Vec
<
String
>); impl fmt::Display for
Wrapper { fn fmt
(&
self
, f: &
mut fmt::Formatter) -> fmt::
Result
{ write!
(f,
"[{}]"
, self
0
.join(
", "
))
}
} fn main
() { let w = Wrapper(
vec!
[
String
::from(
"hello"
),
String
::from(
"world"
)]); println!
(
"w = {}"
, w);
}

Расширенные типы
Система типов Rust имеет некоторые возможности, которые мы упоминали в этой книге,
но ещё не обсуждали. Мы начнём с обсуждения новых типов (newtypes) в целом, по мере изучения того, почему новые типы полезны в качестве типов. Затем мы перейдём к псевдонимам, возможности похожей на новые типы (newtypes), но с немного другой семантикой. Мы также обсудим тип
!
и с динамическими типами (dynamically sized type).
Использование Newtype шаблона для безопасности типов и
реализации абстракций
Примечание. В следующем разделе предполагается, что вы прочитали предыдущий раздел "Использование шаблона Newtype для реализации внешних типажей у внешних типов"
Шаблон newtype полезен для задач помимо тех, которые мы обсуждали до сих пор,
включая статическое обеспечение того, чтобы значения никогда не путались и указывали единицы значения. Вы видели пример использования newtype для обозначения единиц в листинге 19-15. Вспомним, что структуры
Millimeters и
Meters содержат обёрнутые значения u32
в newtype. Если бы мы написали функцию с параметром типа
Millimeters
, мы не смогли бы скомпилировать программу, которая случайно пыталась вызвать эту функция со значением типа
Meters или обычным u32
Другое использование шаблона newtype - абстрагирование от некоторых деталей реализации типа: новый тип может предоставлять открытый API, отличный от API
приватного внутреннего типа, если мы напрямую использовали новый тип для ограничения доступного функционала, например.
Варианты шаблона (Newtypes) также могут скрывать внутреннюю реализацию.
Например, мы могли бы предоставить тип
People для оборачивания типа
HashMapString>
, которой хранит идентификатор человека связанного с его именем. Код использующий
People будет взаимодействовать только с предоставляемым нами открытым API, например метод добавления строки имени в коллекцию
People
; этому коду не понадобилось бы знать, что мы внутри присваиваем ID код типа i32
именам.
Шаблон newtype - это лёгкий способ добиться инкапсуляции, скрыть детали реализации,
которые мы обсуждали в разделе "Инкапсуляция, которая скрывает детали реализации"
главы 17.
Создание синонимов типа с помощью псевдонимов типа

Наряду с шаблоном newtype, Rust предоставляет возможность объявить псевдоним типа
чтобы дать существующему типу другое имя. Для этого мы используем ключевое слово type
. Например, мы можем создать псевдоним типа
Kilometers для i32
следующим образом:
Теперь псевдоним
Kilometers является синонимом для i32
; в отличие от типов
Millimeters и
Meters
, которые мы создали в листинге 19-15,
Kilometers не являются отдельными, новыми типами. Значения с типом
Kilometers будут обрабатываться так же, как значения типа i32
:
Поскольку
Kilometers и i32
являются одинаковым типом, мы можем сложить значения обоих типы и мы можем передать значения
Kilometers в функции, которые принимают параметры типа i32
. Однако, используя этот метод, мы не получаем преимуществ проверки типа, которые доступны в шаблоне newtype, обсуждавшемся ранее.
Синонимы в основном используются для уменьшения повторяемости. Например, у нас есть тип:
Запись этого длинного типа в сигнатурах функций и в виде аннотаций типов по всему коду может быть утомительной и приводить к ошибкам. Представьте, что у вас есть проект, полный кодом как в листинге 19-24.
Листинг 19-24: Использование длинного типа во многих местах
Псевдоним типа делает этот код более управляемым за счёт сокращения повторений. В
листинге 19-25 мы представили псевдоним
Thunk для "многословного" типа и теперь можем заменить все использования такого типа на более короткий псевдонимом
Thunk type
Kilometers
= i32
; type
Kilometers
= i32
; let x: i32
=
5
; let y: Kilometers =
5
; println!
(
"x + y = {}"
, x + y);
Box
<
dyn
Fn
() +
Send
+
'static
> let f:
Box
<
dyn
Fn
() +
Send
+
'static
> =
Box
::new(|| println!
(
"hi"
)); fn takes_long_type
(f:
Box
<
dyn
Fn
() +
Send
+
'static
>) {
// --snip--
} fn returns_long_type
() ->
Box
<
dyn
Fn
() +
Send
+
'static
> {
// --snip--
}

Листинг 19-25: Представление псевдонима
Thunk
для уменьшения количества повторений
Этот код намного легче читать и писать! Выбор значимого имени для псевдоним типа может также помочь сообщить о ваших намерениях (thunk является словом для кода,
который будет вычисляться позднее, так что это подходящее название для замыкания,
которое сохраняется).
Псевдоним типы также обычно используются с типом
Result
для сокращения повторения. Рассмотрим модуль std::io в стандартной библиотеке. Операции ввода/
вывода часто возвращают тип
Result
для обработки ситуаций, когда операция не выполняется из-за ошибки. Эта библиотека имеет структуру std::io::Error которая представляет все возможные ошибки ввода/вывода. Многие функции в библиотеке std::io будут возвращать
Result
, где
E
- это std::io::Error
, например, такие как функции в
Write типаже:
Тип
Result<..., Error>
многократно повторяется. Таким образом, std::io имеет этот тип как объявление псевдонима:
Поскольку это объявление находится в модуле std::io
, мы можем использовать полностью квалифицированный псевдоним std::io::Result
, что является
ResultE>
с типом
E
заполненным типом std::io::Error
. Сигнатуры функций типажа
Write в
конечном итоге выглядят как:
type
Thunk
=
Box
<
dyn
Fn
() +
Send
+
'static
>; let f: Thunk =
Box
::new(|| println!
(
"hi"
)); fn takes_long_type
(f: Thunk) {
// --snip--
} fn returns_long_type
() -> Thunk {
// --snip--
} use std::fmt; use std::io::Error; pub trait
Write
{ fn write
(&
mut self
, buf: &[
u8
]) ->
Result
<
usize
, Error>; fn flush
(&
mut self
) ->
Result
<(), Error>; fn write_all
(&
mut self
, buf: &[
u8
]) ->
Result
<(), Error>; fn write_fmt
(&
mut self
, fmt: fmt::Arguments) ->
Result
<(), Error>;
} type
Result
= std::result::
Result
;

Псевдоним типа помогает двумя способами: он облегчает написание кода и даёт нам согласованный интерфейс для всего из std::io
. Поскольку это псевдоним, то это просто ещё один тип
Result
, что означает, что с ним мы можем использовать любые методы, которые работают с
Result
, а также специальный синтаксис вроде
?
оператора.
Тип Never, который никогда не возвращается
Rust имеет специальный тип с названием
!
, который известен в теории типов как
пустой тип (empty type), потому что у него нет значений. Мы предпочитаем называть его тип никогда (never type), потому что он стоит на месте возвращаемого типа, такая функция никогда не возвращает управление. Вот пример:
Этот код читается как «функция bar никогда не возвращается». Функции, которые никогда не возвращаются называются расходящимися функциями (diverging functions).
Нельзя создавать значения типа
!
, так как bar никогда не может вернуться.
Но для чего нужен тип, для которого вы никогда не сможете создать значения?
Напомним код из листинга 2-5; мы воспроизвели его часть здесь в листинге 19-26.
Листинг 19-26: Сопоставление
match
с веткой, которая заканчивается
continue
В то время мы опустили некоторые детали в этом коде. В главе 6 раздела "Оператор управления потоком match
"
мы обсуждали, что все ветви match должны возвращать одинаковый тип. Например, следующий код не работает:
pub trait
Write
{ fn write
(&
mut self
, buf: &[
u8
]) ->
Result
<
usize
>; fn flush
(&
mut self
) ->
Result
<()>; fn write_all
(&
mut self
, buf: &[
u8
]) ->
Result
<()>; fn write_fmt
(&
mut self
, fmt: fmt::Arguments) ->
Result
<()>;
} fn bar
() -> ! {
// --snip--
} let guess: u32
= match guess.trim().parse() {
Ok
(num) => num,
Err
(_) => continue
,
}; let guess = match guess.trim().parse() {
Ok
(_) =>
5
,
Err
(_) =>
"hello"
,
};

Тип guess в этом коде должен быть целым числом и строкой и Rust требует, чтобы guess имел только один тип. Так что тогда возвращает код continue
? Как нам разрешили вернуть u32
из одной ветки и иметь другую ветку заканчивающуюся на continue в
листинге 19-26?
Как вы уже возможно догадались, continue имеет значение
!
. То есть, когда Rust вычисляет тип guess
, он смотрит на обе сопоставляемые ветки, первая со значением u32
и последняя со значением
!
. Так как
!
никогда не может иметь значение, то Rust решает что типом guess является тип u32
Формальным способом описания этого поведения является то, что выражения типа
!
могу быть приведены (coerced) к любому другому типу. Нам разрешено закончить сопоставление этой match ветки с помощью continue
, потому что continue не возвращает значение; вместо этого она передаёт контроль обратно в начало цикла,
поэтому в случае
Err мы никогда не присваиваем guess значение.
Never тип полезен также с макросом panic!
. Помните, функцию unwrap
, которую мы вызываем для значений
Option
, чтобы создать значение или вызвать панику? Вот её
определение:
В этом коде происходит то же самое, что и в выражении match из листинга 19-26: Rust видит, что val имеет тип
T
и panic!
имеет тип
!
, поэтому общим результатом match выражения является
T
. Этот код работает, потому что panic!
не производит значения;
он завершает выполнение программы. В случае
None
, мы не будем возвращать значение из unwrap
, поэтому этот код действительный.
Последнее выражение, которое имеет тип
!
это loop
:
Здесь цикл никогда не заканчивается, так что
!
(never type) является значением выражения. Тем не менее, это не будет правдой, если мы добавим в цикл break
, потому что цикл мог бы завершится, когда дело дойдёт до break impl

Option
{ pub fn unwrap
(
self
) -> T { match self
{
Some
(val) => val,
None
=> panic!
(
"called `Option::unwrap()` on a `None` value"
),
}
}
} print!
(
"forever "
); loop
{ print!
(
"and ever "
);
}

Динамические типы и Sized типаж
В связи с необходимостью Rust знать определённые детали, например, сколько места выделять для значения определённого типа, то существует краеугольный камень его системы типов, который может сбивать с толку. Это концепция динамических типов
(dynamically sized types). Иногда она упоминается как DST или безразмерные типы (unsized types), эти типы позволяют писать код, используя значения, чей размер известен только во время выполнения.
Давайте углубимся в детали динамического типа str
, который мы использовали на протяжении всей книги. Все верно, не типа
&str
, а типа str самого по себе, который является DST. Мы не можем знать, какой длины строка до момента времени выполнения,
то есть мы не можем создать переменную типа str и не можем принять аргумент типа str
. Рассмотрим следующий код, который не работает:
Rust должен знать, сколько памяти выделить для любого значения конкретного типа и все значения типа должны использовать одинаковый объем памяти. Если Rust позволил бы нам написать такой код, то эти два значения str должны были бы занимать одинаковое количество памяти. Но они имеют разную длину: s1
нужно 12 байтов памяти, а для s2
нужно 15. Вот почему невозможно создать переменную имеющую динамический тип.
Так что же нам делать? В этом случае вы уже знаете ответ: мы делаем типы s1
и s2
в виде типа
&str
, а не str
. Напомним, что в разделе "Строковые срезы"
главы 4, мы сказали, что структура данных срез хранит начальную позицию и длину среза.
Таким образом, хотя
&T
является единственным значением, которое хранит адрес памяти где находится тип
T
, тип
&str является двумя значениями: адресом str и его длиной. Таким образом, мы можем знать размер значения
&str во время компиляции:
это двойная длина от типа usize
. То есть мы всегда знаем размер
&str
, неважно какой длины является строка на которую она ссылается. В общем, это способ которым в Rust используются динамические типы: у них есть дополнительные метаданные в которых хранится размер динамической информации. Золотое правило динамических типов в том, что мы всегда должны ставить значения динамических типов позади некоторого указателя.
Можно комбинировать str со всеми видами указателей: например,
Box
или
Rc
. На самом деле, вы видели это раньше, но с другим динамическим типом:
типажом. Каждый типаж является динамическим типом к которому можно обратиться используя имя типажа. В разделе "Использование объектов-типажей, которые разрешаю использовать разные значения типов"
главы 17, мы упоминали, что для использования типажей в качестве объектов-типажей мы должны поместить их за указателем, например
&dyn Trait или
Box
(
Rc
тоже будет работать).
let s1: str
=
"Hello there!"
; let s2: str
=
"How's it going?"
;

Для работы с DST в Rust есть особый типаж, называемый
Sized для определения,
известен ли размер типа во время компиляции. Этот типаж автоматически реализуется для всех типов, чей размер известен во время компиляции. Кроме того, Rust неявно добавляет ограничение
Sized в каждую обобщённую функцию. То есть определение обобщённой функции написанное как:
на самом деле рассматривается как если бы мы написали её в виде:
По умолчанию обобщённые функции будут работать только с типами чей размер известен в время компиляции. Тем не менее, можно использовать следующий специальный синтаксис, чтобы ослабить это ограничение:
Ограничение на типаж
?Sized означает «
T
может или не может быть
Sized
», и это обозначение имеет приоритет по умолчанию. Общие типы должны иметь известный размер во время компиляции. Синтаксис
?Trait с таким значением доступен только для
Sized
, но не для любых других типажей.
Также обратите внимание, что мы поменяли тип параметра t
с
T
на
&T
. Поскольку тип мог бы не быть
Sized
, мы должны использовать его за каким-либо указателем. В в этом случае мы выбрали ссылку.
Далее мы поговорим о функциях и замыканиях!
fn generic
(t: T) {
// --snip--
} fn generic
Sized
>(t: T) {
// --snip--
} fn generic
Sized
>(t: &T) {
// --snip--
}

Продвинутые функции и замыкания
Наконец, мы рассмотрим некоторые дополнительные возможности, связанные с функциями и замыкания, которые включают указатели на функции и возврат замыканий.
Указатели функций
Мы говорили о том, как передавать замыкания в функции; но вы также можете передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать функцию, которую вы уже определили, а не объявлять новое замыкание. Указатель функции позволит использовать функции как аргументы к другим функциям. Функции приводятся (coerce) к типу fn
(с нижним регистром f), не к путать с типажом замыкания
Fn
. Тип fn называется указателем функции. Синтаксис для указания того, что параметр является указателем функции, похож на замыкание как показано в листинге 19-27.
Файл: src/main.rs
Листинг 19-27: Использование типа
fn
для принятия указателя функции в качестве аргумента
Этот код печатает
The answer is: 12
. Мы указываем, что параметр вызова f
для функции do_twice является fn
, которая принимает один параметр типа i32
и возвращает тип i32
. Затем мы можем вызвать f
в теле функции do_twice
. В main показано как можно передать имя функции add_one в качестве первого аргумента для функции do_twice
В отличие от замыканий, fn является типом, а не типажом, поэтому мы указываем fn как параметр типа напрямую, а не объявляем параметр обобщённого типа с одним из типажей
Fn в качестве ограничения типажа.
Указатели функций реализуют все три типажа замыканий (
Fn
,
FnMut и
FnOnce
), поэтому вы всегда можете передать указатель функции в качестве аргумента функции ожидающей замыкание. Лучше всего объявлять функции, используя обобщённый тип и fn add_one
(x: i32
) -> i32
{ x +
1
} fn do_twice
(f: fn
(
i32
) -> i32
, arg: i32
) -> i32
{ f(arg) + f(arg)
} fn main
() { let answer = do_twice(add_one,
5
); println!
(
"The answer is: {}"
, answer);
}
одним из типажей замыкания, так что ваши функции могут принимать либо функции,
либо замыкания.
Пример того, где вы хотели бы принимать только тип fn
, а не замыкания является взаимодействие с внешним кодом, который не имеет замыканий: функции в C могут принимать функции в качестве аргументов, но C не имеет замыканий.
Для примера того, где вы могли бы использовать либо замыкание, определённое как встроенное, либо именованную функцию, давайте посмотрим на использование map
Для использования функции map
, чтобы превратить вектор чисел в вектор строк, мы могли бы использовать замыкание, как здесь:
Или мы могли бы назвать функцию вместо замыкания в качестве аргумента при вызове map
, как здесь:
Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Расширенные типажи"
, потому что доступно несколько функций с именем to_string
. Здесь мы используем функцию to_string определённую в типаже
ToString
, который реализован в стандартной библиотеке для любого типа реализующего типаж
Display
У нас есть ещё один полезный шаблон, который использует детали реализации структур кортежей (tuple structs) и вариантов перечислений структур кортежей (tuple-struct enum).
Эти типы используют
()
в качестве синтаксиса инициализатора, который выглядит как вызов функции. Инициализаторы на самом деле реализованы как функции,
возвращающие экземпляр, который построен из их аргументов. Мы можем использовать эти функции инициализаторы как указатели на функции, которые реализуют типажи замыканий, что означает мы можем указать инициализирующие функции в качестве аргументов для методов, которые принимают замыкания, например:
Здесь мы создаём экземпляры
Status::Value
, используя каждое значение u32
в диапазоне (0..20), с которым вызывается map с помощью функции инициализатора
Status::Value
. Некоторые люди предпочитают этот стиль, а некоторые предпочитают let list_of_numbers = vec!
[
1
,
2
,
3
]; let list_of_strings:
Vec
<
String
> = list_of_numbers.iter().map(|i| i.to_string()).collect(); let list_of_numbers = vec!
[
1
,
2
,
3
]; let list_of_strings:
Vec
<
String
> = list_of_numbers.iter().map(
ToString
::to_string).collect(); enum
Status
{
Value(
u32
),
Stop,
} let list_of_statuses:
Vec
= (
0u32 20
).map(Status::Value).collect();
использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее.
Возврат замыканий
Замыкания представлены типажами, что означает невозможность напрямую вернуть замыкания. В большинстве случаев, когда вы возможно хотите вернуть типаж, вы вместо этого используете конкретный тип, который реализует типаж в качестве возвращаемого значения функции. Но вы не можете сделать этого с замыканиями, потому что у них нет конкретного типа, который можно вернуть; не разрешается использовать указатель функции fn в качестве возвращаемого типа, например.
Следующий код пытается напрямую вернуть замыкание, но он не компилируется:
Ошибка компилятора выглядит следующим образом:
Ошибка снова ссылается на типаж
Sized
! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект:
fn returns_closure
() -> dyn
Fn
(
i32
) -> i32
{
|x| x +
1
}
$
cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example) error[E0746]: return type cannot have an unboxed trait object
-->
src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see
1   ...   50   51   52   53   54   55   56   57   ...   62
Источник: https://files.student-it.ru/previewfile/264058