ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1144
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 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*
Этот вывод является не тем, что мы хотели получить. Мы хотим вызвать функцию 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)
}
}
Расширенные типы
Система типов 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);
}
For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error fn returns_closure
() ->
Box
<
dyn
Fn
(
i32
) -> i32
> {
Box
::new(|x| x +
1
)
}
Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов"
главы 17.
Далее давайте посмотрим на макросы!
Макросы
Мы использовали макросы, такие как println!
на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Термин макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules!
и три вида процедурных (procedural) макросов:
Пользовательские (выводимые)
#[derive]
макросы, которые указывают код добавленный с помощью derive атрибута, используемые для структур и перечислений
Макросы подобные атрибутам (attribute-like), которые определяют настраиваемые атрибуты, используемые для любого элемента языка
Функционально подобные (function-like) макросы, которые выглядят как вызовы функций, но работают с TokenStream
Мы поговорим о каждом из них по очереди, но сначала давайте рассмотрим, зачем вообще нужны макросы, если есть функции.
Разница между макросами и функциями
По сути, макросы являются способом написания кода, который записывает другой код,
что известно как мета программирование. В приложении C мы обсуждаем атрибут derive
, который генерирует за вас реализацию различных типажей. Вы также использовали макросы println!
и vec!
в книге. Все эти макросы раскрываются для генерации большего количества кода, чем исходный код написанный вами вручную.
Мета программирование полезно для уменьшения объёма кода, который вы должны написать и поддерживать, что также является одним из предназначений функций.
Однако макросы имеют некоторые дополнительные возможности, которых функции не имеют.
Сигнатура функции должна объявлять некоторое количество и тип этих параметров имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число параметров: мы можем вызвать println!("hello")
с одним аргументом или println!
("hello {}", name)
с двумя аргументами. Также макросы раскрываются до того как компилятор интерпретирует смысл кода, поэтому макрос может, например, реализовать типаж заданного типа. Функция этого не может, потому что она вызывается во время выполнения и типаж должен быть реализован во время компиляции.
Обратной стороной реализации макроса вместо функции является то, что определения макросов являются более сложными, чем определения функций, потому что вы создаёте
Rust код, который записывает другой Rust код. Из-за этой косвенности, объявления макросов, как правило, труднее читать, понимать и поддерживать, чем объявления функций.
Другое важное различие между макросами и функциями заключается в том, что вы должны объявить макросы или добавить их в область видимости прежде чем можете вызывать их в файле, в отличии от функций, которые вы можете объявить где угодно и вызывать из любого места.
Декларативные макросы с macro_rules! для общего мета
программирования
Наиболее широко используемой формой макросов в Rust являются декларативные
макросы. Они также иногда упоминаются как "макросы на примере", "
macro_rules!
макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match в Rust. Как обсуждалось в главе 6, match выражения являются управляющими структурами, которые принимают некоторое выражение,
результат значения выражения сопоставляют с шаблонами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с шаблонами, которые связаны с конкретным кодом: в этой ситуации значение является литералом исходного кода Rust, переданным в макрос. Шаблоны сравниваются со структурами этого исходного кода и при совпадении код, связанный с каждым шаблоном, заменяет код переданный макросу. Все это происходит во время компиляции.
Для определения макроса используется конструкция macro_rules!
. Давайте рассмотрим,
как использовать macro_rules!
глядя на то, как объявлен макрос vec!
. В главе 8
рассказано, как можно использовать макрос vec!
для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор,
содержащий три целых числа:
Мы также могли использовать макрос vec!
для создания вектора из двух целых чисел или вектора из пяти строковых срезов. Мы не смогли бы использовать функцию, чтобы сделать то же самое, потому что мы не знали бы заранее количество или тип значений.
В листинге 19-28 приведено несколько упрощённое определение макроса vec!
Файл: src/lib.rs let v:
Vec
<
u32
> = vec!
[
1
,
2
,
3
];
Листинг 19-28: Упрощённая версия определения макроса
vec!
Примечание: фактическое определение макроса vec!
в стандартной библиотеке включает сначала код для предварительного выделения правильного объёма памяти. Этот код является оптимизацией, которую мы здесь не включаем, чтобы сделать пример проще.
Аннотация
#[macro_export]
указывает, что данный макрос должен быть доступен всякий раз, когда крейт с объявленным макросом, добавлен в область видимости. Без этой аннотации макрос нельзя добавить в область видимости.
Затем мы начинаем объявление макроса с помощью macro_rules!
и имени макроса,
который объявляется без восклицательного знака. Название, в данном случае vec
, после которого следуют фигурные скобки, указывающие тело определения макроса.
Структура в теле макроса vec!
похожа на структуру match выражения. Здесь у нас есть одна ветвь с шаблоном
( $( $x:expr ),* )
, затем следует ветвь
=>
и блок кода,
связанный с этим шаблоном. Если шаблон сопоставлен успешно, то соответствующий блок кода будет сгенерирован. Учитывая, что данный код является единственным шаблоном в этом макросе, существует только один действительный способ сопоставления, любой другой шаблон приведёт к ошибке. Более сложные макросы будут иметь более чем одна ветвь.
Допустимый синтаксис шаблона в определениях макросов отличается от синтаксиса шаблона рассмотренного в главе 18, потому что шаблоны макроса сопоставляются со структурами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части шаблона в листинге 19-28 что означают; полный синтаксис макроса см. в ссылке
Во-первых, набор скобок охватывает весь шаблон. Далее идёт знак доллара (
$
), затем следует набор скобок, который захватывает значения, соответствующие шаблону в скобках для использования в коде замены. Внутри
$()
находится
$x:expr
, который соответствует любому выражению Rust и даёт выражению имя
$x
#[macro_export]
macro_rules!
vec {
( $( $x:expr ),* ) => {
{ let mut temp_vec =
Vec
::new();
$( temp_vec.push($x);
)* temp_vec
}
};
}
Запятая, следующая за
$()
указывает на то, что буквенный символ-разделитель запятой может дополнительно появиться после кода, который соответствует коду в
$()
Звёздочка
*
указывает, что шаблон соответствует ноль или больше раз тому, что предшествует
*
Когда вызывается этот макрос с помощью vec![1, 2, 3];
шаблон
$x соответствует три раза всем трём выражениям
1
,
2
и
3
Теперь давайте посмотрим на шаблон в теле кода, связанного с этой ветвью: temp_vec.push()
внутри
$()*
генерируется для каждой части, которая соответствует символу
$()
в шаблоне ноль или более раз в зависимости от того, сколько раз шаблон сопоставлен. Символ
$x заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3];
, сгенерированный код, заменяющий этот вызов макроса будет следующим:
Мы определили макрос, который может принимать любое количество аргументов любого типа и может генерировать код для создания вектора, содержащего указанные элементы.
Есть несколько странных краевых случаев у макроса macro_rules!
. В будущем у Rust будет второй вид декларативного макроса, который будет работать аналогичным образом, но поправит некоторые из этих краевых случаев. После этого обновления macro_rules!
будет фактически устаревшим. Имея это в виду, а также тот факт, что большинство Rust программистов будут использовать макросы больше, чем сами писать
макросы, мы далее не будем обсуждать macro_rules!
. Чтобы узнать больше о том, как писать макросы, обратитесь к электронной документации или другим ресурсам, таким как
“The Little Book of Rust Macros”
Процедурные макросы для генерации кода из атрибутов
Вторая форма макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются типом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с шаблонами и замену кода другим кодом, как это делают декларативные макросы.
Все три вида процедурных макросов (пользовательские выводимые, похожие на атрибуты и похожие на функции) все работают аналогично.
{ let mut temp_vec =
Vec
::new(); temp_vec.push(
1
); temp_vec.push(
2
); temp_vec.push(
3
); temp_vec
}
При создании процедурных макросов объявления должны находиться в собственном крейте специального типа. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. Использование процедурных макросов выглядит как код в листинге 19-29, где some_attribute является заполнителем для использования специального макроса.
Файл: src/lib.rs
Листинг 19-29: Пример использования процедурного макроса
Функция, которая определяет процедурный макрос, принимает
TokenStream в качестве входных данных и создаёт
TokenStream в качестве вывода. Тип
TokenStream объявлен крейтом proc_macro
, включённым в Rust и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным
TokenStream
, а код создаваемый макросом является выходным
TokenStream
. К
функции имеет также прикреплённый атрибут, определяющий какой тип процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же крейте.
Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия,
делающие другие формы отличающимися.
Как написать пользовательский derive макрос
Давайте создадим крейт с именем hello_macro
, который определяет типаж с именем
HelloMacro и имеет одну с ним ассоциированную функцию с именем hello_macro
Вместо того, чтобы пользователи нашего крейта самостоятельно реализовывали типаж
HelloMacro для каждого из своих типов, мы предоставим им процедурный макрос, чтобы они могли аннотировать свой тип с помощью атрибута
#[diverve(HelloMacro)]
и получили реализацию по умолчанию для функции hello_macro
. Реализация по умолчанию выведет
Hello, Macro! My name is TypeName!
, где
TypeName
- это имя типа,
для которого был определён этот типаж. Другими словами, мы напишем крейт,
использование которого позволит другому программисту писать код показанный в листинге 19-30.
Файл: src/main.rs use proc_macro;
#[some_attribute]
pub fn some_name
(input: TokenStream) -> TokenStream {
}
Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего
процедурного макроса
Этот код напечатает
Hello, Macro! My name is Pancakes!
, когда мы закончим. Первый шаг - создать новый, библиотечный крейт так:
Далее, мы определим типаж
HelloMacro и ассоциированную с ним функцию:
Файл: src/lib.rs
У нас есть типаж и его функция. На этом этапе пользователь крейта может реализовать типаж для достижения желаемой функциональности, так:
Тем не менее, ему придётся написать блок реализации для каждого типа, который он хотел использовать вместе с hello_macro
; а мы хотим избавить их от необходимости делать эту работу.
Кроме того, мы пока не можем предоставить функцию hello_macro с реализацией по умолчанию, которая будет печатать имя типа, для которого реализован типаж: Rust не имеет возможностей рефлексии (reflection), поэтому он не может выполнить поиск имени use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct
Pancakes
; fn main
() {
Pancakes::hello_macro();
}
$
cargo new hello_macro --lib pub trait
HelloMacro
{ fn hello_macro
();
} use hello_macro::HelloMacro; struct
Pancakes
; impl
HelloMacro for
Pancakes { fn hello_macro
() { println!
(
"Hello, Macro! My name is Pancakes!"
);
}
} fn main
() {
Pancakes::hello_macro();
}
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
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
Однако ассоциированные функции являющиеся частью типажей не имеют 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 {}"
,
}
В общем, полностью квалифицированный синтаксис определяется следующим образом:
Для ассоциированных функций при их вызове не будет 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
**********
* *
* (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);
}
В качестве примера, мы хотим реализовать типаж
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 для оборачивания типа
HashMap
, которой хранит идентификатор человека связанного с его именем. Код использующий
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
, что является
Result
с типом
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
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
(
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
// --snip--
} fn generic
>(t: T) {
// --snip--
} fn generic
>(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
, а не замыкания является взаимодействие с внешним кодом, который не имеет замыканий: функции в 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
Возврат замыканий
Замыкания представлены типажами, что означает невозможность напрямую вернуть замыкания. В большинстве случаев, когда вы возможно хотите вернуть типаж, вы вместо этого используете конкретный тип, который реализует типаж в качестве возвращаемого значения функции. Но вы не можете сделать этого с замыканиями, потому что у них нет конкретного типа, который можно вернуть; не разрешается использовать указатель функции 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
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
|
For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error fn returns_closure
() ->
Box
<
dyn
Fn
(
i32
) -> i32
> {
Box
::new(|x| x +
1
)
}
Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов"
главы 17.
Далее давайте посмотрим на макросы!
Макросы
Мы использовали макросы, такие как println!
на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Термин макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules!
и три вида процедурных (procedural) макросов:
Пользовательские (выводимые)
#[derive]
макросы, которые указывают код добавленный с помощью derive атрибута, используемые для структур и перечислений
Макросы подобные атрибутам (attribute-like), которые определяют настраиваемые атрибуты, используемые для любого элемента языка
Функционально подобные (function-like) макросы, которые выглядят как вызовы функций, но работают с TokenStream
Мы поговорим о каждом из них по очереди, но сначала давайте рассмотрим, зачем вообще нужны макросы, если есть функции.
Разница между макросами и функциями
По сути, макросы являются способом написания кода, который записывает другой код,
что известно как мета программирование. В приложении C мы обсуждаем атрибут derive
, который генерирует за вас реализацию различных типажей. Вы также использовали макросы println!
и vec!
в книге. Все эти макросы раскрываются для генерации большего количества кода, чем исходный код написанный вами вручную.
Мета программирование полезно для уменьшения объёма кода, который вы должны написать и поддерживать, что также является одним из предназначений функций.
Однако макросы имеют некоторые дополнительные возможности, которых функции не имеют.
Сигнатура функции должна объявлять некоторое количество и тип этих параметров имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число параметров: мы можем вызвать println!("hello")
с одним аргументом или println!
("hello {}", name)
с двумя аргументами. Также макросы раскрываются до того как компилятор интерпретирует смысл кода, поэтому макрос может, например, реализовать типаж заданного типа. Функция этого не может, потому что она вызывается во время выполнения и типаж должен быть реализован во время компиляции.
Обратной стороной реализации макроса вместо функции является то, что определения макросов являются более сложными, чем определения функций, потому что вы создаёте
Rust код, который записывает другой Rust код. Из-за этой косвенности, объявления макросов, как правило, труднее читать, понимать и поддерживать, чем объявления функций.
Другое важное различие между макросами и функциями заключается в том, что вы должны объявить макросы или добавить их в область видимости прежде чем можете вызывать их в файле, в отличии от функций, которые вы можете объявить где угодно и вызывать из любого места.
Декларативные макросы с macro_rules! для общего мета
программирования
Наиболее широко используемой формой макросов в Rust являются декларативные
макросы. Они также иногда упоминаются как "макросы на примере", "
macro_rules!
макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match в Rust. Как обсуждалось в главе 6, match выражения являются управляющими структурами, которые принимают некоторое выражение,
результат значения выражения сопоставляют с шаблонами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с шаблонами, которые связаны с конкретным кодом: в этой ситуации значение является литералом исходного кода Rust, переданным в макрос. Шаблоны сравниваются со структурами этого исходного кода и при совпадении код, связанный с каждым шаблоном, заменяет код переданный макросу. Все это происходит во время компиляции.
Для определения макроса используется конструкция macro_rules!
. Давайте рассмотрим,
как использовать macro_rules!
глядя на то, как объявлен макрос vec!
. В главе 8
рассказано, как можно использовать макрос vec!
для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор,
содержащий три целых числа:
Мы также могли использовать макрос vec!
для создания вектора из двух целых чисел или вектора из пяти строковых срезов. Мы не смогли бы использовать функцию, чтобы сделать то же самое, потому что мы не знали бы заранее количество или тип значений.
В листинге 19-28 приведено несколько упрощённое определение макроса vec!
Файл: src/lib.rs let v:
Vec
<
u32
> = vec!
[
1
,
2
,
3
];
Листинг 19-28: Упрощённая версия определения макроса
vec!
Примечание: фактическое определение макроса vec!
в стандартной библиотеке включает сначала код для предварительного выделения правильного объёма памяти. Этот код является оптимизацией, которую мы здесь не включаем, чтобы сделать пример проще.
Аннотация
#[macro_export]
указывает, что данный макрос должен быть доступен всякий раз, когда крейт с объявленным макросом, добавлен в область видимости. Без этой аннотации макрос нельзя добавить в область видимости.
Затем мы начинаем объявление макроса с помощью macro_rules!
и имени макроса,
который объявляется без восклицательного знака. Название, в данном случае vec
, после которого следуют фигурные скобки, указывающие тело определения макроса.
Структура в теле макроса vec!
похожа на структуру match выражения. Здесь у нас есть одна ветвь с шаблоном
( $( $x:expr ),* )
, затем следует ветвь
=>
и блок кода,
связанный с этим шаблоном. Если шаблон сопоставлен успешно, то соответствующий блок кода будет сгенерирован. Учитывая, что данный код является единственным шаблоном в этом макросе, существует только один действительный способ сопоставления, любой другой шаблон приведёт к ошибке. Более сложные макросы будут иметь более чем одна ветвь.
Допустимый синтаксис шаблона в определениях макросов отличается от синтаксиса шаблона рассмотренного в главе 18, потому что шаблоны макроса сопоставляются со структурами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части шаблона в листинге 19-28 что означают; полный синтаксис макроса см. в ссылке
Во-первых, набор скобок охватывает весь шаблон. Далее идёт знак доллара (
$
), затем следует набор скобок, который захватывает значения, соответствующие шаблону в скобках для использования в коде замены. Внутри
$()
находится
$x:expr
, который соответствует любому выражению Rust и даёт выражению имя
$x
#[macro_export]
macro_rules!
vec {
( $( $x:expr ),* ) => {
{ let mut temp_vec =
Vec
::new();
$( temp_vec.push($x);
)* temp_vec
}
};
}
Запятая, следующая за
$()
указывает на то, что буквенный символ-разделитель запятой может дополнительно появиться после кода, который соответствует коду в
$()
Звёздочка
*
указывает, что шаблон соответствует ноль или больше раз тому, что предшествует
*
Когда вызывается этот макрос с помощью vec![1, 2, 3];
шаблон
$x соответствует три раза всем трём выражениям
1
,
2
и
3
Теперь давайте посмотрим на шаблон в теле кода, связанного с этой ветвью: temp_vec.push()
внутри
$()*
генерируется для каждой части, которая соответствует символу
$()
в шаблоне ноль или более раз в зависимости от того, сколько раз шаблон сопоставлен. Символ
$x заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3];
, сгенерированный код, заменяющий этот вызов макроса будет следующим:
Мы определили макрос, который может принимать любое количество аргументов любого типа и может генерировать код для создания вектора, содержащего указанные элементы.
Есть несколько странных краевых случаев у макроса macro_rules!
. В будущем у Rust будет второй вид декларативного макроса, который будет работать аналогичным образом, но поправит некоторые из этих краевых случаев. После этого обновления macro_rules!
будет фактически устаревшим. Имея это в виду, а также тот факт, что большинство Rust программистов будут использовать макросы больше, чем сами писать
макросы, мы далее не будем обсуждать macro_rules!
. Чтобы узнать больше о том, как писать макросы, обратитесь к электронной документации или другим ресурсам, таким как
“The Little Book of Rust Macros”
Процедурные макросы для генерации кода из атрибутов
Вторая форма макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются типом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с шаблонами и замену кода другим кодом, как это делают декларативные макросы.
Все три вида процедурных макросов (пользовательские выводимые, похожие на атрибуты и похожие на функции) все работают аналогично.
{ let mut temp_vec =
Vec
::new(); temp_vec.push(
1
); temp_vec.push(
2
); temp_vec.push(
3
); temp_vec
}
При создании процедурных макросов объявления должны находиться в собственном крейте специального типа. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. Использование процедурных макросов выглядит как код в листинге 19-29, где some_attribute является заполнителем для использования специального макроса.
Файл: src/lib.rs
Листинг 19-29: Пример использования процедурного макроса
Функция, которая определяет процедурный макрос, принимает
TokenStream в качестве входных данных и создаёт
TokenStream в качестве вывода. Тип
TokenStream объявлен крейтом proc_macro
, включённым в Rust и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным
TokenStream
, а код создаваемый макросом является выходным
TokenStream
. К
функции имеет также прикреплённый атрибут, определяющий какой тип процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же крейте.
Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия,
делающие другие формы отличающимися.
Как написать пользовательский derive макрос
Давайте создадим крейт с именем hello_macro
, который определяет типаж с именем
HelloMacro и имеет одну с ним ассоциированную функцию с именем hello_macro
Вместо того, чтобы пользователи нашего крейта самостоятельно реализовывали типаж
HelloMacro для каждого из своих типов, мы предоставим им процедурный макрос, чтобы они могли аннотировать свой тип с помощью атрибута
#[diverve(HelloMacro)]
и получили реализацию по умолчанию для функции hello_macro
. Реализация по умолчанию выведет
Hello, Macro! My name is TypeName!
, где
TypeName
- это имя типа,
для которого был определён этот типаж. Другими словами, мы напишем крейт,
использование которого позволит другому программисту писать код показанный в листинге 19-30.
Файл: src/main.rs use proc_macro;
#[some_attribute]
pub fn some_name
(input: TokenStream) -> TokenStream {
}
Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего
процедурного макроса
Этот код напечатает
Hello, Macro! My name is Pancakes!
, когда мы закончим. Первый шаг - создать новый, библиотечный крейт так:
Далее, мы определим типаж
HelloMacro и ассоциированную с ним функцию:
Файл: src/lib.rs
У нас есть типаж и его функция. На этом этапе пользователь крейта может реализовать типаж для достижения желаемой функциональности, так:
Тем не менее, ему придётся написать блок реализации для каждого типа, который он хотел использовать вместе с hello_macro
; а мы хотим избавить их от необходимости делать эту работу.
Кроме того, мы пока не можем предоставить функцию hello_macro с реализацией по умолчанию, которая будет печатать имя типа, для которого реализован типаж: Rust не имеет возможностей рефлексии (reflection), поэтому он не может выполнить поиск имени use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct
Pancakes
; fn main
() {
Pancakes::hello_macro();
}
$
cargo new hello_macro --lib pub trait
HelloMacro
{ fn hello_macro
();
} use hello_macro::HelloMacro; struct
Pancakes
; impl
HelloMacro for
Pancakes { fn hello_macro
() { println!
(
"Hello, Macro! My name is Pancakes!"
);
}
} fn main
() {
Pancakes::hello_macro();
}
типа во время выполнения кода. Нам нужен макрос для генерации кода во время компиляции.
Следующим шагом является определение процедурного макроса. На момент написания этой статьи процедурные макросы должны быть в собственном крейте. Со временем это ограничение может быть отменено. Соглашение о структурировании крейтов и макросов является следующим: для крейта с именем foo
, его пользовательский, крейт с выводимым процедурным макросом называется foo_derive
. Давайте начнём с создания нового крейта с именем hello_macro_derive внутри проекта hello_macro
:
Наши два крейта тесно связаны, поэтому мы создаём процедурный макрос-крейт в каталоге крейта hello_macro
. Если мы изменим определение типажа в hello_macro
, то нам придётся также изменить реализацию процедурного макроса в hello_macro_derive
Два крейта нужно будет опубликованы отдельно и программисты, использующие эти крейты, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что крейт hello_macro использует hello_macro_derive как зависимость и реэкспортирует код процедурного макроса.
Однако то, как мы структурировали проект, делает возможным программистам использовать hello_macro даже если они не хотят derive функциональность.
Нам нужно объявить крейт hello_macro_derive как процедурный макрос-крейт. Также понадобятся функционал из крейтов syn и quote
, как вы увидите через мгновение,
поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml
для hello_macro_derive
:
Файл: hello_macro_derive/Cargo.toml
Чтобы начать определение процедурного макроса, поместите код листинга 19-31 в ваш файл src/lib.rs крейта hello_macro_derive
. Обратите внимание, что этот код не скомпилируется пока мы не добавим определение для функции impl_hello_macro
Файл: hello_macro_derive/src/lib.rs
$
cargo new hello_macro_derive --lib
[lib]
proc-macro = true
[dependencies]
syn =
"1.0"
quote =
"1.0"
Следующим шагом является определение процедурного макроса. На момент написания этой статьи процедурные макросы должны быть в собственном крейте. Со временем это ограничение может быть отменено. Соглашение о структурировании крейтов и макросов является следующим: для крейта с именем foo
, его пользовательский, крейт с выводимым процедурным макросом называется foo_derive
. Давайте начнём с создания нового крейта с именем hello_macro_derive внутри проекта hello_macro
:
Наши два крейта тесно связаны, поэтому мы создаём процедурный макрос-крейт в каталоге крейта hello_macro
. Если мы изменим определение типажа в hello_macro
, то нам придётся также изменить реализацию процедурного макроса в hello_macro_derive
Два крейта нужно будет опубликованы отдельно и программисты, использующие эти крейты, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что крейт hello_macro использует hello_macro_derive как зависимость и реэкспортирует код процедурного макроса.
Однако то, как мы структурировали проект, делает возможным программистам использовать hello_macro даже если они не хотят derive функциональность.
Нам нужно объявить крейт hello_macro_derive как процедурный макрос-крейт. Также понадобятся функционал из крейтов syn и quote
, как вы увидите через мгновение,
поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml
для hello_macro_derive
:
Файл: hello_macro_derive/Cargo.toml
Чтобы начать определение процедурного макроса, поместите код листинга 19-31 в ваш файл src/lib.rs крейта hello_macro_derive
. Обратите внимание, что этот код не скомпилируется пока мы не добавим определение для функции impl_hello_macro
Файл: hello_macro_derive/src/lib.rs
$
cargo new hello_macro_derive --lib
[lib]
proc-macro = true
[dependencies]
syn =
"1.0"
quote =
"1.0"