Rust на примерах
Rust — современный язык программирования, нацеленный на безопасность, скорость и параллелизм. Данные цели достигаются за счёт безопасной работы с памятью без использования сборщика мусора.
Rust на примерах (Rust by Example, RBE) — это набор исполняемых примеров, которые иллюстрируют различные концепции языка Rust, а так же возможности его стандартной библиотеки. Для того, чтобы почерпнуть ещё больше из этих примеров, не забудьте установить Rust на своём компьютере и ознакомиться с официальной документацией. Самые любознательные могут заглянуть в исходный код этого сайта.
Итак, давайте приступим!
-
Hello World - начнём с традиционной программы Hello World.
-
Примитивы - узнаем о целых числах со знаком, целых числах без знака и других примитивах.
-
Пользовательские типы -
struct
иenum
. -
Связывание переменных - изменяемые связывания, область видимости, затенение.
-
Типы - изменение и определение типов.
-
Управление потоком -
if
/else
,for
и другие. -
Функции - узнаем о методах, замыканиях и функциях высокого порядка.
-
Модули - организуем код с помощью модулей.
-
Пакет - единица компиляции в Rust. Научимся создавать библиотеку.
-
Cargo - познакомимся с основными функциями официального пакетного менеджера Rust.
-
Атрибуты - метаданные, применяемые к какому-либо модулю, пакету или элементу.
-
Обобщения - узнаем о написании функции или типа данных, которые могут работать для нескольких типов аргументов.
-
Правила областей видимости - области видимости играют важную роль во владении, заимствовании и продолжительности жизни.
-
Трейты - это набор методов, определённых для неизвестного типа:
Self
. -
Обработка ошибок - узнаем, как это делать в Rust.
-
Типы стандартной библиотеки - изучим некоторые пользовательские типы, предоставленные стандартной библиотекой.
-
Разное в стандартной библиотеке - больше пользовательских типов для обработки файлов, потоков.
-
Тестирование - все виды тестов в Rust.
-
Meta - документация, бенчмаркинг.
Привет, мир
Это исходный код традиционной программы "Привет, мир!".
println!
- это макрос, который отображает текст в консоли.
Исполняемый файл может быть сгенерирован с помощью компилятора Rust — rustc
.
$ rustc hello.rs
rustc
создаст исполняемый файл hello
, который можно будет запустить.
$ ./hello
Привет, мир!
Задание
Нажми кнопку "Run", чтобы увидеть ожидаемый результат. Затем добавь новую строку с другим макросом println!
, чтобы вывод был таким:
Привет, мир!
Я программирую на языке Rust!
Комментарии
Каждая программа, безусловно, нуждается в комментариях и Rust предоставляет несколько способов комментирования кода:
-
Обычные комментарии, которые игнорируются компилятором:
// Однострочный комментарий. Который завершается в конце строки.
/* Блочный комментарий, который продолжается до завершающего символа. */
-
Doc комментарии, которые будут сгенерированы в HTML документацию:
/// Генерация документации для функции.
//! Генерация документации для модуля.
Смотрите также:
Форматированный вывод
Вывод обрабатывается несколькими макросами, которые определены в std::fmt
. Вот некоторые из них:
format!
: записывает форматированный текст вString
.print!
: работает аналогично сformat!
, но текст выводится в консоль (io::stdout).println!
: аналогичноprint!
, но в конце добавляется переход на новую строку.eprint!
: аналогичноformat!
, но текст выводится в стандартный поток ошибок (io::stderr).eprintln!
: аналогичноeprint!
, но в конце добавляется переход на новую строку.
Весь текст обрабатывается аналогичным образом. Плюс данного метода в том, что корректность форматирования будет проверена на этапе компиляции программы.
std::fmt
содержит в себе много типажей, которые управляют отображением текста. Базовая форма двух самых важных рассмотрена ниже:
fmt::Debug
: Использует маркер{:?}
. Форматирует текст для отладочных целей.fmt::Display
: Использует маркер{}
. Форматирует текст в более элегантном,удобном для пользователя стиле.
В данном примере используется fmt::Display
, потому что стандартная библиотека предоставляет реализацию для данного типа. Для отображения собственных типов потребуется больше дополнительных шагов.
Реализация типажа fmt::Display
автоматически предоставляет реализацию типажа ToString
, который позволяет нам конвертировать наш тип в String
.
Задания
- Исправьте две ошибки в коде выше (смотрите ИСПРАВЬТЕ), чтобы код компилировался без ошибок
- Добавьте макрос
println!
, который выводит:Pi is roughly 3.142
с помощью управления количеством знаков после запятой. Для выполнения данного задания создайте переменную, которая будет хранить в себе значение числа Пи:let pi = 3.141592
. (Подсказка: вам необходимо ознакомиться с документацией поstd::fmt
, чтобы узнать, как отобразить в консоли только часть знаков после запятой).
Смотрите также:
std::fmt
, макросы, struct
, и trait
Debug
Все типы, которые будут использовать типажи форматирования std::fmt
, требуют их реализации для возможности печати. Автоматическая реализация предоставлена только для типов из стандартной библиотеки (std
). Все остальные типы должны иметь собственную реализацию.
C помощью типажа fmt::Debug
это сделать очень просто. Все типы могут выводить (автоматически создавать, derive
) реализацию fmt::Debug
. Сделать подобное с fmt::Display
невозможно, он должен быть реализован вручную.
Все типы из библиотеки std
также могут быть автоматически распечатаны с помощью {:?}
:
Так что fmt::Debug
определённо позволяет распечатать объект, но жертвует некоторым изяществом. Rust также обеспечивает "красивую печать" с помощью {:#?}
.
Можно вручную реализовать fmt::Display
для управления отображением.
Смотрите также:
атрибуты, derive
, std::fmt
, и struct
Display
fmt::Debug
выглядит не очень компактно и красиво, поэтому полезно настраивать внешний вид информации, которая будет напечатана. Это можно сделать реализовав типаж fmt::Display
вручную, который использует маркер {}
для печати. Его реализация выглядит следующим образом:
Вывод fmt::Display
может быть более чистым, чем fmt::Debug
, но может быть проблемой для стандартной библиотеки (std
). Как нестандартные типы должны отображаться? Например, если std
предоставляет единый стиль вывода для Vec<T>
, каким этот вывод должен быть? Любой из этих двух?
Vec<path>
:/:/etc:/home/username:/bin
(разделитель:
)Vec<number>
:1,2,3
(разделитель,
)
Нет, потому что не существует идеального стиля вывода для всех типов, поэтому std
не может его предоставить. fmt::Display
не реализован для Vec<T>
или для других обобщённых контейнеров. Для этих случаев подойдёт fmt::Debug
.
Это не проблема, потому что для любых новых контейнеров, типы которых не обобщённые, может быть реализован fmt::Display
.
Итак, fmt::Display
был реализован, но fmt::Binary
нет, следовательно не может быть использован. std::fmt
имеет много таких типажей и каждый из них требует свою реализацию. Это более подробно описано в документации к std::fmt
.
Задание
После того, как запустите код, представленный выше, используйте структуру Point2D
как пример и добавьте новую структуру Complex
, чтобы вывод был таким:
Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }
Смотрите также:
derive
, std::fmt
, макросы
, struct
, trait
и use
Пример: список
Реализовать fmt::Display
для структуры, в которой каждый элемент должен обрабатываться последовательно, не так-то просто. Проблема в том, что write!
каждый раз возвращает fmt::Result
. Для правильного обращения с этим необходимо обрабатывать все результаты. Для этой цели Rust предоставляет оператор ?
.
Использование ?
для write!
выглядит следующим образом:
// Попробуй исполнить `write!`, чтобы узнать, вернется ли ошибка. Если будет ошибка — верни её.
// Если нет, то продолжи.
write!(f, "{}", value)?;
С помощью оператора ?
реализация fmt::Display
для Vec
довольно простая:
Задание
Попробуйте изменить программу так, чтобы индекс элемента тоже выводился в консоль. Новый вывод должен выглядеть примерно вот так:
[0: 1, 1: 2, 2: 3]
Смотрите также:
for
, ref
, Result
, struct
, ?
, и vec!
Форматирование
Мы видели, что форматирование задаётся макросом форматирования:
format!("{}", foo)
->"3735928559"
format!("0x{:X}", foo)
->"0xDEADBEEF"
format!("0o{:o}", foo)
->"0o33653337357"
Одна и та же переменная (foo
) может быть отображена по разному в зависимости от используемого типа аргумента: X
, o
или неопределённый.
Функционал форматирования реализован благодаря типажу, и для каждого типа аргумента существует свой. Наиболее распространённый типаж для форматирования — Display
, который работает без аргументов: например {}
.
Вы можете посмотреть полный список типажей форматирования и их типы аргументов в документации к std::fmt
.
Задание
Добавьте реализацию типажа fmt::Display
для структуры Color
, чтобы вывод отображался вот так:
RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000
Пара подсказок, если вы не знаете, что делать:
- Возможно, вам потребуется перечислить каждый цвет несколько раз.
- Вы можете добавить немного нулей с
:02
.
Смотрите также:
Примитивы
Rust предоставляет доступ к большому количеству примитивов
:
Скалярные типы
- знаковые целочисленные:
i8
,i16
,i32
,i64
иisize
(размер указателя) - беззнаковые целочисленные:
u8
,u16
,u32
,u64
иusize
(размер указателя) - вещественные:
f32
,f64
char
скалярное значение Unicode, например:'a'
,'α'
и'∞'
(4 байта каждый)bool
:true
илиfalse
- единичный тип
()
, значение которого так же()
Несмотря на то, что значение единичного типа является кортежем, оно не считается составным типом, потому что не содержит нескольких значений.
Составные типы
- массивы, например
[1, 2, 3]
- кортежи, например
(1, true)
Переменные всегда должны быть аннотированы.
Числам можно указать определённый тип с помощью суффикса,
иначе будет присвоен тип по умолчанию.
Целочисленные значения по умолчанию i32
, а вещественные f64
.
Стоит заметить, что Rust также умеет выводить типы из контекста.
Смотрите также:
стандартная библиотека (std
), mut
, вывод типов и затенение
Литералы и операторы
Целочисленное 1
, вещественное 1.2
, символ 'a'
, строка "abc"
, логическое true
и единичный тип ()
могут быть выражены с помощью литералов.
Целочисленные значения так же могут быть выражены с помощью шестнадцатеричного, восьмеричного или двоичного обозначения используя соответствующие префиксы: 0x
, 0o
или 0b
.
Для улучшения читаемости числовых литералов можно использовать подчёркивания, например 1_000
тоже самое, что и 1000
, и 0.000_001
равно 0.000001
.
Нам необходимо указать компилятору какой тип для литерала мы используем. Сейчас мы используем суффикс u32
, чтобы указать, что литерал - беззнаковое целое число 32-х бит и суффикс i32
- знаковое целое 32-х битное число.
Доступные операторы и их приоритет в Rust такой же как и в других C-подобных языках.
Кортежи
Кортежи - коллекция, которая хранит в себе переменные разных типов. Кортежи
создаются с помощью круглых скобок ()
, и каждый кортеж является переменной
с сигнатурой типов (T1, T2, ...)
, где T1
, T2
тип члена кортежа.
Функции могут использовать кортежи для возвращения нескольких значений,
так кортежи могут хранить любое количество значений.
Задание
-
Повторение: Добавьте реализацию типажа
fmt::Display
дляструктуры
Matrix в примерах выше, чтобы, когда вы измените формат вывода с{:?}
на{}
на консоль вывелось:( 1.1 1.2 ) ( 2.1 2.2 )
Вы можете вернуться на пример print display.
-
Добавьте функцию
transpose
, используя функциюreverse
, как пример, которая принимает матрицу, как аргумент и возвращает матрицу, в которой два элемента поменялись местами. Например:println!("Matrix:\n{}", matrix); println!("Transpose:\n{}", transpose(matrix));
Результат:
Matrix: ( 1.1 1.2 ) ( 2.1 2.2 ) Transpose: ( 1.1 2.1 ) ( 1.2 2.2 )
Массивы и срезы
Массив — это коллекция объектов одинакового типа T
, расположенных в памяти непосредственно друг за другом. Массивы создаются с помощью квадратных скобок []
, а их размер должен быть известен во время компиляции и является частью сигнатуры типа [T; size]
.
Срезы похожи на массивы, но их размер неизвестен в момент компиляции программы. Срезы представляют собой объекты, состоящие из указателя на данные и размер среза. Размер среза равен размеру usize
и зависит от архитектуры процессора: например, для x86-64 он равен 64 битам. Срезы могут быть использованы для заимствования части массива и будут иметь сигнатуру типа &[T]
.
Пользовательские типы
В языке программирования Rust пользовательские типы данных в основном создаются при помощи двух ключевых слов:
struct
: определение структурыenum
: определение перечисления
Константы так же могут быть созданы с помощью ключевых слов const
и static
.
Структуры
Существует три типа структур, которые можно создать с помощью ключевого слова struct
:
- Кортежная структура, которая на самом деле является именованным кортежем.
- Классическая C-структура
- Единичная структура, которая не имеет полей, но может быть полезна для обобщённых типов.
Задание
- Добавьте функцию
rect_area
, которая рассчитывает площадь прямоугольника. Попробуйте использовать деструктуризацию — разбор на части. - Добавьте функцию
square
которая принимаетPoint
иf32
в качестве аргументов, и возвращаетRectangle
левый верхний угол которогоPoint
, а ширина и высота соответствуютf32
.
Смотрите также:
Перечисления
Ключевое слово enum
позволяет создавать тип данных, который представляет собой один из нескольких возможных вариантов. Любой вариант, действительный как struct
, также действителен как enum
.
Псевдонимы типов
Если вы используете псевдонимы типов, то вы можете обратиться к каждому варианту перечисления через его псевдоним. Это может быть полезно, если у перечисления слишком длинное имя или оно слишком обобщено, и вы хотите переименовать его.
Самое частое место, где можно это увидеть, — impl
-блоки, которые используют Self
.
Чтобы больше узнать о перечислениях и псевдонимах типов, вы можете почитать отчёт о стабилизации, в котором эта возможность была включена в Rust.
Смотрите также:
match
, fn
, String
и "Type alias enum variants" RFC
Декларация use
Декларация use
используется, чтобы убрать необходимость указывать область видимости:
Смотрите также:
С-подобные
enum
могут быть использованы как С-подобные перечисления.
Смотрите также:
Пример: Связанный список
Пример использования enums
для создания связанного списка:
Смотрите также:
Константы
В Rust есть два типа констант, которые могут быть объявлены в любой области видимости, включая глобальную. Оба требуют явной аннотации типа:
const
: Неизменяемая переменная (в общем случае).static
: Возможно,изменяемая
переменная с временем жизни'static
. Статическое время жизни подразумевается и может не быть указано явно. Доступ или модификация изменяемой статической переменной — небезопасны (см.unsafe
).
Смотрите также:
RFC для const
/static
, время жизни 'static
Связывание переменных
Rust предоставляет безопасность типов с помощью статической типизации. Тип переменной может быть указан при объявлении связи с переменной. Тем не менее, в большинстве случаев, компилятор сможет определить тип переменной из контекста, что часто позволяет избавиться от бремени аннотирования кода.
Значения (как и литералы) могут быть привязаны к переменным, используя оператор let
.
Изменяемость
По умолчанию связывание переменных является неизменяемым, но с помощью модификатора mut
изменения можно разрешить.
Компилятор будет выводить подробные сообщения об ошибках, связанных с изменяемостью.
Область видимости и затенение
Связывание переменных происходит в локальной области видимости — они ограничены существованием внутри блока. Блок — это набор инструкций, заключённый между фигурными скобками {}
.
Кроме того, допускается затенение переменных.
Предварительное объявление
Можно сначала объявить связь с переменной, а инициализировать её позже. Однако такая форма используется редко, так как может привести к использованию неинициализированных переменных.
Компилятор запрещает использование неинициализированных переменных, так как это привело бы к неопределённому поведению.
Заморозка
Когда данные неизменяемо привязаны к тому же имени, они замораживаются. Замороженные данные не могут быть изменены до тех пор, пока неизменяемая привязка не выйдет из области видимости:
Типы
Rust предоставляет несколько механизмов изменения или определения примитивных и пользовательских типов:
- Приведение между примитивными типами
- Указание желаемого типа при помощи литералов
- Использование вывода типов
- Псевдонимы типов
Приведение типов
Rust не предусматривает неявного (принудительного) преобразования типов между примитивами. Однако явное преобразование типов (casting) можно выполнить, используя ключевое слово as
.
Правила, используемые для преобразования внутренних типов, такие же, как в языке C, за исключением тех случаев, когда преобразование типов в языке C вызывает неопределённое поведение. Поведение всех приведений между встроенными типами чётко определено в Rust.
Литералы
Числовые литералы могут быть обозначены добавлением типа в качестве суффикса. Например, чтобы указать, что литерал 42
должен иметь тип i32
, необходимо написать 42i32
.
Без суффикса тип литерала будет зависеть от того, как он используется. Если нет никаких ограничений, то компилятор будет использовать i32
для целочисленных литералов, а f64
— для литералов с плавающей точкой.
В предыдущем коде используются некоторые понятия, которых мы ранее не касались. Вот краткое пояснение для нетерпеливых читателей:
std::mem::size_of_val
является функцией, но вызывается с указанием полного пути. Код можно разделить на логические единицы, называемые модулями. В данном случае функция определена в модулеmem
, а модульmem
определён в контейнереstd
. Подробнее см. модули и контейнеры, а также соответствующую главу в книге
Вывод типов
Движок вывода типов весьма умён. Он делает куда больше, чем просто смотрит на тип r-value при инициализации. Он также следит за тем, как используется значение после инициализации, чтобы определить его тип. Вот расширенный пример вывода типов:
Не потребовалось никакой аннотации типов переменных, компилятор счастлив, как и программист!
Псевдонимы
Оператор type
используется, чтобы задать новое имя существующему типу. Имя типа должно быть в стиле UpperCamelCase
, иначе компилятор выдаст предупреждение. Исключением являются примитивные типы: usize
, f32
и другие.
Основное применение псевдонимов — сокращение размера кода: например, тип IoResult<T>
является псевдонимом типа Result<T, IoError>
.
Смотрите также:
Приведение типов
Примитивные типы могут быть сконвертированы в другие при помощи приведения типов.
Rust предоставляет преобразование между пользовательскими типами (такими как, struct
и enum
) через использование трейтов. Общие преобразования используют трейты From
и Into
. Однако есть и конкретные трейты для более частных случаев, например для конвертации String
.
From
и Into
Типажи From
и Into
связаны по своей сути, и это стало частью их реализации. Если вы можете конвертировать тип А
в тип В
, то будет легко предположить, что мы должны быть в состоянии конвертировать тип В
в тип А
.
From
Типаж From
позволяет типу определить, как он будет создаваться из другого типа, что предоставляет очень простой механизм конвертации между несколькими типами. Есть несколько реализаций этот типажа в стандартной библиотеке для преобразования примитивов и общих типов.
Для примера, мы можем легко конвертировать str
в String
Мы можем сделать нечто похожее для определения конвертации для нашего собственного типа.
Into
Трейт Into
является полной противоположностью трейта From
. Так что если вы реализовали для вашего типа трейт From
, то трейт Into
вызовет его при необходимости.
Использование типажа Into
обычно требует спецификации типа, в который мы собираемся конвертировать, так как компилятор чаще всего не может это вывести. Однако это небольшой компромисс, учитывая, что данную функциональность мы получаем бесплатно.
TryFrom
и TryInto
Как и From
и Into
, TryFrom
и TryInto
-
обобщённые типажи для конвертации между типами. Но в отличии
от From
/Into
, типажи
TryFrom
/TryInto
используются для
преобразований с ошибками и возвращают
Result
.
use std::convert::TryFrom;
use std::convert::TryInto;
#[derive(Debug, PartialEq)]
struct EvenNumber(i32);
impl TryFrom<i32> for EvenNumber {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNumber(value))
} else {
Err(())
}
}
}
fn main() {
// TryFrom
assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
assert_eq!(EvenNumber::try_from(5), Err(()));
// TryInto
let result: Result<EvenNumber, ()> = 8i32.try_into();
assert_eq!(result, Ok(EvenNumber(8)));
let result: Result<EvenNumber, ()> = 5i32.try_into();
assert_eq!(result, Err(()));
}
FromStr
и ToString
Конвертация в строку
Преобразовать любой тип в String
так же просто, как и реализовать для него типаж ToString
. Вместо того, чтобы делать это напрямую, вы должны реализовать типаж fmt::Display
, который автоматически предоставляет реализацию ToString
, а
также позволяет распечатать тип, как обсуждалось в секции print!
.
Парсинг строки
Один из наиболее общих типов конвертации - это преобразование строки в число. Идиоматический подход это сделать при помощи функции parse
и указания типа, в который будем преобразовывать, что можно сделать либо через выведение типа, либо при помощи 'turbofish'-синтаксиса. Оба варианта показаны в следующем примере.
Это преобразует строку в указанный тип при условии, что для этого типа реализован типаж FromStr
.
Он реализован для множества типов стандартной библиотеки.
Чтобы получить эту функциональность для пользовательского типа, надо просто реализовать для этого типа типаж FromStr
.
Выражения
Программы на языке Rust - это (в основном) набор последовательных операторов:
fn main() {
// оператор
// оператор
// оператор
}
Существует несколько типов операторов в Rust.
Наиболее распространённые - оператор связывания и выражение, заканчивающееся ;
:
fn main() {
// оператор связывания
let x = 5;
// оператор выражения
x;
x + 1;
15;
}
Блоки так же могут быть частью оператора выражения.
Они используются в качестве r-values при присваивании.
Последнее выражение в блоке будет присвоено l-value.
Однако, если последнее выражение в блоке оканчивается точкой с запятой,
в качестве значения будет возвращено ()
.
Управление потоком
Неотъемлемой частью любого языка программирования являются управляющие конструкции: if
/ else
, for
и другие. Давайте поговорим о них в Rust.
if/else
Ветвление с помощью if
-else
похоже на аналоги в других языка программирования. В отличие от многих из них, логическое условие не должно быть заключено в круглые скобки, а после каждого условия должен следовать блок. Условные операторы if
-else
являются выражениями, и все ветки должны возвращать значения одного и того же типа.
loop
Rust предоставляет ключевое слово loop
для обозначения бесконечного цикла.
Оператор break
используется, чтобы выйти из цикла в любое время, а оператор continue
используется ,чтобы пропустить оставшуюся часть цикла и начать новую итерацию.
Вложенность и метки
Можно прерывать выполнение внешних циклов с помощью break
или continue
,
когда речь заходит о вложенных циклах.
Для этого циклы должны быть обозначены метками вроде 'label
,
а метки должны быть переданы операторам break
или continue
.
Возврат из циклов
Одним из видов использования цикла loop
является повторение операции, пока
она не будет выполнена. Если операция возвращает значение, вам может
потребоваться передать его в другую часть кода: поместите его после break
,
и оно будет возвращено выражением loop
.
while
Ключевое слово while
используется для создания цикла, который будет выполняться, пока условие истинно.
Давайте напишем печально известный FizzBuzz, используя цикл while
.
Цикл for
for
и диапазоны
Конструкция for in
может быть использована для итерации по итераторам (Iterator
). Один из самых простых способов создать итератор это использовать диапазон значений a..b
. Это вернёт нам значения от a
(включительно) до b
(исключительно) за один шаг.
Давайте напишем FizzBuzz, используя for
вместо while
.
Также, может быть использован диапазон a..=b
, включающий оба конца. Код выше может быть записан следующим образом:
for
и итераторы
Конструкция for in
может взаимодействовать с итератором разными способами. Как обсуждается далее про типаж Iterator
, цикл for
применяет к предоставленной коллекции метод into_iter
, чтобы преобразовать её в итератор. Однако, это не единственный способ преобразования коллекции в итератор.
Каждый из методов into_iter
, iter
и iter_mut
преобразует коллекцию в итератор по своему, предоставляя разные отображения содержащихся данных.
iter
- эта функция заимствует каждый элемент коллекции на каждой итерации. Благодаря этому, он оставляет коллекцию нетронутой и доступной для использования после цикла.
fn main() {
let names = vec!["Bob", "Frank", "Ferris"];
for name in names.iter() {
match name {
&"Ferris" => println!("Программисты Rust вокруг нас!"),
_ => println!("Привет {}", name),
}
}
}
into_iter
- эта функция потребляет коллекцию так что на каждой итерации предоставляются данные. Коллекция больше не доступна для использования так как владение ею перешло в эту функцию.
fn main() {
let names = vec!["Bob", "Frank", "Ferris"];
for name in names.into_iter() {
match name {
"Ferris" => println!("Программисты Rust вокруг нас!"),
_ => println!("Привет {}", name),
}
}
}
iter_mut
- эта функция делает изменяемое заимствование каждого элемента коллекции, позволяя изменять коллекцию на месте.
fn main() {
let mut names = vec!["Bob", "Frank", "Ferris"];
for name in names.iter_mut() {
*name = match name {
&mut "Ferris" => "Программисты Rust вокруг нас!",
_ => "Привет",
}
}
println!("имена: {:?}", names);
}
В вышеуказанных кусках кода, обратите внимание на ветку match
, которая имеет ключевое отличие в зависимости от типа выполнения итераций. Разница в типе, конечно, подразумевает различные действия, которые могут быть выполнены.
Смотрите также:
match
Rust обеспечивает сопоставление с образцом с помощью ключевого слова match
, которое можно использовать похожим образом, как switch
в языке C. Срабатывает первая подходящая ветка, и все возможные значения должны быть перечислены.
Деструктуризация
Блок match
может деструктурировать элементы в различных формах.
Кортежи
Кортежи можно деструктурировать с помощью match
следующим образом:
Смотрите также:
Перечисления
Деструктуризация enum
происходит следующим образом:
Смотрите также:
#[allow(...)]
, цветовая модель и перечисления
Указатели и ссылки
Для указателей нужно отметить разницу между деструктуризацией и разыменованием, так как это разные понятия, которые используются по разному начиная с языков вроде C/C++.
- Разыменование использует
*
- Деструктуризация использует
&
,ref
иref mut
Смотрите также:
Структуры
Структуры
могут быть деструктурированы следующим образом:
Смотрите также:
Ограничители шаблонов
Внутри конструкции match
можно добавить ограничитель шаблонов
для фильтрации возможных вариантов.
Смотрите также:
Связывание
Косвенный доступ к переменной делает невозможным ветвление и использование
переменной без повторной привязки. match
предоставляет символ @
для привязки значения к имени:
Вы также можете использовать привязку для "деструктурирования"
вариантов enum
, таких как Option
:
Смотрите также:
if let
В некоторых случаях использование match
выглядит неуклюже. Например:
if let
намного компактнее и выразительнее для данного случая и, кроме того, позволяет рассмотреть различные варианты ошибок.
Точно так же, if let
может быть использован для сравнения любого значения перечисления:
Другое преимущество if let
в том, что он позволяет сопоставлять нам не параметризованные варианты перечисления. Это возможно даже если для перечисления не реализован и не выведен типаж PartialEq
. В некоторых случаях, if Foo::Bar == a
не скомпилируется, потому что экземпляры перечисления не могут быть равны. Однако, с if let
всё будет работать.
Хотите вызов? Исправьте следующий пример с использованием if let
:
Смотрите также:
while let
Так же, как иif let
, while let
может сделать неудобный match
более терпимым. Рассмотрим следующий пример, в котором мы увеличиваем значение i
:
Использование while let
делает этот пример намного приятнее:
Смотрите также:
Функции
Функции объявляются с помощью ключевого слова fn
. Их аргументы имеют явно заданный тип, как у переменных, и, если функция возвращает значение, возвращаемый тип должен быть указан после стрелки ->
.
Последнее выражение в функции будет использовано как возвращаемое значение. Так же можно использовать оператор return
, чтобы вернуть значение из функции раньше, даже из цикла или оператора if
.
Давайте перепишем FizzBuzz используя функции!
Связанные функции и Методы
Некоторые функции связаны с определённым типом. Они бывают двух видов: связанные функции и методы. Связанные функции — это функции, которые обычно определены для типа в целом, а методы — это связанные функции, которые вызываются для конкретного экземпляра типа.
Замыкания
Замыкания в Rust, так же называемые лямбда, это функции, которые замыкают своё окружение. Для примера, замыкание, которое захватывает значение переменной x:
|val| val + x
Синтаксис и возможности замыканий делают их очень удобными для использования "на лету". Использование замыканий похоже на использование функций. Однако, тип входных и возвращаемых значений может быть выведен, а название аргумента должно быть указано.
Другие характеристики замыканий включают в себя:
- использование
||
вместо()
для аргументов. - опциональное ограничения тела функции (
{}
) для одного выражения (в противном случае обязательно). - возможность захвата переменных за пределами окружения
Захват
Замыкания довольно гибкие и делают всё, что требуется, для работы с ними без дополнительных указаний. Это позволяет захватывать переменные гибко, перемещая их или заимствуя, в зависимости от ситуации. Замыкания могут захватывать переменные:
- по ссылке:
&T
- по изменяемой ссылке:
&mut T
- по значению:
T
Преимущественно, они захватывают переменные по ссылке, и используют другие способы только там, где это необходимо.
Использование move
перед вертикальными линиями позволяет получить владение над захваченными переменными:
Смотрите также:
Как входные параметры
В то время как замыкания Rust выбирают способ захвата переменных на лету, по большей части без указания типов, эта двусмысленность недопустима при написании функций. При использовании замыкания в качестве входного параметра, его тип должен быть указан с использованием одного из типажей. Вот они, в порядке уменьшения ограничений:
Fn
: замыкание захватывает по ссылке (&T
)FnMut
: замыкание захватывает по изменяемой ссылке (&mut T
)FnOnce
: замыкание захватывает по значению (T
)
Компилятор стремится захватывать переменные наименее ограничивающим способом.
Для примера, рассмотрим аргумент, указанный как FnOnce
. Это означает, что
замыкание может захватывать &T
, &mut T
, или T
, но компилятор в итоге
будет выбирать в зависимости от того, как захваченные переменные используются
в замыкании.
Это связано с тем, что если перемещение возможно, тогда любой тип заимствования
также должен быть возможен. Отметим, что обратное не верно. Если параметр
указан как Fn
, то захват переменных как &mut T
или T
недопустим.
В следующем примере попробуйте поменять местами использование Fn
, FnMut
, и
FnOnce
, чтобы увидеть результат:
Смотрите также:
std::mem::drop
, Fn
, FnMut
, Обобщения, where и FnOnce
Анонимность типов
Замыкания временно захватывают переменные из окружающих областей видимости. Имеет ли это какие-либо последствия? Конечно. Как видите, использование замыкания в аргументах функции требует обобщённых типов, из-за особенностей реализации замыканий:
Когда компилятор встречает определение замыкания, он неявно создаёт новую анонимную структуру для хранения захваченных переменных, тем временем реализуя функциональность для этого неизвестного типа, с помощью одного из типажей: Fn
, FnMut
, или FnOnce
. Этот тип присваивается переменной, которая хранится до самого вызова замыкания.
Так как этот новый тип заранее неизвестен, любое его использование в функции потребует обобщённых типов. Тем не менее, неограниченный параметр типа <T>
по прежнему будет неоднозначным и недопустимым. Таким образом, ограничение по одному из типажей: Fn
, FnMut
, или FnOnce
(которые он реализует) является достаточным для указания этого типа.
Смотрите также:
Подробный разбор, Fn
, FnMut
, и FnOnce
Входные функции
Так как замыкания могут использоваться в аргументах, вы можете ожидать, что то же самое можно сказать и про функции. И это действительно так! Если вы объявляете функцию, принимающую замыкание как аргумент, то любая функция, удовлетворяющая ограничениям типажа этого замыкания, может быть передана как аргумент.
Стоит отметить, что типажи Fn
, FnMut
и FnOnce
указывают, как замыкание захватывает переменные из своей области видимости.
Смотрите также:
Как выходные параметры
Замыкания могут выступать как в качестве входных параметров, так и в качестве выходных. Однако тип анонимных замыканий по определению не известен, из-за чего для их возврата нам придётся использовать impl Trait
.
Для возврата замыкания мы можем использовать такие трейты:
Fn
FnMut
FnOnce
Помимо этого, должно быть использовано ключевое слово move
, чтобы сигнализировать о том, что все переменные захватываются по значению. Это необходимо, так как любые захваченные по ссылке значения будут удалены после выхода из функции, оставляя недопустимые ссылки в замыкании.
Смотрите также:
Fn
, FnMut
, обобщения и impl Trait.
Примеры из библиотеки std
Этот раздел содержит несколько примеров использования замыканий из библиотеки std
.
Iterator::any
Iterator::any
- это функция, которая принимает итератор и возвращает true
,
если любой элемент удовлетворяет предикату. Иначе возвращает false
. Её
объявление:
pub trait Iterator {
// Тип, по которому выполняется итерирование
type Item;
// `any` принимает `&mut self`, что означает заимствование
// и изменение, но не поглощение `self`.
fn any<F>(&mut self, f: F) -> bool where
// `FnMut` означает, что любая захваченная переменная
// может быть изменена, но не поглощена. `Self::Item`
// указывает на захват аргументов замыкания по значению.
F: FnMut(Self::Item) -> bool {}
}
Смотрите также:
Поиск через итераторы
Iterator::find
- функция, которая перебирает значения итератора и ищет первое значение, удовлетворяющее условию. Если ни одно из значений не удовлетворяет условию, то возвращается None
. Её сигнатура:
pub trait Iterator {
// Тип, по которому выполняется итерирование.
type Item;
// `find` принимает `&mut self`, что означает, что вызывающий объект может быть заимствован
// и изменён, но не поглощён.
fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
// `FnMut` означает, что любая захваченная переменная может максимум быть
// изменена, но не поглощена. `&Self::Item`
//казывает на то, что аргументы замыкания берутся по ссылке.
P: FnMut(&Self::Item) -> bool;
}
Iterator::find
даёт ссылку на элемент. Но если вы хотите получить его индекс, используйте Iterator::position
.
Смотрите также:
std::iter::Iterator::rposition
Функции высшего порядка
Rust предоставляет Функции Высшего Порядка (ФВП). Это функции, которые получают на вход одну или несколько функций и/или выдают более полезную функцию. ФВП и ленивые итераторы придают языку Rust функциональный оттенок.
Option и Iterator реализуют значительную часть функций высшего порядка..
Расходящиеся функции
Расходящиеся функции никогда не возвращают результат. Они помечены с помощью !
, который является пустым типом.
В отличие от всех других типов, этот не может быть создан, потому что
набор всех возможных значений этого типа пуст. Обратите
внимание, что он отличается от типа ()
, который
имеет ровно одно возможное значение.
Например, эта функция имеет возвращаемое значение, хотя о нём нет информации.
fn some_fn() {
()
}
fn main() {
let a: () = some_fn();
println!("Эта функция возвращает управление и вы можете увидеть эту строку.")
}
В отличие от этой функции, которая никогда не вернёт элемент управления вызывающей стороне.
#![feature(never_type)]
fn main() {
let x: ! = panic!("Этот вызов никогда не вернёт управление.");
println!("вы никогда не увидете эту строку!");
}
Хотя это может показаться абстрактным понятием, на самом деле
это очень полезно и может пригодится. Главное преимущество
этого типа в том, что его можно привести к любому другому типу и
поэтому используется в местах, где требуется точный тип,
например в ветвях match
. Это позволяет нам писать
такой код:
fn main() {
fn sum_odd_numbers(up_to: u32) -> u32 {
let mut acc = 0;
for i in 0..up_to {
// Обратите внимание, что возвращаемый тип этого выражения match должен быть u32
// потому что такой тип в переменной "addition" .
let addition: u32 = match i%2 == 1 {
// Переменная "i" типа u32, что совершенно нормально.
true => i,
// С другой стороны выражение "continue" не возвращает
// u32, но это тоже нормально, потому что это тип не возвращающий управление,
// не нарушает требования к типу выражения match.
false => continue,
};
acc += addition;
}
acc
}
println!("Сумма нечётных чисел до 9 (исключая): {}", sum_odd_numbers(9));
}
Это также возвращаемый тип функций, которые содержат вечный
цикл (например, loop {}
), как сетевые серверы или
функции, завершающие процесс (например, exit()
).
Модули
Rust предоставляет мощную систему модулей, которая используется для иерархического разделения кода на логические единицы (модули) и управления видимостью (публичное и приватное) между ними.
Модуль - это набор элементов, таких как: функции, структуры, типажи, блоки реализации (impl
) и даже другие модули.
Видимость
По умолчанию, элементы модуля являются приватными, но это можно изменить добавив модификатор pub
. Только публичные элементы модуля могут быть доступны за пределами его области видимости.
Видимость структуры
Структуры имеют дополнительный уровень видимости благодаря полям. По умолчанию
видимость полей приватная, но это можно изменить с помощью модификатора pub
.
Приватная видимость имеет значение только при обращении к структуре извне модуля,
где она определена, и имеет целью скрытие информации (инкапсуляция).
Смотрите также:
Декларация use
Декларация use
используется, чтобы связать полный путь с новым именем, что упрощает доступ.
Вы можете использовать ключевое слово as
, что импортировать сущности и функции под другим именем:
super
и self
Ключевые слова super
и self
в пути используются,
чтобы устранить неоднозначность между используемыми элементами модуля.
Иерархия файлов
Модули могут быть отображены на иерархию файлов и директорий. Давайте разобьём пример с видимостью модулей на файлы:
$ tree .
.
├── my
│ ├── inaccessible.rs
│ └── nested.rs
├── my.rs
└── split.rs
В split.rs
:
// Это объявление запустит поиск файла с именем `my.rs` и
// вставит его содержимое в модуль с именем `my` под текущей областью видимости
mod my;
fn function() {
println!("вызвали `function()`");
}
fn main() {
my::function();
function();
my::indirect_access();
my::nested::function();
}
В my.rs
:
// Точно так же, `mod inaccessible` и `mod nested` обнаружат файлы `nested.rs`
// и `inaccessible.rs`, и затем вставят их здесь в соответствующие модули
mod inaccessible;
pub mod nested;
pub fn function() {
println!("вызвана `my::function()`");
}
fn private_function() {
println!("вызывает `my::private_function()`");
}
pub fn indirect_access() {
print!("вызвана `my::indirect_access()`, которая\n> ");
private_function();
}
В my/nested.rs
:
pub fn function() {
println!("вызвана `my::nested::function()`");
}
#[allow(dead_code)]
fn private_function() {
println!("вызвана `my::nested::private_function()`");
}
В my/inaccessible.rs
:
#[allow(dead_code)]
pub fn public_function() {
println!("вызвана `my::inaccessible::public_function()`");
}
Давайте проверим, что все ещё работает, как раньше:
$ rustc split.rs && ./split
вызвана `my::function()`
вызвана `function()`
вызвана `my::indirect_access()`, которая
> вызвана `my::private_function()`
вызвана `my::nested::function()`
Контейнеры
Контейнер (crate
) — единица компиляции в языке Rust.
Когда вызывается rustc some_file.rs
, some_file.rs
обрабатывается как файл контейнера.
Если в some_file.rs
есть декларация mod
, то содержимое модуля
будет объединено с файлом контейнера перед его компиляцией.
Другими словами, модули не собираются отдельно, собираются лишь контейнеры.
Контейнер может быть скомпилирован в исполняемый файл или в библиотеку.
По умолчанию rustc
создаёт из контейнера исполняемый файл.
Это поведение может быть изменено добавлением флага --crate-type
со значением lib
к rustc
.
Создание проекта
Давайте создадим библиотеку и посмотрим, как связать её с другим контейнером.
pub fn public_function() {
println!("called rary's `public_function()`");
}
fn private_function() {
println!("called rary's `private_function()`");
}
pub fn indirect_access() {
print!("called rary's `indirect_access()`, that\n> ");
private_function();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib
Библиотеки получают префикс «lib», и по умолчанию они получают имена в честь своего крейта, но это имя по умолчанию можно переопределить, передав параметр --crate-name
в rustc
или используя атрибут crate_name
.
extern crate
Чтобы связать контейнер с новой библиотекой, нужна декларация extern crate
.
Она не только свяжет библиотеку, но и импортирует все элементы в модуль
с тем же именем, что и сама библиотека.
Правила видимости, применимые к модулям, так же применимы и к библиотекам.
// Ссылка на `library`. Импортируем элементы, как модуль `rary`
extern crate rary;
fn main() {
rary::public_function();
// Ошибка! Функция `private_function` приватная
//rary::private_function();
rary::indirect_access();
}
# Где library.rlib путь к скомпилированной библиотеке. Предположим, что
# она находится в той же директории:
$ rustc executable.rs --extern rary=library.rlib && ./executable
вызвана `public_function()` библиотеки rary
вызвана `indirect_access()` библиотеки rary, и в ней
> вызвана `private_function()` библиотеки rary
Cargo
cargo
- официальный менеджер пакетов языка Rust. В нем много функций
для улучшения качества кода и увеличения скорости разработки! К ним относятся:
- Управление зависимостями и интеграция с crates.io (официальный реестр пакетов Rust)
- Осведомлённость о модульных тестах
- Осведомлённость о тестах производительности
Эта глава рассказывает об основах, но вы можете найти полное описание по адресу The Cargo Book.
Зависимости
Большинство программ зависят от нескольких библиотек. Если вам приходилось
когда-либо управлять зависимостями вручную, вы знаете, сколько боли это
может доставить. К счастью экосистема языка Rust содержит такой
инструмент как cargo
! cargo
может управлять зависимостями проекта.
Создание нового проекта на языке Rust:
# Исполняемый проект (проект с программой)
cargo new foo
# ИЛИ библиотека
cargo new --lib foo
Предположим, что для оставшейся части главы мы создали исполняемый проект, а не библиотеку, хотя обе концепции одинаковы.
После выполнения следующих команд вы увидите примерно следующую иерархию файлов:
foo
├── Cargo.toml
└── src
└── main.rs
main.rs
- это корневой файл вашего нового проекта.
Cargo.toml
- это конфигурационный файл этого проекта (foo
) для cargo
.
Если посмотрите внутрь файла, вы должны увидеть что-то вроде этого:
[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]
[dependencies]
Поле name
под [package]
определяет имя проекта. Оно используется
если Вы будете его публиковать на crates.io
(более подробно позже).
Также это имя выходного файла при компиляции.
Поле version
- это версия пакета, записанное с использованием системы
семантического версионирования.
Поле authors
содержит список авторов пакета и используется при публикации.
В секции [dependencies]
вы можете указывать зависимости вашего проекта.
Предположим, что вы хотите, чтобы ваша программа имела отличный CLI.
Вы можете найти много хороших пакетов на crates.io
(официальный реестр пакетов языка Rust). Один из популярных вариантов - clap. На момент написания этой статьи
самой последней опубликованной версией clap
является 2.27.1
.
Для добавления зависимости в ваш проект, вы можете просто добавить
соответствующую запись в Ваш Cargo.toml
под [dependencies]
: clap = "2.27.1"
.
И конечно, extern crate clap
в main.rs
. И это все! Вы можете начать
использовать clap
в вашей программе.
cargo
также поддерживает другие типы зависимостей. Здесь только
небольшие примеры:
[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]
[dependencies]
clap = "2.27.1" # из crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # из онлайн репозитория
bar = { path = "../bar" } # из локальной файловой системы
cargo
больше чем менеджер зависимостей. Все поддерживаемые возможности доступны
в спецификации формата Cargo.toml
.
Для сборки проекта Вы можете выполнить команду cargo build
в любой директории проекта
(включая поддиректории!). Также Вы можете выполнить cargo run
для сборки и запуска.
Обратите внимание, что эти команды разрешат все зависимости, скачают пакеты
если нужно, и соберут все, включая ваш пакет. (Обратите внимание, что он собирает только то,
что ещё не собрал, подобно make
).
Вот и все!
Соглашения
В предыдущей главе мы видели следующую иерархию каталогов:
foo
├── Cargo.toml
└── src
└── main.rs
Предположим, что мы хотим иметь два двоичных файла в одном проекте. Что тогда?
Оказывается, cargo
это поддерживает. Двоичный файл по умолчанию называется main.rs
,
это мы видели раньше, но вы можете добавить дополнительные файлы, поместив
их в каталог bin/
:
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── my_other_bin.rs
Чтобы сказать cargo
скомпилировать или запустить этот двоичный файл,
мы просто передаём cargo
флаг --bin my_other_bin
, где my_other_bin
это имя двоичного файла, с которым мы хотим работать.
Помимо дополнительных двоичных файлов, в cargo
есть
встроенная поддержка примеров, модульных тестов,
интеграционных тестов и тестов на производительность.
В следующей главе мы более подробно рассмотрим тесты.
Тестирование
Как мы знаем, тестирование является неотъемлемой частью любого программного обеспечения! Rust имеет первоклассную поддержку модульного и интеграционного тестирования (см. главу о тестировании в TRPL).
Из разделов тестирования, приведённых выше, мы знаем, как писать модульные и интеграционные тесты. Организационно, мы можем расположить модульные тесты в модулях, которые они тестируют, а интеграционные - в собственном каталоге tests/
:
foo
├── Cargo.toml
├── src
│ └── main.rs
└── tests
├── my_test.rs
└── my_other_test.rs
Каждый файл в каталоге tests
- это отдельный интеграционный тест.
cargo
естественно, обеспечивает простой способ запуска всех ваших тестов!
$ cargo test
Вы должны увидеть примерно такой результат:
$ cargo test
Compiling blah v0.1.0 (file:///nobackup/blah)
Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs
Running target/debug/deps/blah-d3b32b97275ec472
running 3 tests
test test_bar ... ok
test test_baz ... ok
test test_foo_bar ... ok
test test_foo ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Вы также можете запустить тесты, чьё имя соответствует шаблону:
$ cargo test test_foo
$ cargo test test_foo
Compiling blah v0.1.0 (file:///nobackup/blah)
Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs
Running target/debug/deps/blah-d3b32b97275ec472
running 2 tests
test test_foo ... ok
test test_foo_bar ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
Одно слово предостережения: Cargo может выполнять несколько тестов одновременно, поэтому убедитесь, что они не участвуют в гонках друг с другом. Например, если они все выводят в файл, вы должны заставить их записывать в разные файлы.
Скрипты сборки
Иногда обычной сборки, предоставляемой cargo, недостаточно. Возможно вашему крейту нужны некоторые предварительные условия, прежде чем он успешно скомпилируется, например кодогенерация или предварительно должен скомпилироваться какой-то нативный код. Для решения этой проблемы, мы имеем скрипты сборки, которые cargo может запустить.
Для добавления скрипта сборки в ваш пакет, вы можете указать его в Cargo.toml
следующим образом:
[package]
...
build = "build.rs"
Иначе по умолчанию cargo будет искать файл build.rs
в директории проекта.
Как использовать скрипт сборки
Скрипт сборки - это просто другой файл на Rust, который будет скомпилирован и вызван до компиляции чего-либо другого в пакете. Следовательно он может быть использовать для выполнения предварительных условий вашего крейта.
Через переменные окружения cargo предоставляет скрипту входные параметры описанные здесь, которые могут быть использованы.
Скрипт возвращает значения через stdout. Все напечатанные строки записываются в
target/debug/build/<pkg>/output
. Кроме того, строки с префиксом cargo:
напрямую интерпретируются cargo и следовательно могут быть использованы для объявления параметров для компиляции пакета.
Больше информации и примеров можно найти в спецификации cargo.
Атрибуты
Атрибуты - это метаданные, применяемые к какому-либо модулю, контейнеру или их элементу. Благодаря атрибутам можно:
- выполнить условную компиляцию кода
- задать имя крейта, его версию и тип (бинарный или библиотечный)
- отключить lints (предупреждения)
- включить возможности компилятора (макросы, глобальный импорт и другое.)
- связаться с внешней библиотекой
- пометить функции как юнит тесты
- пометить функции, которые будут частью бенчмарка
- использовать макросы, похожие на атрибуты
Когда атрибуты применяются ко всему контейнеру, их синтаксис будет #![crate_attribute]
, а когда они применяются к модулю или элементу модуля, их синтаксис станет #[item_attribute]
(обратите внимание на отсутствие !
).
Атрибуты могут принимать аргументы с различным синтаксисом:
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value)]
Атрибуты могут иметь несколько значений и могут быть разделены несколькими строками:
#[attribute(value, value2)]
#[attribute(value, value2, value3,
value4, value5)]
dead_code
Компилятор предоставляет проверку dead_code
, которая предупреждает о неиспользованных функциях. Атрибут dead_code можно использовать, чтобы отключить данную проверку.
Обратите внимание, что в реальных программах вы должны удалить неиспользуемый код. В этих примерах мы разрешаем оставить неиспользуемый код в некоторых местах — но это только для примера!
Контейнеры
Атрибут crate_type
используется, чтобы сказать компилятору, какой контейнер является библиотекой (и каким типом библиотеки), а какой — исполняемым файлом. Атрибут crate_name
используется для указания имени контейнера.
Однако важно отметить, что атрибуты crate_type
и create_name
не имеют значения при использовании пакетного менеджера Cargo. В виду того, что Cargo используется для большинства проектов на Rust, в реальном мире использование crate_type
и crate_name
достаточно ограничено.
Если мы используем атрибут crate_type
, то нам больше нет необходимости передавать компилятору флаг --crate-type
.
$ rustc lib.rs
$ ls lib*
library.rlib
cfg
Условная конфигурация возможна при помощи двух разных операторов:
- атрибута
cfg
:#[cfg(...)]
, который указывается на месте атрибута - макроса
cfg!
:cfg!(...)
, который можно использовать в условных выражениях
В то время как первый атрибут включает условную компиляцию, второй преобразуется в литералы true
или false
, позволяя сделать проверку во время исполнения. Оба варианта используют идентичный синтаксис для аргументов.
Смотрите также:
Собственные условия
Некоторые условия, например target_os
, предоставляются компилятором. Если мы хотим создать собственные условия, то их необходимо передать компилятору используя флаг --cfg
.
Попробуйте запустить без указания флага cfg
.
С указанием флага cfg
:
$ rustc --cfg some_condition custom.rs && ./custom
condition met!
Обобщения
Обобщения (Generics) позволяют абстрагировать типы и функциональность для более общих случаев. Они
чрезвычайно полезны для снижения дублирования кода, однако их синтаксис может быть
сравнительно сложным. А именно, использование обобщений требует особого
внимания при определении того, какими реальными типами в действительности могут заменяться обобщённые.
Наиболее простым и распространённым применением обобщений является обобщение параметров
типа.
Обобщить параметр типа можно используя угловые скобки и верхний верблюжий регистр: <Aaa, Bbb, ...>
. "Обобщённые параметры типа" обычно представлены как <T>
. В Rust, "обобщённым" также принято называть все, что может принимать один или более обобщённых параметров типа <T>
. Любой тип, указанный в качестве параметра обобщённого типа, является обобщённым, а всё остальное является конкретным (не обобщённым).
Например, объявление обобщённой функции foo
принимающей аргумент T
любого типа:
fn foo<T>(arg: T) { ... }
Поскольку T
был объявлен как обобщённый тип, посредством <T>
, он считается обобщённым когда используется как (arg: T)
. Это работает даже если T
был определён как структура
.
Пример ниже демонстрирует синтаксис в действии:
Смотрите также:
Функции
Тот же набор правил применяется и к функциям: тип T
становится
обобщённым, когда предшествует <T>
.
При использовании обобщённых функций, иногда требуется явно указывать тип данных параметров. Это может быть необходимо в случае, если вызываемая функция возвращает обобщённый тип или у компилятора недостаточно информации для вывода необходимого типа данных.
Вызов функции с явно указанными типами данных параметров выглядит так:
fun::<A, B, ...>()
.
Смотрите также:
Реализация
Подобно функциям, реализации требуют выполнения некоторых условий, чтобы оставаться обобщёнными.
Смотрите также:
Функции, возвращающие ссылки, impl
и struct
Типажи
Конечно типажи
тоже могут быть обобщёнными. Здесь мы определяем, тот
который повторно реализует типаж
Drop
как обобщённый метод, чтобы
удалить себя и входные данные.
Смотрите также:
Ограничения
При работе с обобщениями параметры типа часто должны использовать типажи
в качестве ограничений, чтобы определить какие функциональные возможности
реализует тип. Например, в следующем примере для печати используется
типаж Display
и поэтому требуется T
ограничить по Display
.
Это значит что T
должен реализовать Display
.
// Определим функцию `printer`, которая принимает обобщённый тип `T`,
// который должен реализовать типаж `Display`
fn printer<T: Display>(t: T) {
println!("{}", t);
}
Ограничение сужает список типов, допустимых к использованию. То есть:
struct S<T: Display>(T);
// Ошибка! `Vec<T>` не реализует `Display`. Эта
// специализация не удастся
let s = S(vec![1]);
Другой эффект ограничения заключается в том, что обобщённые экземпляры
имеют доступ к методам
типажей, указанных в ограничениях. Например:
Утверждения where
также могут использоваться для применения
ограничений в некоторых случаях, чтобы добавить выразительности.
Смотрите также:
Пример: пустые ограничения
Следствием того, как работают ограничения по трейту, является то, что даже если трейт не включает в себя какие-либо функциональные возможности, вы все равно можете использовать его в качестве ограничения. Примерами таких трейтов являются Eq
и Ord
из стандартной библиотеки.
Смотрите также:
std::cmp::Eq
, std::marker::Copy
и трейты
Множественные ограничения
Множественные ограничения по типажу могут быть применены с помощью +
.
Разные типы разделяются с помощью ,
.
Смотрите также:
Утверждения where
Ограничение типажа также может быть выражено с помощью утверждения where
непосредственно перед открытием {
, а не при первом упоминании типа.
Кроме того, утверждения where
могут применять ограничения типажей к
произвольным типам, а не только к параметрам типа.
В некоторых случаях утверждение where
является полезным:
- При указании обобщённых типов и ограничений типажей отдельно, код становится более ясным:
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}
// Выражение ограничений типажей через утверждение `where`
impl <A, D> MyTrait<A, D> for YourType where
A: TraitB + TraitC,
D: TraitE + TraitF {}
- Использование утверждения
where
более выразительно, чем использование обычного синтаксиса. В этом примереimpl
не может быть непосредственно выражен без утвержденияwhere
:
Смотрите также:
New Type идиома
Идиома newtype
гарантирует во время компиляции, что программе передаётся значение правильного типа.
Например, функция верификации возраста, которая проверяет возраст в годах должна получать значение типа Years
.
struct Years(i64);
struct Days(i64);
impl Years {
pub fn to_days(&self) -> Days {
Days(self.0 * 365)
}
}
impl Days {
/// truncates partial years
pub fn to_years(&self) -> Years {
Years(self.0 / 365)
}
}
fn old_enough(age: &Years) -> bool {
age.0 >= 18
}
fn main() {
let age = Years(5);
let age_days = age.to_days();
println!("Old enough {}", old_enough(&age));
println!("Old enough {}", old_enough(&age_days.to_years()));
// println!("Old enough {}", old_enough(&age_days));
}
Удалите комментарий с последнего println
, чтобы увидеть, что тип должен быть Years
.
Чтобы получить из newtype
-переменной значение базового типа, вы можете использовать кортежный синтаксис, как в примере:
struct Years(i64);
fn main() {
let years = Years(42);
let years_as_primitive: i64 = years.0;
}
Смотрите также:
Ассоциированные элементы
"Ассоциированные элементы" относятся к набору правил, касающихся элементов различных типов. Это расширение для обобщённых типажей, которое позволяет им определить новый элемент внутри себя.
Каждый такой элемент называется ассоциированным типом
и предоставляет упрощённый шаблон использования, когда
trait
является обобщённым для своего контейнера.
Смотрите также:
Проблема
trait
, являющийся обобщённым для своего контейнера, есть требование к спецификации типа - пользователи trait
должны специфицировать все обобщённые типы.
В примере ниже, trait
Contains
позволяет
использовать обобщённые типы A
и B
.
Затем этот типаж реализуется для типа Container
, в
котором A
и B
специфицированы, как
i32
, чтобы их можно было использовать в функции
fn difference()
.
Потому что Contains
имеет обобщение, мы должны
явно указать все обобщённые типы для
fn difference()
. На практике, мы хотим выразить
A
и B
через входной
параметр C
. Как вы можете увидеть в следующем
разделе, ассоциированные типы предоставляют именно эту
возможность.
Смотрите также:
Ассоциированные типы
Использование "ассоциированных типов" улучшает общую
читаемость кода через локальное перемещение внутренних типов в
типаж в качестве выходных типов. Синтаксис для
объявления trait
будет следующим:
Обратите внимание, что функции, использующие trait
Contains
больше не требуют указания A
или B
:
// Без использования ассоциированных типов
fn difference<A, B, C>(container: &C) -> i32 where
C: Contains<A, B> { ... }
// С использованием ассоциированных типов
fn difference<C: Contains>(container: &C) -> i32 { ... }
Давайте перепишем пример их предыдущего раздела с использованием ассоциированных типов:
PhantomData-параметры
Параметры фантомного типа - единственное, что не отображается во время выполнения, но проверяется статически (и только статически) во время компиляции.
Типы данных могут использовать дополнительные обобщённые типы в качестве параметров-маркеров или для выполнения проверки типов во время компиляции. Эти дополнительные параметры не сохраняют значения и не имеют поведения во время выполнения.
В следующем примере мы совместили std::marker::PhantomData и концепцию параметров фантомных типов для создания кортежей разных типов.
Смотрите также:
derive
, struct
и кортежные структуры
Пример: unit clarification
Полезный метод преобразования единиц измерения может быть
получен путём реализации типажа Add
с
параметром фантомного типа.
trait``Add
рассмотрен ниже:
// Эта конструкция будет навязывать: `Self + RHS = Output`
// где RHS по умолчанию Self, если иное не указано в реализации.
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
// `Output` должен быть `T<U>` так что `T<U> + T<U> = T<U>`.
impl<U> Add for T<U> {
type Output = T<U>;
...
}
Вся реализация:
Смотрите также:
Заимствование (&
), ограничения (X: Y
), перечисления, impl & self
,
перегрузка, ref
, типажи (X for Y
) и кортежные структуры.
Правила области видимости
Области видимости играют важную роль во владении, заимствовании и времени жизни. То есть, они указывают компилятору, когда заимствования действительны, когда ресурсы могут быть освобождены, и когда переменные создаются или уничтожаются.
RAII
Переменные в Rust не только держат данные в стеке, они также могут владеть
ресурсами; к примеру, Box<T>
владеет памятью в куче. Поскольку Rust строго
придерживается идиоме RAII, то когда объект выходит за зону видимости, вызывается
его деструктор, а ресурс, которым он владеет освобождается.
Такое поведение защищает от багов, связанных с утечкой ресурсов. Вам больше никогда не потребуется вручную освобождать память или же беспокоиться об её утечках! Небольшой пример:
Конечно, мы можем убедиться, что в нашей программе нет ошибок с памятью,
используя valgrind
:
$ rustc raii.rs && valgrind ./raii
==26873== Memcheck, a memory error detector
==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==26873== Command: ./raii
==26873==
==26873==
==26873== HEAP SUMMARY:
==26873== in use at exit: 0 bytes in 0 blocks
==26873== total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated
==26873==
==26873== All heap blocks were freed -- no leaks are possible
==26873==
==26873== For counts of detected and suppressed errors, rerun with: -v
==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)
Утечки отсутствуют!
Деструктор
Понятие деструктора в Rust обеспечивается через типаж Drop
.
Деструктор вызывается, когда ресурс выходит за пределы области видимости.
Этот типаж не требуется реализовать для каждого типа.
Реализовать его для вашего типа вам потребуется, только если
требуется своя логика при удалении экземпляра типа.
Выполните пример ниже, чтобы увидеть, как работает типаж Drop
. Когда переменная в функции main
выходит за пределы области действия,
будет вызван пользовательский деструктор.
Смотрите также:
Владение и перемещение
Поскольку переменные ответственны за освобождение своих ресурсов, ресурсы могут иметь лишь одного владельца. Это ограничение предотвращает возможность высвобождения ресурсов более одно раза. Обратите внимание, что не все переменные владеют своим ресурсом (например, ссылки).
При присваивании (let x = y
) или при передаче функции аргумента по значению (foo(x)
),
владение ресурсами передаётся. В языке Rust это называется перемещением.
После перемещения ресурсов, переменная, владевшая ресурсами ранее, не может быть использована. Это предотвращает создание висячих указателей.
Изменяемость
Изменяемость данных может быть изменена при передаче владения.
Заимствование
Большую часть времени мы хотим обращаться к данным без получения владения над
ними. Для этого Rust предоставляет механизм заимствования Вместо передачи
объектов по значению (T
), объекты могут быть переданы по ссылке (&T
).
Компилятор статически гарантирует, что ссылки всегда указывают на допустимые объекты посредством проверки заимствований. К примеру, исходный объект не может быть уничтожен, пока существуют ссылки на него.
Изменяемость
Изменяемые данные могут быть заимствованы с возможностью изменения при помощи &mut T
. Это называется изменяемая ссылка и даёт заимствующему возможность чтения и записи. В отличие от неё, &T
заимствует данные через неизменяемую ссылку и заимствующий может читать данные, но не может модифицировать их:
Смотрите также:
Алиасинг
Данные могут быть заимствованы без возможности изменения любое количество раз, но пока такое заимствование существует, оригинальные данные не могут быть заимствованы с возможностью изменения. С другой стороны, одновременно может быть только одно изменяемое заимствование. Исходные данные могут быть снова заимствованы только после того, как изменяемая ссылка выйдет из области видимости.
ref
паттерн
Когда мы используем сопоставление с образцом или
деструктурируем при помощи let
, можно
использовать ключевое слово ref
для получения
ссылки на поле структуры или кортежа. Пример ниже показывает
несколько случаев, когда это может быть полезно:
Времена жизни
Время жизни - это конструкция, которую компилятор (или более конкретно, его анализатор заимствований) использует, чтобы убедиться, что все заимствования действительны. В частности время жизни переменной начинается с момента её создания и заканчивается когда она уничтожается. Времена жизни и области видимости упоминаются часто вместе, но они не совпадают.
Возьмём, например, случай когда мы заимствуем переменную через &
.
Срок действия заимствования определяется местом его объявления.
В результате, заимствование действительно до тех пор,
пока оно не закончится или пока кредитор не будет уничтожен. Однако,
область заимствования определяется местом использования ссылки.
В следующем примере и в остальной части этого раздела мы увидим, как времена жизни связаны с областями видимости, а также как они различаются.
Обратите внимание, что для меток времени жизни не назначаются имена или типы. Это ограничивает то, как время жизни будет использоваться, как мы увидим далее.
Явное аннотирование
Анализатор заимствований использует явные аннотации времён жизни для определения того, как долго ссылки будут действительны. В случаях, когда времена жизни не скрыты1, Rust требует их явного аннотирования, чтобы определить какое у ссылки должно быть время жизни. Для явного аннотирования времени жизни используется синтаксис с символом апострофа, как тут:
foo<'a>
// `foo` имеет параметр времени жизни `'a`
Подобно замыканиям, явное использование времён жизни требует обобщённого параметра. Кроме того, такой синтаксис показывает, что время жизни foo
не может превышать 'a
. Явная аннотация для типа имеет форму &'a T
, где 'a
уже задана.
В случаях со множественными временами жизни, синтаксис будет подобен следующему:
foo<'a, 'b>
// `foo` имеет параметры времён жизни `'a` и `'b`
В данном случае, время жизни foo
не может превышать ни 'a
, ни 'b
.
Рассмотрим следующий пример, в котором используется явная аннотация времён жизни:
сокрытие позволяет скрыть аннотации времён жизни, но они всё же присутствуют.
Смотрите также:
Функции
Сигнатуры функции с указанием времени жизни имеют некоторые ограничения:
- любая ссылка должна иметь аннотированное время жизни
- любая возвращаемая ссылка должна иметь то же время жизни, что входящая ссылка или
static
.
Кроме того, обратите внимание, что возврат ссылок из функции, которая не имеет ссылок во входных аргументах, запрещён, если он приведёт к возвращению ссылок на недопустимые данные. В следующем примере показаны некоторые действительные формы функции со временем жизни:
Смотрите также:
Методы
Методы аннотируются аналогично функциям:
Смотрите также:
Структуры
Аннотирование времени жизни в структурах аналогично функциям:
Смотрите также:
Типажи
Аннотирование времён жизни для методов типажей в основном
похоже на аннотирование в функциях. Обратите внимание, что
impl
также может иметь аннотацию времени жизни.
Смотрите также:
Ограничения
Так же, как и обобщённые типы, времена жизни (обобщённое само по себе) могут быть ограничены.
Для них знак :
имеет немного другое значение,
но знак +
такое же. Прочитайте следующую заметку:
T: 'a
: Все ссылки вT
должны пережить время жизни'a
.T: Trait + 'a
: ТипT
должен реализовать типажTrait
и все ссылки наT
должны пережить'a
.
Пример ниже демонстрирует синтаксис в действии и использует его после ключевого слова where
:
Смотрите также:
Обобщения, ограничения в обобщениях и множественные ограничения в обобщениях
Приведение (coercion)
Длинное время жизни может быть приведено к короткому, благодаря чему всё работает нормально внутри области видимости, хотя кажется, что не должно. Это достигается за счёт того что компилятор Rust выполняет приведение времён жизни и за счёт объявления разницы между ними разницы:
Static
В Rust есть несколько зарезервированных имён времени жизни. Одно из них — 'static
. Вы можете столкнуться с ним в двух случаях:
Оба они похожи, но немного отличаются, что часто вызывает путаницу при изучении Rust. Вот несколько примеров для каждой ситуации:
Время жизни ссылки
'static
как время жизни ссылки означает, что данные, на которые указывает ссылка, живут в течение всего времени жизни работающей программы. В тоже время, этот срок может быть сокращён принудительно.
Есть два способа создать переменную с временем жизни 'static
, и оба они лежат в области "только для чтения" бинарного файла:
- Создание константы с ключевым словом
static
. - Создание
строкового
литерала, который имеет тип &'static str.
Рассмотрим следующий пример, который показывает оба метода:
Ограничение типажа
Как ограничение типажа, это означает, что тип не содержит нестатических ссылок. Например. получатель может хранить тип столько, сколько захочет, и тип никогда не станет недействительным, пока получатель его не удалит.
Важно понимать, что это означает, что любые владеющие данные всегда проходят проверку на ограничение времени жизни 'static
, но ссылка на эти владеющие данные обычно не проходит:
Компилятор скажет вам:
error[E0597]: `i` does not live long enough
--> src/lib.rs:15:15
|
15 | print_it(&i);
| ---------^^--
| | |
| | borrowed value does not live long enough
| argument requires that `i` is borrowed for `'static`
16 | }
| - `i` dropped here while still borrowed
Смотрите также:
Сокрытие
Некоторые шаблоны времён жизни достаточно общие и поэтому анализатор заимствований может позволить вам опустить их чтобы ускорить написание кода и увеличить его читаемость. Это известно как сокрытие времён жизни. Сокрытие появилось в Rust, исключительно из-за того, что они применяются к общим шаблонам.
Следующий код показывает несколько примеров сокрытия. Для более полного описания сокрытия, обратитесь к главе про [a0}сокрытие времён жизни в TRPL.
Смотрите также:
Типажи (трейты)
Типаж (trait, трейт)
- это набор методов, определённых для неизвестного типа: Self
. Они могут получать доступ к другим методам, которые были объявлены в том же типаже.
Типажи могут быть реализованы для любых типов данных. В примере ниже, мы определили группу методов Animal
. Типаж Animal
реализован для типа данных Sheep
, что позволяет использовать методы из Animal
внутри Sheep
.
Атрибут Derive
Компилятор способен предоставить основные реализации для некоторых типажей
с помощью атрибута #[derive]
. Эти типажи могут быть
реализованы вручную, если необходимо более сложное поведение.
Ниже приводится список выводимых типажей:
- Типажи сравнения:
Eq
,PartialEq
,Ord
,PartialOrd
Clone
, для созданияT
из&T
с помощью копии.Copy
, чтобы создать тип семантикой копирования, вместо семантики перемещения.Hash
, чтобы вычислить хеш из&T
.Default
, чтобы создать пустой экземпляр типа данных.Debug
, чтобы отформатировать значение с помощью{:?}
.
Смотрите также:
Возврат типажа с dyn
Компилятору Rust нужно знать сколько места занимает результат
каждой функции. Это обозначает, что все ваши функции имеют
конкретный тип результата. В отличие от других языком, если у вас
есть типаж, например Animal
, то вы не можете
написать функцию, которая вернёт Animal
, по той
причине, что разные реализации этого типажа будут занимать
разное количество памяти.
Однако есть простой обходной путь. Вместо не посредственного
возврата типажа-объекта, наши функции могут возвращать
Box
, который содержит некоторую
реализацию Animal
. box
- это просто
ссылка на какую-то память в куче. Так как размер ссылки известен
статически и компилятор может гарантировать, что она указывает
на аллоцированную в куче реализацию, мы можем вернуть типаж
из нашей функции!
Rust пытается быть предельно явным, когда он выделяет память в
куче. Так что если ваша функция возвращает
указатель-на-типаж-в-куче, вы должны дописать к возвращаемому
типу ключевое слово dyn
, например
Box<dyn Animal>
.
Перегрузка операторов
В Rust, множество операторов могут быть перегружены с помощью типажей. То есть, некоторые
операторы могут использоваться для выполнения различных задач на основе вводимых аргументов.
Это возможно, потому что операторы являются синтаксическим сахаром для вызова методов. Например,
оператор +
в a + b
вызывает метод add
(как в a.add(b)
).
Метод add
является частью типажа Add
.
Следовательно, оператор +
могут использовать все, кто реализуют типаж Add
.
Список типажей, таких как Add
, которые перегружают операторы, доступен здесь.
Смотрите также:
Типаж Drop
Типаж Drop
имеет только один метод: drop
, который вызывается автоматически,
когда объект выходит из области видимости. Основное применение типажа Drop
заключается в том, чтобы освободить ресурсы, которыми владеет экземпляр реализации.
Box
, Vec
, String
, File
, и Process
- это некоторые примеры типов, которые
реализуют типаж Drop
для освобождения ресурсов. Типаж Drop
также может быть
реализован вручную для любых индивидуальных типов данных.
В следующем примере мы добавим вывод в консоль к функции drop
, чтобы было видно,
когда она вызывается.
Итераторы
Типаж Iterator
используется для итерирования
по коллекциям, таким как массивы.
Типаж требует определить метод next
, для получения следующего элемента.
Данный метод в блоке impl
может быть определён
вручную или автоматически (как в массивах и диапазонах).
Для удобства использования, например в цикле for
, некоторые коллекции
превращаются в итераторы с помощью метода .into_iterator()
.
impl Trait
Если ваша функция возвращает тип, реализующий MyTrait
, вы можете записать возвращаемый тип как -> impl MyTrait
. Это может достаточно сильно упростить сигнатуру вашей функции!
Что более важно, некоторые типы в Rust не могут быть записаны. Например, каждое замыкание имеет свой собственный безымянный тип. До появления синтаксиса impl Trait
, чтобы вернуть замыкание, вы должны были аллоцировать её в куче. Но теперь вы можете сделать это всё статически, например так:
Вы также можете использовать impl Trait
для возврата итератора, который использует замыкания map
или filter
! Это упрощает использование map
и filter
. Из-за того, что замыкание не имеет имени, вы не можете явно записать возвращаемый тип для функции, возвращающей итератор с замыканием. Но с impl Trait
вы можете сделать это:
Типаж Clone
При работе с ресурсами, стандартным поведением является передача их (ресурсов) в ходе выполнения или вызов функции. Однако, иногда нам нужно также объявить копию ресурса.
Типаж Clone
помогает нам сделать именно это. Чаще всего, мы можем использовать метод .clone()
объявленный типажом Clone
.
Супертрейты
В Rust нет "наследования", но вы можете объявить трейт, который будет надмножеством для другого. Например:
Смотрите также:
Глава "The Rust Programming Language" о супертрейтах
Устранение неоднозначности в перекрывающихся трейтах
Тип может реализовывать много разных трейтов. Что если два трейта будут требовать метод с одним и тем же именем? например, много трейтов могут иметь метод get()
, которые так же могут иметь разные возвращаемые типы!
Хорошие новости: благодаря тому, что каждая реализация трейта имеет собственный impl
-блок, становится яснее для какого трейта мы написали метод get
.
А что будет, когда придёт время вызвать эти методы? Чтобы устранить неоднозначность, мы можем использовать полное имя метода (Fully Qualified Syntax).
Смотрите также:
Глава "The Rust Programming Language" о полном имени методов (Fully Qualified syntax)
macro_rules!
Rust предоставляет мощную систему макросов, которая позволяет
использовать метапрограммирование. Как вы могли видеть в предыдущих главах,
макросы выглядят как функции, но их имя заканчивается восклицательным знаком (!
).
Вместо вызова функции, макросы расширяются в исходный код, который впоследствии
компилируется с остальной частью программы.
Однако, в отличие от макросов на C и других языках, макросы Rust расширяются
в абстрактные синтаксические деревья, а не в подстановку строк,
поэтому Вы не получаете неожиданных ошибок приоритета операций.
Макросы создаются с помощью макроса macro_rules!
Так почему же макросы полезны?
-
Не повторяйтесь. Есть много случаев, когда вам может понадобиться подобная функциональность в нескольких местах, но с различными типами. Чаще всего написание макроса - это полезный способ избежать повторения кода. (Подробнее об этом позже)
-
Предметно-ориентированные языки. Макросы позволяют определить специальный синтаксис для конкретной цели. (Подробнее об этом позже)
-
Вариативные интерфейсы. Иногда вы хотите объявить интерфейс, принимающий переменное число аргументов. Например,
println!
, принимающий такое же число аргументов, сколько объявлено в строке с форматом. (Подробнее об этом позже)
Синтаксис
В следующем подразделе мы посмотрим как в Rust объявить макрос. Есть три основные идеи:
Указатели
Аргументы макроса имеют префикс знака доллара $
и тип аннотируется
с помощью указателей фрагмента:
Это список всех указателей:
block
expr
используют для обозначения выраженийident
используют для обозначения имени переменной/функцииitem
literal
используется для литеральных константpat
(образец)path
stmt
(единственный оператор)tt
(единственное дерево лексем)ty
(тип)vis
(спецификатор видимости)
Полный список указателей, вы можете увидеть в Rust Reference.
Перегрузка
Макросы могут быть перегружены, принимая различные комбинации аргументов.
В этом плане, macro_rules!
может работать аналогично блоку сопоставления (match):
Повторение
Макросы могут использовать знак +
в списке аргументов, чтобы указать, какие аргументы
могут повторяться хоть один раз, или знак *
, чтобы указать, какие аргументы могут
повторяться ноль или несколько раз.
В следующем примере, шаблон, окружённый $(...),+
будет
сопоставлять одно или несколько выражений, разделённых запятыми.
Также обратите внимание, что точка с запятой является
необязательной в последнем случае.
DRY (Не повторяйся)
Макросы позволяют писать DRY код, путём разделения общих частей функций
и/или набор тестов. Вот пример, который реализует и тестирует операторы
+=
, *=
и -=
на Vec<T>
:
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Domain Specific Languages (DSLs)
DSL - это мини язык, встроенный в макросы Rust. Это полностью допустимый код на Rust, так как система макросов разворачивается в нормальные конструкции, но выглядит как маленький язык. Это позволяет вам определять краткий или интуитивный синтаксис для некоторой функциональности (в пределах границ).
Предположим, я хочу определить небольшое API для калькулятора. Я хотел бы предоставить выражение и вывести результат в консоль.
Вывод:
1 + 2 = 3
(1 + 2) * (3 / 4) = 0
Это очень простой пример, но можно разработать и гораздо более
сложные интерфейсы, такие как lazy_static
или clap
.
Также обратите внимание на две пары скобок в макросе. Внешняя
пара является частью синтаксиса macro_rules!
, в
дополнение к ()
или []
.
Вариативные интерфейсы
Интерфейсы с переменным числом параметров (вариативные интерфейсы) принимают произвольное число
аргументов. Например, println!
может принимать
произвольное число аргументов, как определено в формате строки.
Мы можем расширить наш макрос calculate!
из
предыдущей главы, чтобы он имел вариативный интерфейс:
Вывод:
1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7
Обработка ошибок
Обработка ошибок - это процесс управления возможными сбоями. Например если при чтении файла произошла ошибка, а мы будем и дальше использовать данные, выданные плохим чтением, это может привести к проблемам. Обнаружение и явное управление такими ошибками убережёт остальные части программы от различных неожиданностей.
В Rust есть разные пути работы с ошибками, которые описаны в следующих подразделах. Они все имеют чуть большие или чуть меньшие отличия, а также разные сценарии использования. Как правило:
Явный panic
в основном используется для тестирования и работы с невосстановимыми ошибками. При прототипировании его можно применять, например, когда мы работаем с ещё не реализованными функциями, но в этом случае лучше использовать более говорящее unimplemented
. В тестах panic
- разумный способ явного оповещения об ошибке.
Тип Option
предназначен для случаев, когда значение не обязательно или когда отсутствие значения не является ошибкой. Например, корневые директории /
и C:
не имеют родителя. При работе с Option
, для прототипирования и случаев, когда мы точно знаем, что значение должно быть, отлично подходит unwrap
. Однако более полезен expect
, так как он позволяет указать сообщение об ошибке на случай, если что-то пойдёт не так.
Когда есть вероятность, что что-то пойдёт не так и вызывающая сторона должна как-то обработать эту ситуацию, используйте Result
. Вы также можете использовать unwrap
и expect
(пожалуйста, не делайте этого, если вы не пишете тест или не прототипируете).
Для более полного изучения обработки ошибок, обратитесь к соответствующему разделу в книге.
panic
Самый простой механизм обработки ошибок, с которым мы познакомимся – это panic
. Он печатает сообщение с ошибкой, начинает процедуру раскрутки стека и, чаще всего, завершает программу. В данном примере мы явно вызываем panic
в случае ошибки:
Option
и unwrap
В последнем примере мы показали, что можем вызвать сбой программы по своему желанию. Мы сказали нашей программе вызвать panic
, если мы выпьем сладкий лимонад. Но что, если ожидаем какой-то напиток, но не получаем его? Этот случай тоже плохой, так что и он должен быть обработан!
Мы могли бы сравнить значение с пустой строкой (""
) так же, как мы сделали это с лимонадом. Поскольку мы используем Rust, пусть компилятор сам укажет нам случаи, когда напитка нет.
Перечисление (enum
) из стандартной библиотеки (std
), называющееся Option<T>
, используется, когда значение может отсутствовать. Оно может находиться в одном из двух состояний:
Some(T)
: элемент типаT
найденNone
: элемент не найден
Эти случаи могут быть или явно обработаны с помощью match
, или неявно с unwrap
. Неявная обработка либо вернёт внутренний элемент, либо вызовет panic
.
Обратите внимание, что можно вручную настроить сообщение выдаваемое при панике с помощью expect, в противном случае unwrap
оставляет нам менее понятный вывод, чем явная обработка. В следующем примере явная обработка даёт более контролируемый результат, при этом сохраняется возможность паниковать, если это необходимо.
Разворачивание Option
с ?
Вы можете развернуть Option
с использованием match
, но часто проще бывает использовать
оператор?
. Если x
- Option
, то выражениеx?
вернёт
значение переменной, если x
- Some
, в противном же случае оно завершит выполнение текущей функции и вернёт None
.
Чтобы ваш код был более читаемым, вы можете составить цепочку из нескольких ?
.
Комбинаторы: map
match
- возможный метод для работы с Option
.
Однако постоянное его использование может быть утомительным,
особенно с операциями, которые получают только проверенные
данные.
В этом случае можно использовать комбинаторы, которые
позволяют управлять потоком выполнения в модульном режиме.
Option
имеет встроенный метод, зовущийся map()
, комбинатор для простого преобразования Some -> Some
и None -> None
. Для большей гибкости, несколько вызовов map()
могут быть связаны друг с другом в цепочку.
В следующем примере, process()
заменяет все предшествующие ей функции, оставаясь, при этом, компактной:
Смотрите также:
Замыкания, Option
, Option::map()
Комбинаторы: and_then
map()
описывался как использование цепочек
функций для упрощения выражения match
.
Однако использование map()
с функцией, которая в
качестве результата возвращает Option<T>
приводит к вложенности Option<Option<T>>
. Такая цепочка из множества вызовов в итоге может
запутать. Вот тут и появляется другой комбинатор, зовущийся
and_then()
, известный в некоторых языках как
flatmap
.
and_then()
запускает функцию, которая на вход получает обёрнутое значение, а возвращает результирующее
значение. Если Option
равен None
, то
он вернёт None
.
В следующем примере, cookable_v2()
возвращает
Option<Food>
. Используя map()
вместо and_then()
мы получим
Option<Option<Food>>
, который является
не правильным типом для eat()
.
Смотрите также:
Замыкания, Option
, и Option::and_then()
Result
Result
является более богатой версией типа Option
, тип который описывает возможную ошибку вместо возможного её отсутствия.
Result<T, E>
имеет два возможных значения:
Ok(T)
: Значение типаT
Err(E)
: Ошибка обработки элемента, типаE
По соглашению, ожидаемый результат Ok
, тогда как не ожидаемый - Err
.
Подобно Option
, Result
имеет множество ассоциированных с ним методов. Например, unwrap()
или возвращает T
, или вызывает panic
.
Для обработки результата у Result
существует множество комбинаторов, которые совпадают с комбинаторами Option
.
При работе с Rust вы, скорее всего, столкнётесь с методами, которые возвращают тип Result
, например метод parse()
. Не всегда
можно разобрать строку в другой тип, поэтому parse()
возвращает Result
, указывающий на возможный сбой.
Давайте посмотрим, что происходит, когда мы успешно и безуспешно попытаемся преобразовать строку с помощью parse()
:
При неудаче, parse()
оставляет на с ошибкой, с
которой unwrap()
вызывает panic
. Дополнительно, panic
завершает нашу программу и
предоставляет неприятное сообщение об ошибке.
Для повышения качества наших сообщений об ошибка, мы должны более явно указать возвращаемый тип и рассмотреть возможной явной обработки ошибок.
Использование Result
в main
Также Result
может быть возвращаемым типом функции main
, если это указано явно. Обычно функция main
имеют следующую форму:
fn main() {
println!("Hello World!");
}
Однако main
также может и возвращать тип Result
. Если ошибка происходит в пределах функции main
, то она возвращает код ошибки и выводит отладочное представление ошибки (используя типаж Debug
). Следующий пример показывает такой сценарий и затрагивает аспекты, описанные в последующем разделе.
map
для Result
Паника в предыдущем примере делает код ненадёжным. Обычно, мы хотим вернуть ошибку вызывающей стороне, чтобы уже она решала, как с ней поступить.
Первое, что нам нужно знать - это с каким типом ошибки мы
работаем. Для определения типа Err
, мы посмотрим
на parse()
, реализованную с типажом
FromStr
для i32
.
В результате, тип Err
указан как
ParseIntError
.
В примере ниже, простой match
делает код более громоздким.
К счастью, map
, and_then
многие
другие комбинаторы Option
также реализованы и
для Result
.
Документация по Result
содержит полный
их список.
Псевдонимы для Result
Как насчёт случая, когда мы хотим использовать конкретный тип Result
много раз? Напомним, что Rust позволяет нам создавать псевдонимы. Мы можем удобно объявить псевдоним для конкретного Result
.
Особенно полезным может быть создание псевдонимов на уровне модулей. Ошибки, найденные в конкретном модуле, часто имеют один и тот же тип Err
, поэтому один псевдоним может лаконично объявить все ассоциированные Results
. Это настолько полезно, что библиотека std
обеспечивает даже один: io::Result
!
Ниже приведён краткий пример для демонстрации синтаксиса:
Смотрите также:
Ранний выход
В предыдущем примере мы явно обработали ошибки при помощи комбинаторов. Другой способ сделать это - использовать комбинацию выражения match
и раннего выхода.
Таким образом мы просто можем остановить работу функции и вернуть ошибку, если она произошла. Для некоторых, такой код будет легче в чтении и написании. Посмотрите код из предыдущего примера, переписанный с использованием раннего выхода:
На данный момент, мы изучили обработку ошибок при помощи комбинаторов и раннего выхода. Мы хотим избежать паники, но явная обработка всех ошибок достаточно громоздка.
В следующем разделе, мы познакомимся с ?
для случаев, где нам просто хотим сделать unwrap
без возможности вызова panic
.
Представляем: ?
Иногда мы хотим получить простоту unwrap
, но без
panic
. До текущего момента unwrap
заставлял нас делать всё больше и больше, в то время как мы
хотели только извлечь переменную. Для этих целей был
введён ?
.
При обнаружении Err
, можно выполнить два действия:
panic!
, который мы решили по возможности избегатьreturn
так как возвратErr
говорит о том, что мы её не обрабатывали
?
почти1 эквивалентен
unwrap
, который при Err
делает
return
вместо panic
. Давайте
посмотрим как мы можем упростить наш пример, использующий
комбинаторы:
Макрос try!
До появления ?
, аналогичная функциональность
была доступна через макрос try!
.
Сейчас рекомендуется использовать оператор ?
, но
вы до сих пор можете найти try!
, когда
просматриваете старый код. Функция multiply
из
предыдущего примера с использованием try!
будет
выглядеть следующим образом:
Посмотрите главу "Другие способы использования ?
" для большей информации.
Несколько типов ошибок
Предыдущие примеры всегда были очень удобны: Result
взаимодействовали с другими Result
, а Option
- с другими Option
.
Иногда Option
необходимо взаимодействовать с
Result
, или Result<T, Error1>
с
Result<T, Error2>
. В этих случаях, нам нужно
управлять этими разными типами ошибок таким образом, чтобы
можно было их компоновать и легко взаимодействовать с ними.
В следующем коде, два варианта unwrap
генерируют разные типы ошибок. Vec::first
возвращает Option
, в то время как
parse::<i32>
возвращает
Result<i32, ParseIntError>
:
В следующих главах мы рассмотрим различные стратегии обработки этих типов проблем.
Извлечение Result
из Option
Наиболее простой способ обработки ошибок разных типов - это встраивание их друг в друга.
Бывает, мы хотим приостановить работу при ошибке (как при
помощи оператора ?
), но продолжать
работать, если Option
None
. Есть
пара комбинаторов, которые поменяют местами
Result
и Option
.
Объявление типа ошибки
Иногда для упрощения кода необходимо скрыть все типы ошибок за какой-то одной ошибкой. Мы скроем их за пользовательской ошибкой.
Rust позволяет нам определить наш собственный тип ошибок. В общем случае "хороший" тип ошибки должен:
- Представлять разные ошибки с таким же типом
- Предоставлять хорошее сообщение об ошибке пользователю
- Легко сравниваться с другими типами
- Хорошо:
Err(EmptyVec)
- Плохо:
Err("Пожалуйста, используйте вектор хотя бы с одним элементом".to_owned())
- Хорошо:
- Содержать информацию об ошибке
- Хорошо:
Err(BadChar(c, position))
- Плохо:
Err("+ не может быть использован в данном месте".to_owned())
- Хорошо:
- Хорошо сочетаться с другими ошибками
Упаковка ошибок (Box
)
Чтобы написать простой код и при этом использовать оригинальные ошибки, необходимо упаковать (Box
) их. Минусом данного способа является то, что тип ошибок известен только во время выполнения программы, а не определён статически.
Стандартная библиотека помогает упаковывать наши ошибки. Это достигается за счёт того, что для Box
реализована конвертация из любого типа, реализующего типаж Error
, в типаж-объект Box<Error>
через From
.
Смотрите также:
Динамическая диспетчеризация и типаж Error
Другие способы использования ?
Вы обратили внимание, что сразу же после вызова parse
, мы в map_err
упаковали ошибку из библиотеки?
.and_then(|s| s.parse::<i32>()
.map_err(|e| e.into())
Это простая и распространённая операция и было бы не плохо, если бы мы могли её опустить. Но из-за того, что and_then
недостаточно гибок, мы не можем этого сделать. Однако, тут нам может помочь ?
.
Ранее ?
был рассмотрен как unwrap
или return Err(err)
. По большей части это правда: на самом деле ?
означает unwrap
или return Err(From::from(err))
. Поскольку From::from
используется для преобразования между разными типами, применение ?
к ошибке автоматически преобразует её в возвращаемый тип (при условии, что исходная ошибка может быть в него преобразована).
Теперь мы перепишем наш предыдущий пример с использованием ?
. В результате у нас пропал map_err
, так как для нашего типа реализован From::from
:
Сейчас код выглядит довольно чисто. По сравнению с panic
, это похоже на замену вызова unwrap
на ?
за исключением того, что возвращаемый тип будет Result
. В результате, он может быть обработан уровнем выше.
Смотрите также:
From::from
и ?
Оборачивание ошибок
Альтернативой упаковке ошибок является оборачивание их в ваш собственный тип.
Это добавляет чуть больше шаблонного кода для обработки ошибок и может быть не нужно всем приложениям. Есть библиотеки, которые могут избавить вас от написания этого шаблонного кода.
Смотрите также:
Итерирование по Result
При работе метода Iter::map
может случиться ошибка, например:
Давайте рассмотрим стратегии обработки этого.
Игнорирование неудачных элементов с filter_map()
filter_map
вызывает функцию и отфильтровывает результаты, вернувшие None
.
Сбой всей операции с collect()
Result
реализует FromIter
так что вектор из результатов (Vec<Result<T, E>>
)
может быть преобразован в результат с вектором (Result<Vec<T>, E>
). Если будет найдена хотя бы одна Result::Err
, итерирование завершится.
Та же самая техника может использоваться с Option
.
Сбор всех корректных значений и ошибок с помощью partition()
Если вы посмотрите на результаты работы, вы заметите, что они всё ещё обёрнуты в Result
. Потребуется немного больше шаблонного кода, чтобы получить нужный результат.
Типы стандартной библиотеки
Стандартная библиотека (std
) предоставляет множество пользовательских типов, которые значительно расширяют примитивы
. Некоторые из них:
- расширяемые строки
String
:"hello world"
- динамический массивы:
[1, 2, 3]
- опциональные типы:
Option<i32>
- типы для обработки ошибок:
Result<i32, i32>
- указатели на объекты в куче:
Box<i32>
Смотрите также:
Примитивы и стандартная библиотека
Box
, стек и куча
Все значения в Rust по умолчанию располагаются на стеке. Значения могут быть упакованы
(созданы в куче) при помощи Box<T>
. Box
- это умный указатель на расположенное в куче значение типа T
. Когда Box
покидает область видимости, вызывается его деструктор, который уничтожает внутренний объект, и занятая им память в куче освобождается.
Упакованные значения могут быть разыменованы с помощью операции *
.
Эта операция убирает один уровень косвенности.
Вектора
Вектора - это массивы изменяемого размера. Их размер, как и у срезов, не известен во время компиляции, но он может расти или уменьшаться в любое время. Вектора представляются при помощи 3 параметров:
- указатель на данные
- длина
- вместимость
Вместимость показывает сколько памяти зарезервировано для вектора. Вектор может расти до тех пор, пока его длина меньше вместимости. Если при следующей вставке порог может быть превышен, под вектор выделяется больше памяти и данные переносятся в новый вектор.
Подробную информацию о методах объекта Vec можно почитать в разделе модуля std::vec
Строки
В Rust есть два типа строк: String
и &str
.
String
сохраняется как вектор байт
(Vec<u8>
), но с гарантией, что это всегда будет
действительная UTF-8 последовательность. String
выделяется в куче, расширяемая и не заканчивается нулевым байтом
(не null-terminated).
&str
- это срез (&[u8]
),
который всегда указывает на действительную UTF-8
последовательность, и является отображением
String
, так же как и &[T]
-
отображение Vec<T>
.
Больше методов str
и String
вы
можете найти в описании модулей std::str и
std::string.
Литералы и экранирование
Есть несколько способов написать строковый литерал со
специальными символами в нём. Все способы приведут к одной и
той же строке, так что лучше использовать тот способ, который
легче всего написать. Аналогично все способы записать строковый
литера из байтов в итоге дадут &[u8; N]
.
Обычно специальные символы экранируются с помощью обратной косой черты: \
. В этом случае вы можете добавить в вашу
строку любые символы, даже непечатаемые и те, которые вы не
знаете как набрать. Если вы хотите добавить обратную косую черту,
экранируйте его с помощью ещё одной: \\
.
Строковые или символьные разделители литералов (кавычки, встречающиеся внутри другого литерала, должны быть экранированы: "\""
, '.'
.
Иногда приходится экранировать слишком много символов или легче записать строку как она есть. В этот момент в игру вступают сырые строковые литералы.
fn main() {
let raw_str = r"Экранирование здесь не работает: \x3F \u{211D}";
println!("{}", raw_str);
// Если вам необходимы кавычки с сырой строке, добавьте пару `#`
let quotes = r#"И затем я сказал: "Здесь нет экранирования!""#;
println!("{}", quotes);
// Если вам необходимо добавить в вашу строку `"#`, то просто добавьте больше `#` в разделитель.
// Здесь нет ограничений на количество `#` которое вы можете использовать.
let longer_delimiter = r###"Строка с "# внутри неё. И даже с "##!"###;
println!("{}", longer_delimiter);
}
Хотите строку, которая не UTF-8? (Помните, str
и
String
должны содержать действительные UTF-8
последовательности). Или возможно вы хотите массив байтов,
которые в основном текст? Байтовые строки вас спасут!
use std::str;
fn main() {
// Обратите внимание, что в действительности это не `&str`
let bytestring: &[u8; 21] = b"это строка байтов";
// Для массива байтов не реализован типаж `Display`, поэтому способы его печати ограничены
println!("Строка байтов: {:?}", bytestring);
// Байтовые строки могут содержать экранированные байты...
let escaped = b"\x52\x75\x73\x74 как байты";
// ... но не Unicode
// let escaped = b"\u{211D} здесь не разрешён";
println!("Экранированные байты: {:?}", escaped);
// Сырые байтовые строки работают также, как и сырые строки
let raw_bytestring = br"\u{211D} здесь не экранировано";
println!("{:?}", raw_bytestring);
// Преобразование массива байт в `str` может завершиться ошибкой
if let Ok(my_str) = str::from_utf8(raw_bytestring) {
println!("И то же самое в виде текста: '{}'", my_str);
}
let _quotes = br#"Вы также можете использовать удобное для вас форматирование, \
как и с обычными сырыми строками"#;
// Байтовые строки не обязаны быть UTF-8
let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82"; // "ようこそ" в SHIFT-JIS
// Но из-за этого они не всегда могут быть преобразованы в `str`
match str::from_utf8(shift_jis) {
Ok(my_str) => println!("Удачное преобразование: '{}'", my_str),
Err(e) => println!("Неудачное преобразование: {:?}", e),
};
}
Для преобразования между кодировками символов, посмотрите крейт encoding.
Более детальный список способов записи строковых литералов и экранирования символов можно найти в главе 'Tokens' Rust Reference.
Option
Иногда желательно перехватить ошибку в какой-либо части программы вместо вызова паники с помощью макроса panic!
. Это можно сделать с помощью перечисления Option
.
Перечисление Option<T>
имеет два варианта:
None
, указывающий о наличии ошибки или отсутствия значенийSome(value)
, кортежная структура, обёртка длязначения
типаT
.
Result
Раньше мы видели, что в качестве возвращаемого значения из функции, которая может завершиться с ошибкой, можно использовать перечисление Option
, в котором None
будет обозначать неудачу. Однако иногда важно понять почему операция потерпела неудачу. Для этого у нас есть перечисление Result
.
Перечисление Result<T, E>
имеет два варианта:
Ok(value)
, который обозначает, что операция успешно завершилась, и оборачивает значение (value
), возвращаемое операцией (value
имеет типT
).Err(why)
, который показывает, что операция потерпела неудачу, оборачивает значение ошибки (причину,why
), которое (надеемся) описывает причину неудачи.why
имеет типE
.
?
Разбор цепочки результатов с использованием match
может стать
довольно неопрятной, к счастью, с помощью оператора
?
можно сделать разбор снова красивым.
?
используется в конце выражения, возвращающего
Result
и эквивалентен выражению match
, в котором
ветка Err(err)
разворачивается в
Err(From::from(err))
, а ветка Ok(ok)
во
внутреннее значение (ok
).
Обязательно посмотрите документацию, так как есть много
методов для работы с Result
.
panic!
Макрос panic!
используется для генерации паники и раскрутки стека. Во время раскрутки стека, среда выполнения возьмёт на себя всю ответственность по освобождению ресурсов, которыми владеет текущий поток, вызывая деструкторы всех объектов.
Так как в данном случае мы имеем дело с однопоточной программой, panic!
заставит программу вывести сообщение с ошибкой и завершится.
Давайте убедимся, что panic!
не приводит к утечке памяти.
$ rustc panic.rs && valgrind ./panic
==4401== Memcheck, a memory error detector
==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==4401== Command: ./panic
==4401==
thread '<main>' panicked at 'division by zero', panic.rs:5
==4401==
==4401== HEAP SUMMARY:
==4401== in use at exit: 0 bytes in 0 blocks
==4401== total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated
==4401==
==4401== All heap blocks were freed -- no leaks are possible
==4401==
==4401== For counts of detected and suppressed errors, rerun with: -v
==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
HashMap
В то время как вектора сохраняют значения с числовыми индексами, HashMap
сохраняют значения по ключу. Ключи HashMap
могут иметь логический, числовой, строковый или любой другой тип данных, который реализует типажи Eq
и Hash
. Подробнее об этом в следующей главе.
Как у векторов, размер объектов HashMap
может увеличиваться, но они также могут и уменьшиться, если часть занимаемой ими памяти не используется. Вы можете создать хэш-карту с определённой начальной вместимостью при помощи функции HashMap::with_capacity(uint)
или использовать HashMap::new()
для получения хэш-карты с начальной вместимостью по умолчанию (рекомендуется).
Для большей информации о том, как работает хеширование и хэш-карты (который иногда называются хэш-таблицами), вы можете обратиться к Wikipedia.
Альтернативные (пользовательские) типы ключей
Любой тип, реализующий типажи Eq
и
Hash
могут являться ключами в
HashMap
. Туда входят:
bool
(хотя он будет не очень полезен, так как будет всего лишь два возможных ключа)int
,uint
и все их вариантыString
и&str
(подсказка: вы можете сделатьHashMap
с ключами типаString
, а вызывать.get()
- с&str
)
Заметьте, что f32
и f64
не
реализуют Hash
, из-за того, что ошибки
точности при работе с плавающей запятой могут привести к
ужасным ошибкам при использовании их в качестве ключей для
хэш-карт.
Все классы коллекций реализуют Eq
и
Hash
если содержащийся в них тип также реализует
Eq
и Hash
. Например,
Vec<T>
реализует Hash
, если
T
реализует Hash
.
Вы можете легко реализовать Eq
и
Hash
для пользовательских типов добавив всего
лишь одну строчку: #[derive(PartialEq, Eq, Hash)]
Компилятор сделает всё остальное. Если вы хотите больше
контроля над деталями, вы можете сами реализовать
Eq
и/или Hash
. Данное руководство
не охватывает специфику реализации Hash
.
Чтобы поиграть с использованием struct
в
HashMap
, давайте попробуем реализовать очень
простую систему авторизации пользователей:
HashSet
Рассмотрим HashSet
как HashMap
в
котором мы заботимся только о ключах (в действительности,
HashSet<T>
- это просто адаптер к
HashMap<T, ()>
).
"Какой в этом смысл?", - спросите вы. - "Я бы мог просто хранить
ключи в Vec
."
Уникальная особенность HashSet
в том, что он
гарантирует, что в нём не содержится повторяющихся элементом.
Это условие выполняет любой набор (set). HashSet
-
всего лишь одна реализация (смотрите также:
BTreeSet
).
Если вы вставите значение, которое уже содержится в
HashSet
, (например, новое значение равно
существующему значению и они оба имеют одинаковый хэш), то
новое значение заменит старое.
Это хорошо подходит для случаев, когда вы не хотите иметь в коллекции больше одного "чего-либо" или когда вам необходимо знать имеете ли вы что-либо.
Но наборы могут делать гораздо более.
Наборы имеют 4 основные операции (все вызовы вернут итератор):
-
union
: получить все уникальные элементы из обоих наборов. -
difference
: получить все элементы, представленные в первом наборе, но отсутствующие во втором. -
intersection
: получить только те элементы, которые присутствуют в обоих наборах. -
symmetric_difference
: получить элементы содержащиеся либо только в первом наборе, либо только во втором, но не в обоих (xor).
Попробуем эти методы в следующем примере:
(Пример адаптирован из документации)
Rc
Когда необходимо множественное владение, может использоваться Rc
(счётчик ссылок). Rc
отслеживает количество ссылок, то есть количество владельцев значения, сохранённого внутри Rc
.
Счётчик ссылок в Rc
увеличивается на 1 каждый
раз, когда Rc
клонируется, и уменьшается на 1, когда
любой из клонов Rc
выходит из области видимости и удаляется. Когда
количество ссылок в Rc
становится равным нулю,
т.е. когда владельцев больше нет, и сам Rc
, и значение внутри него удаляются.
При клонировании Rc
никогда не делается глубокая копия. Клонирование лишь создаёт ещё один указатель на обёрнутое значение и увеличивает счётчик.
Смотрите также:
std::rc and std::sync::arc.
Разное в стандартной библиотеке
Стандартная библиотека предоставляет много других типов, позволяющих работать с такими вещами как например:
- Потоки
- Каналы
- Операции файлового ввода/вывода
Они расширяют возможности, которые предоставляют примитивы.
Смотрите также:
примитивы и стандартная библиотека
Потоки
Rust предоставляет механизм для создания собственных потоков операционной системы через функцию spawn
. Аргументом этой функции является замыкание, которое принимает владение захваченным ею окружением.
Эти потоки будут запланированы ОС.
Пример: map-reduce
Rust позволяет очень легко распределить обработку данных между потоками, без головной боли, традиционно связанной с попыткой сделать это.
Стандартная библиотека предоставляет отличные примитивы для работы потоками из коробки. Они в сочетании с концепцией владения и правилами алиасинга в Rust, автоматически предотвращают гонки данных.
Правила алиасинга (одна уникальная ссылка на запись или много ссылок на чтение) автоматически не позволяет вам манипулировать состоянием, которое видно другим потокам. (Где синхронизация необходима, есть примитивы синхронизации, такие как mutex
(мьютексы) или channel
(каналы).)
В этом примере мы вычислим сумму всех цифр в блоке чисел. Мы сделаем это, разбив куски блока на разные потоки. Каждый поток будет суммировать свой крошечный блок цифр, и впоследствии мы будем суммировать промежуточные суммы, полученные каждым потоком.
Обратите внимание на то, что хоть мы и передаём ссылки через границы потоков, Rust понимает, что мы только передаём неизменяемые ссылки, которые можно только читать, и что из-за этого не может быть никакой небезопасности и гонок данных. Так как мы перемещаем (move
) сегменты данных в поток, Rust также уверен, что данные будут жить до тех пор, пока поток не завершится, и висящих указателей не появится.
Назначения
Не стоит позволять числу наших потоков быть зависимом от введённых пользователем данных. Что если пользователь решит вставить много пробелов? Мы действительно хотим создать 2000 потоков? Измените программу так, чтобы данные разбивались на ограниченное число блоков, объявленных статической константой в начале программы.
Смотрите также:
- Потоки
- вектора и итераторы
- замыкания, семантика передачи владения и перемещения (
move
) в замыканиях - деструктуризация при присвоениях
- нотация turbofish в помощь механизму вывода типов
unwrap
илиexpect
- перечисления
Каналы
Rust предоставляет асинхронные каналы (channel
) для
взаимодействия между потоками. Каналы обеспечивают
однонаправленную передачу информации между двумя конечными
точками: отправителем (Sender
) и получателем
(Receiver
).
Path
Структура Path
представляет пути к файлу в файловой
системе. Есть два вида Path
: posix::Path
,
для UNIX - подобных систем, и windows::Path
, для
Windows. В прелюдии экспортируется соответствующий
платформозависимый вариант Path
.
Path
может быть создан из OsStr
, и
предоставляет некоторые методы для получения информации о
файле или директории, на которые он указывает.
Обратите внимание, что внутренне представление
Path
не является UTF-8 строкой, но вместо
этого хранит вектор байт (Vec<u8>
).
Следовательно, преобразование Path
в
&str
не бесплатно и может закончиться
неудачей (возвращается Option
).
Не забудьте проверить остальные методы Path
(posix::Path
или windows::Path
) и
структуры Metadata
.
Смотрите также:
Файловый ввод-вывод
Структура File
представляет открытый файл (она является обёрткой над файловым дескриптором) и даёт возможность чтения/записи этого файла.
Из-за того, что многие вещи могут пойти не так в процессе файлового
ввода-вывода, все методы File
возвращают тип
io::Result<T>
, который является псевдонимом для
Result<T, io::Error>
.
Это делает явными ошибки всех операций ввода-вывода. Благодаря этому, программист может увидеть все пути отказов и обрабатывать их упреждающей форме.
open
Статический метод open
может использоваться для открытия файла в режиме только для чтения.
Структура File
владеет ресурсом, файловым дескриптором, и заботится о том, чтобы он был закрыт, когда структура удаляется из памяти.
Вот ожидаемый результат:
$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt содержит:
Hello World!
(Рекомендуем протестировать предыдущий пример при различных условиях сбоев: файл hello.tx
t не существует или hello.t
xt не читаемый и другое)
create
Статический метод create
открывает файл в режиме только для записи. Если файл уже существует, то его содержимое уничтожится, в противном же случае, создастся новый файл.
static LOREM_IPSUM: &str =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn main() {
let path = Path::new("out/lorem_ipsum.txt");
let display = path.display();
// Откроем файл в режиме для записи. Возвращается `io::Result<File>`
let mut file = match File::create(&path) {
Err(why) => panic!("невозможно создать {}: {}", display, why),
Ok(file) => file,
};
// Запишем строку `LOREM_IPSUM` в `file`. Возвращается `io::Result<()>`
match file.write_all(LOREM_IPSUM.as_bytes()) {
Err(why) => panic!("невозможно записать в {}: {}", display, why),
Ok(_) => println!("успешно записано в {}", display),
}
}
Вот расширенный ожидаемый результат:
$ rustc create.rs && ./create
successfully wrote to lorem_ipsum.txt
$ cat lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
(Как и в предыдущем примере, предлагаем вам протестировать этот код с различными вариантами отказа.)
Существует структура OpenOptions
, которая может использоваться для настройки того, как файл будет открыт.
read_lines
Метод lines()
возвращает итератор, проходящий через
все строки файла.
File::open
работает с чем-то, что реализует типаж AsRef<Path>
. Поэтому read_lines()
будет ожидать это же.
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
fn main() {
// Файл `hosts` должен существовать в текущей директории
if let Ok(lines) = read_lines("./hosts") {
// Получает итератор, который возвращает Option
for line in lines {
if let Ok(ip) = line {
println!("{}", ip);
}
}
}
}
// Для обработки ошибок, возвращаемое значение оборачивается в Result
// Возвращаем `Iterator` для построчного чтения файла.
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
Запуск этой программы просто выводит эти строки на экран по отдельности.
$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts
$ rustc read_lines.rs && ./read_lines
127.0.0.1
192.168.0.1
Такой подход более эффективен, чем создание String
в памяти, особенно при работе с большими файлами.
Дочерние процессы
Структура process::Output
представляет результат завершённого дочернего процесса,
и структура process::Command
- это строитель процесса.
(Рекомендуется попробовать предыдущий пример с неправильным флагом обращения к rustc
)
Pipes
Структура std::Child
представляет собой запущенный дочерний процесс и предоставляет дескрипторы stdin
, stdout
и stderr
для взаимодействия с этим процессом через каналы (pipes).
use std::io::prelude::*;
use std::process::{Command, Stdio};
static PANGRAM: &'static str =
"the quick brown fox jumped over the lazy dog\n";
fn main() {
// Создадим команду `wc`
let process = match Command::new("wc")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
Err(why) => panic!("не удалось создать wc: {}", why.description()),
Ok(process) => process,
};
// Запишем строку в `stdin` созданной команды.
//
// `stdin` имеет тип `Option<ChildStdin>`, но так как мы знаем, что экземпляр должен быть только один,
// мы можем напрямую вызвать `unwrap`.
match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
Err(why) => panic!("не удалось записать в stdin команды wc: {}", why),
Ok(_) => println!("пангамма отправлена"),
}
// Так как `stdin` не существует после вышележащих вызовов, он разрушается
// и канал закрывается.
//
// Это очень важно, иначе `wc` не начал бы обработку только что
// отправленных данных.
// Поле `stdout` имеет тип `Option<ChildStdout>` и может быть извлечено.
let mut s = String::new();
match process.stdout.unwrap().read_to_string(&mut s) {
Err(why) => panic!("невозможно прочесть stdout команды wc: {}", why),
Ok(_) => print!("wc ответил:\n{}", s),
}
}
Ожидание
Если вы хотите дождаться завершения process::Child
, вы должны вызвать Child::wait
, который вернёт process::ExitStatus
.
use std::process::Command;
fn main() {
let mut child = Command::new("sleep").arg("5").spawn().unwrap();
let _result = child.wait().unwrap();
println!("достигнут конец функции main");
}
$ rustc wait.rs && ./wait
# `wait` продолжает работать в течение 5 секунд, пока команда `sleep 5` не завершится
достигнут конец функции main
Работа с файловой системой
Модуль std::fs
содержит различные функции для
работы с файловой системой.
use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;
// Упрощённая реализация `% cat path`
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// Упрощённая реализация `% echo s > path`
fn echo(s: &str, path: &Path) -> io::Result<()> {
let mut f = File::create(path)?;
f.write_all(s.as_bytes())
}
// Упрощённая реализация `% touch path` (игнорирует существующие файлы)
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn main() {
println!("`mkdir a`");
// Создаём директорию, получаем `io::Result<()>`
match fs::create_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(_) => {},
}
println!("`echo hello > a/b.txt`");
// Предыдущий `match` может быть написан проще, с помощью метода`unwrap_or_else`
echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`mkdir -p a/c/d`");
// Рекурсивно создаём директории, получаем `io::Result<()>`
fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`touch a/c/e.txt`");
touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`ln -s ../b.txt a/c/b.txt`");
// Создаём символическую ссылку, получаем `io::Result<()>`
if cfg!(target_family = "unix") {
unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
println!("`cat a/c/b.txt`");
match cat(&Path::new("a/c/b.txt")) {
Err(why) => println!("! {:?}", why.kind()),
Ok(s) => println!("> {}", s),
}
println!("`ls a`");
// Читаем содержимое директории, получаем `io::Result<Vec<Path>>`
match fs::read_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(paths) => for path in paths {
println!("> {:?}", path.unwrap().path());
},
}
println!("`rm a/c/e.txt`");
// Удаляем файл, получаем `io::Result<()>`
fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`rmdir a/c/d`");
// Удаляем пустую директорию, получаем `io::Result<()>`
fs::remove_dir("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
Вот ожидаемый результат:
$ rustc fs.rs && ./fs
`mkdir a`
`echo hello > a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
> hello
`ls a`
> "a/b.txt"
> "a/c"
`rm a/c/e.txt`
`rmdir a/c/d`
И конечное состояние директории a
:
$ tree a
a
|-- b.txt
`-- c
`-- b.txt -> ../b.txt
1 directory, 2 files
Альтернативный путь определения функции cat
- с
нотацией ?
:
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
Смотрите также:
Аргументы программы
Стандартная библиотека
Аргументы командной строки могут быть доступны при помощи
std::env::args
, который возвращает итератор, который
выдаёт String
для каждого аргумента:
$ ./args 1 2 3
Мой путь ./args.
У меня 3 аргумента: ["1", "2", "3"].
Крейты
В качестве альтернативы, существует несколько крейтов, которые
предоставляют дополнительную функциональность при создании
приложений командной сроки. Rust Cookbook показывает
лучшие практики, как использовать один из самых популярных
крейтов для аргументов командной строки, clap
.
Парсинг аргументов
Сопоставление может быть использовано для разбора простых аргументов:
$ ./match_args Rust
Это не ответ.
$ ./match_args 42
Это ответ!
$ ./match_args do something
ошибка: второй аргумент не является числом
usage:
match_args <string>
Проверяет является ли данная строка ответом.
match_args {increase|decrease} <integer>
Увеличивает или уменьшает число на 1.
$ ./match_args do 42
ошибка: неизвестная команда
usage:
match_args <string>
Проверяет является ли данная строка ответом.
match_args {increase|decrease} <integer>
Увеличивает или уменьшает число на 1.
$ ./match_args increase 42
43
Foreign Function Interface
Rust предоставляет интерфейс внешних функций (Foreign Function
Interface, FFI) к библиотекам, написанным на языке С. Внешние
функции должны быть объявлены внутри блока extern
и аннотированы при помощи атрибута #[link]
, который
содержит имя внешней библиотеки.
use std::fmt;
// Этот extern-блок подключает библиотеку libm
#[link(name = "m")]
extern {
// Это внешняя функция, которая считает квадратный корень
// комплексного числа одинарной точности
fn csqrtf(z: Complex) -> Complex;
fn ccosf(z: Complex) -> Complex;
}
// Так как вызовы внешних функций считаются unsafe,
// принято писать над ними обёртки.
fn cos(z: Complex) -> Complex {
unsafe { ccosf(z) }
}
fn main() {
// z = -1 + 0i
let z = Complex { re: -1., im: 0. };
// вызов внешней функции - unsafe операция
let z_sqrt = unsafe { csqrtf(z) };
println!("квадратный корень от {:?} равен {:?}", z, z_sqrt);
// вызов безопасного API в котором находится unsafe операция
println!("cos({:?}) = {:?}", z, cos(z));
}
// Минимальная реализация комплексного числа одинарной точности
#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
re: f32,
im: f32,
}
impl fmt::Debug for Complex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.im < 0. {
write!(f, "{}-{}i", self.re, -self.im)
} else {
write!(f, "{}+{}i", self.re, self.im)
}
}
}
Тестирование
Rust - это язык программирования, который очень заботится о корректности и включает в себя поддержку написания тестов программного обеспечения встроенными средствами языка.
Предоставляется три стиля тестирования:
Также Rust поддерживает указание дополнительных зависимостей для тестов:
Смотрите также:
- Глава о тестировании в "The Rust Programming Language"
- Описание API для тестирования примеров из документации.
Unit-тестирование
Тесты - это функции на Rust, которые проверяют, что тестируемый код работает ожидаемым образом. Тело тестовых функций обычно выполняет некоторую настройку, запускает код, который мы тестируем, и затем сравнивает полученный результат с тем, что мы ожидаем.
Большинство модульных тестов располагается в модуле tests
, помеченном атрибутом #[cfg(test)]
. Тестовые функции помечаются атрибутом #[test]
.
Тесты заканчиваются неудачей, когда что-либо в тестовой функции вызывает панику. Есть несколько вспомогательных макросов:
assert!(expression)
- паникует, если результат выражения равенfalse
.assert_eq!(left, right)
иassert_ne!(left, right)
- сравнивает левое и правое выражения на равенство и неравенство соответственно.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Это действительно плохая функция сложения, её назначение в данном // примере - потерпеть неудачу.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
// Обратите внимание на эту полезную идиому: импортирование имён из внешней (для mod - тестов) области видимости.
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
// Это утверждение запустится и проверка не сработает.
// Заметьте, что приватные функции также могут быть протестированы!
assert_eq!(bad_add(1, 2), 3);
}
}
Тесты могут быть запущены при помощи команды cargo test
.
$ cargo test
running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok
failures:
---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::test_bad_add
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
Тесты и ?
Ни один из предшествующих unit-тестов не имеют возвращаемый тип. Но в Rust 2018 ваши unit-тесты могут вернуть Result<()>
, что позволяет использовать в них ?
! Это может сделать их более краткими.
Для дополнительной информации смотрите "The Edition Guide".
Тестирование паники
Для тестирования функций, которые должны паниковать при определённых обстоятельствах, используется атрибут #[should_panic]
. Этот атрибут принимает необязательный параметр expected =
с текстом сообщения о панике. Если ваша функция может паниковать в разных случаях, то этот параметр поможет вам быть уверенным, что вы тестируете именно ту панику, которую собирались.
pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("Divide-by-zero error");
} else if a < b {
panic!("Divide result is zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide() {
assert_eq!(divide_non_zero_result(10, 2), 5);
}
#[test]
#[should_panic]
fn test_any_panic() {
divide_non_zero_result(1, 0);
}
#[test]
#[should_panic(expected = "Divide result is zero")]
fn test_specific_panic() {
divide_non_zero_result(1, 10);
}
}
Запуск этих тестов даст следующее:
$ cargo test
running 3 tests
test tests::test_any_panic ... ok
test tests::test_divide ... ok
test tests::test_specific_panic ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Запуск конкретных тестов
Для запуска конкретного теста надо добавить имя теста в команду cargo test
.
$ cargo test test_any_panic
running 1 test
test tests::test_any_panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Для запуска нескольких тестов, можно указать часть имени, которая есть во всех необходимых тестах.
$ cargo test panic
running 2 tests
test tests::test_any_panic ... ok
test tests::test_specific_panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Игнорирование тестов
Тесты могут быть помечены атрибутом #[ignore]
, чтобы они были исключены из списка запускаемых командой cargo test
. Такие тесты можно запустить с помощью команды cargo test -- --ignored
.
$ cargo test
running 3 tests
test tests::ignored_test ... ignored
test tests::test_add ... ok
test tests::test_add_hundred ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test -- --ignored
running 1 test
test tests::ignored_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Тестирование документации
Основной способ документирования проекта на Rust - это аннотирование исходного кода. Документационные комментарии пишутся с использованием markdown и позволяют использовать внутри блоки кода. Rust заботится о корректности, так что эти блоки кода могут компилироваться и использоваться в качестве тестов.
/// Первая строка - это краткое описание функции.
///
/// Следующие строки представляют детальную документацию. Блоки кода /// начинаются трёх обратных кавычек и внутри содержат неявные
/// `fn main()` и `extern crate <cratename>`. Предположим, мы
/// тестируем крейт `doccomments`:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Ообычно документационные комментарии могут содержат секции "Examples", "Panics" and "Failures".
///
/// Следующая функция делит два числа.
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// Функция паникует, если второй аргумент равен нулю.
///
/// ```rust,should_panic
/// // паникует при делении на 0
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Ошибка деления на 0");
}
a / b
}
Тесты могут быть запущены при помощи cargo test
:
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests doccomments
running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Мотивация для документационных тестов
Главная цель документационных тестов - служить примерами
предоставляемой функциональности, что является одной из самых
важных рекомендаций. Это позволяет использовать
примеры из документации в качестве полных фрагментов кода. Но
использование ?
приведёт к ошибке компиляции, так
как функция main
возвращает ()
(unit
). На помощь приходит возможность скрыть из документации
некоторые строки исходного кода: можно написать
fn try_main() -> Result<(), ErrorType>
, скрыть
её и вызвать её в скрытом main
с
unwrap
. Звучит сложно? Вот пример:
/// Использование скрытой `try_main` в документационных тестах.
///
/// ```
/// # // скрытые строки начинаются с символа `#`, но они всё ещё компилируемы!
/// # fn try_main() -> Result<(), String> { // эта линия оборачивает тело функции, которое отображается в документации
/// let res = try::try_div(10, 2)?;
/// # Ok(()) // возвращается из try_main
/// # }
/// # fn main() { // начало `main` которая выполняет `unwrap()`
/// # try_main().unwrap(); // вызов `try_main` и извлечение результата
/// # // так что в случае ошибки этот тест запаникует
/// # }
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Деление на 0"))
} else {
Ok(a / b)
}
}
Смотрите также:
- RFC505 по стилю документации
- Рекомендации для API по документационному тестированию
Интеграционное тестирование
Модульные тесты тестируют по одному модулю изолированно: они малы и могут проверить не публичный код. Интеграционные тесты являются внешними для вашего пакета и используют только его открытый интерфейс, таким же образом, как и любой другой код. Их цель в том, чтобы проверить, что многие части вашей библиотеки работают корректно вместе.
Cargo ищет интеграционные тесты в каталоге tests
после каталога src
.
Файл src/lib.rs
:
// Задаём это в кресте с именем `adder`.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Файл с тестом: tests/integration_test.rs
:
#[test]
fn test_add() {
assert_eq!(adder::add(3, 2), 5);
}
Запустим тесты можно командой cargo test
:
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-bcd60824f5fbfe19
running 1 test
test test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Каждый файл с исходным кодом Rust в директории tests
компилируется в отдельный крейт.
Чтобы можно было воспользоваться некоторым общим кодом в нескольких интеграционных тестах, можно создать модуль с публичными функциями, импортировать их, и использовать в тестах.
Файл tests/common/mod.rs
:
pub fn setup() {
// некоторый код для настройки, создание необходимых файлов/каталогов, запуск серверов.
}
Файл с тестом: tests/integration_test.rs
// импортируем модуль с общим кодом.
mod common;
#[test]
fn test_add() {
// используем общий код.
common::setup();
assert_eq!(adder::add(3, 2), 5);
}
Создание модуля как tests/common.rs
также работает, но не рекомендуется, потому что средство запуска тестов будет рассматривать файл как тестовый крейт и пытаться запускать тесты внутри него.
Зависимости для разработки
Иногда возникает необходимость иметь зависимости только для тестов (или примеров, или бенчмарков). Такие зависимости добавляются в Cargo.toml
в раздел [dev-dependencies]
. Эти зависимости не распространяются как зависимости на другие пакеты, которые зависят от этого пакета.
Одним из таких примеров является пакет pretty_assertions
, который расширяет стандартные макросы assert_eq!
и assert_ne!
, чтобы обеспечить цветной вывод отличий.
Файл Cargo.toml
:
# Стандартное содержимое крейта здесь пропущено
[dev-dependencies]
pretty_assertions = "1"
Файл src/lib.rs
:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq; // Крейт для использования только во время тестировании. Он не может быть использован вне кода тестов.
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Смотрите также:
Документация Cargo по указанию зависимостей.
Небезопасные операции
В качестве введения в этот раздел процитируем официальную документацию, "нужно стараться минимизировать количество небезопасного кода в кодовой базе." Имея это в виду, давайте начнём! Небезопасные аннотации в Rust используются для обхода блокировок защиты, устанавливаемых компилятором; в частности, существует четыре основных варианта использования небезопасного кода:
- разыменование сырых указателей
- вызов функций или методов, которые являются
unsafe
(включая вызов функции через FFI см. предыдущую главу книги) - доступ или изменение статических изменяемых переменных
- реализация небезопасных типажей
Сырые указатели
Сырые указатели *
и ссылки &T
имеют схожую функциональность, но ссылки всегда безопасны, потому что они гарантированно указывают на достоверные данные за счёт механизма проверки заимствований. Разыменование же сырого указателя можно выполнить только через небезопасный блок.
Вызов небезопасных функций
Некоторые функции могут быть объявлены как unsafe
, то есть за корректность этого кода несёт ответственность программист, а не компилятор. Пример - это метод std::slice::from_raw_parts
, который создаст срез из указателя на первый элемент и длины.
Для slice::from_raw_parts
одним из обязательств, которое должно быть выполнено, является факт того, что переданный указатель указывает на допустимую память, и что в памяти лежит значение правильного типа. Если эти условия не выполнены, то поведение программы не определено, и неизвестно, что произойдёт.
Совместимость
Rust быстро развивается и из-за этого могут возникнуть определённые проблемы совместимости, не смотря на усилия по обеспечению обратной совместимости везде, где это возможно.
Сырые идентификаторы
В Rust, как и во многих других языках программирования, существует концепция "ключевых слов". Эти идентификаторы что-то значат для языка и из-за этого вы не можете использовать их в качестве названия переменных, именах функций и других местах. Сырые идентификаторы позволяют использовать ключевые слова там, где они обычно не разрешены. Это особенно полезно, когда Rust вводит новые ключевые слова и библиотеки, использующие старую редакцию Rust, имеют переменные или функции с таким же именем, как и ключевое слово, введённое в новой редакции.
Например, рассмотрим крейт foo
, скомпилированный с 2015 редакцией Rust, и который экспортирует функцию с именем try
. Это ключевое слово зарезервировано для новой функциональности в 2018 редакции, из-за чего без сырых идентификаторов мы не можем назвать так функцию.
extern crate foo;
fn main() {
foo::try();
}
Вы получите ошибку:
error: expected identifier, found keyword `try`
--> src/main.rs:4:4
|
4 | foo::try();
| ^^^ expected identifier, found keyword
Вы можете записать это при помощи сырого идентификатора:
extern crate foo;
fn main() {
foo::r#try();
}
Meta
Некоторые темы не совсем имеют отношение к тому, как вы программируете. Вместо этого они расскажут вам об инструментах или инфраструктуре, которые пойдут на пользу всем. Среди этих тем:
- Документация: генерирование пользовательской документации с использованием
rustdoc
. - Песочница (Playground): Интегрируйте песочницу Rust в вашу документацию.
Документация
Используйте cargo doc
для сборки документации в
target/doc
.
Используйте cargo test
для запуска всех тестов
(включая документационные тесты) и cargo test --doc
для запуска только документационных тестов.
Эти команды, по мере необходимости, будут соответствующим
образом вызывать rustdoc
(и rustc
).
Документационные комментарии
Документационные комментарии очень полезны для больших
проектов, требующих документирования. Эти комментарии
компилируются в документацию при запуске rustdoc
. Они
обозначаются как ///
и поддерживают
Markdown.
Для запуска тестов сначала соберите код как библиотеку, а затем
укажите rustdoc
где найти эту библиотеку, чтобы он мог
подключить её к каждому документационному тесту:
$ rustc doc.rs --crate-type lib
$ rustdoc --test --extern doc="libdoc.rlib" doc.rs
Смотрите также:
- The Rust Book: Making Useful Documentation Comments
- The Rustdoc Book
- The Reference: Doc comments
- RFC 1574: API Documentation Conventions
- RFC 1946: Relative links to other items from doc comments (intra-rustdoc links)
- Is there any documentation style guide for comments? (reddit)