Rust на примерах

translated

Rust — современный язык программирования, нацеленный на безопасность, скорость и параллелизм. Данные цели достигаются за счёт безопасной работы с памятью без использования сборщика мусора.

Rust на примерах (Rust by Example, RBE) — это набор исполняемых примеров, которые иллюстрируют различные концепции языка Rust, а так же возможности его стандартной библиотеки. Для того, чтобы почерпнуть ещё больше из этих примеров, не забудьте установить Rust на своём компьютере и ознакомиться с официальной документацией. Самые любознательные могут заглянуть в исходный код этого сайта.

Итак, давайте приступим!

Привет, мир

Это исходный код традиционной программы "Привет, мир!".

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

println! - это макрос, который отображает текст в консоли.

Исполняемый файл может быть сгенерирован с помощью компилятора Rust — rustc.

$ rustc hello.rs

rustc создаст исполняемый файл hello, который можно будет запустить.

$ ./hello Привет, мир!

Задание

Нажми кнопку "Run", чтобы увидеть ожидаемый результат. Затем добавь новую строку с другим макросом println!, чтобы вывод был таким:

Привет, мир! Я программирую на языке Rust!

Комментарии

Каждая программа, безусловно, нуждается в комментариях и Rust предоставляет несколько способов комментирования кода:

  • Обычные комментарии, которые игнорируются компилятором:

    • // Однострочный комментарий. Который завершается в конце строки.
    • /* Блочный комментарий, который продолжается до завершающего символа. */
  • Doc комментарии, которые будут сгенерированы в HTML документацию:

    • /// Генерация документации для функции.
    • //! Генерация документации для модуля.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Документирование библиотек

Форматированный вывод

Вывод обрабатывается несколькими макросами, которые определены в std::fmt. Вот некоторые из них:

  • format!: записывает форматированный текст в String.
  • print!: работает аналогично с format!, но текст выводится в консоль (io::stdout).
  • println!: аналогично print!, но в конце добавляется переход на новую строку.
  • eprint!: аналогично format!, но текст выводится в стандартный поток ошибок (io::stderr).
  • eprintln!: аналогично eprint!, но в конце добавляется переход на новую строку.

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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 невозможно, он должен быть реализован вручную.

#![allow(unused)] fn main() { // Эта структура не может быть напечатана с помощью `fmt::Display` // или с помощью `fmt::Debug` struct UnPrintable(i32); // Атрибут `derive` автоматически реализует // необходимые методы, чтобы была возможность // напечатать структуру `struct` с помощью `fmt::Debug`. #[derive(Debug)] struct DebugPrintable(i32); }

Все типы из библиотеки std также могут быть автоматически распечатаны с помощью {:?}:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Так что fmt::Debug определённо позволяет распечатать объект, но жертвует некоторым изяществом. Rust также обеспечивает "красивую печать" с помощью {:#?}.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Можно вручную реализовать fmt::Display для управления отображением.

Смотрите также:

атрибуты, derive, std::fmt, и struct

Display

fmt::Debug выглядит не очень компактно и красиво, поэтому полезно настраивать внешний вид информации, которая будет напечатана. Это можно сделать реализовав типаж fmt::Display вручную, который использует маркер {} для печати. Его реализация выглядит следующим образом:

#![allow(unused)] fn main() { // Импортируем (с помощью `use`) модуль `fmt`, чтобы мы могли его использовать. use std::fmt; // Определяем структуру, для которой будет реализован `fmt::Display`. // Это простая кортежная структура c именем `Structure`, которая хранит в себе `i32`. struct Structure(i32); // Чтобы была возможность использовать маркер `{}` // `типаж (trait) fmt::Display` должен быть реализован вручную // для данного типа. impl fmt::Display for Structure { // Этот типаж требует реализацию метода `fmt` с данной сигнатурой: fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Записываем первый элемент в предоставленный выходной поток: `f`. // Возвращаем `fmt::Result`, который показывает выполнилась операция // успешно или нет. Обратите внимание на то, что синтаксис `write!` // похож на синтаксис `println!`. write!(f, "{}", self.0) } } }

Вывод 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.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Итак, 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 довольно простая:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Задание

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

[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, который работает без аргументов: например {}.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вы можете посмотреть полный список типажей форматирования и их типы аргументов в документации к std::fmt.

Задание

Добавьте реализацию типажа fmt::Display для структуры Color, чтобы вывод отображался вот так:

RGB (128, 255, 90) 0x80FF5A RGB (0, 3, 254) 0x0003FE RGB (0, 0, 0) 0x000000

Пара подсказок, если вы не знаете, что делать:

Смотрите также:

std::fmt

Примитивы

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 также умеет выводить типы из контекста.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

стандартная библиотека (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-подобных языках.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Кортежи

Кортежи - коллекция, которая хранит в себе переменные разных типов. Кортежи создаются с помощью круглых скобок (), и каждый кортеж является переменной с сигнатурой типов (T1, T2, ...), где T1, T2 тип члена кортежа. Функции могут использовать кортежи для возвращения нескольких значений, так кортежи могут хранить любое количество значений.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Задание

  1. Повторение: Добавьте реализацию типажа fmt::Display для структуры Matrix в примерах выше, чтобы, когда вы измените формат вывода с {:?} на {} на консоль вывелось:

    ( 1.1 1.2 ) ( 2.1 2.2 )

    Вы можете вернуться на пример print display.

  2. Добавьте функцию 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].

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Пользовательские типы

В языке программирования Rust пользовательские типы данных в основном создаются при помощи двух ключевых слов:

  • struct: определение структуры
  • enum: определение перечисления

Константы так же могут быть созданы с помощью ключевых слов const и static.

Структуры

Существует три типа структур, которые можно создать с помощью ключевого слова struct:

  • Кортежная структура, которая на самом деле является именованным кортежем.
  • Классическая C-структура
  • Единичная структура, которая не имеет полей, но может быть полезна для обобщённых типов.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Задание

  1. Добавьте функцию rect_area, которая рассчитывает площадь прямоугольника. Попробуйте использовать деструктуризацию — разбор на части.
  2. Добавьте функцию square которая принимает Point и f32 в качестве аргументов, и возвращает Rectangle левый верхний угол которого Point, а ширина и высота соответствуют f32.

Смотрите также:

Атрибуты и деструктуризация

Перечисления

Ключевое слово enum позволяет создавать тип данных, который представляет собой один из нескольких возможных вариантов. Любой вариант, действительный как struct, также действителен как enum.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Псевдонимы типов

Если вы используете псевдонимы типов, то вы можете обратиться к каждому варианту перечисления через его псевдоним. Это может быть полезно, если у перечисления слишком длинное имя или оно слишком обобщено, и вы хотите переименовать его.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Самое частое место, где можно это увидеть, — impl-блоки, которые используют Self.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Чтобы больше узнать о перечислениях и псевдонимах типов, вы можете почитать отчёт о стабилизации, в котором эта возможность была включена в Rust.

Смотрите также:

match, fn, String и "Type alias enum variants" RFC

Декларация use

Декларация use используется, чтобы убрать необходимость указывать область видимости:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

match и use

С-подобные

enum могут быть использованы как С-подобные перечисления.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Приведение типа

Пример: Связанный список

Пример использования enums для создания связанного списка:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Box и методы

Константы

В Rust есть два типа констант, которые могут быть объявлены в любой области видимости, включая глобальную. Оба требуют явной аннотации типа:

  • const: Неизменяемая переменная (в общем случае).
  • static: Возможно, изменяемая переменная с временем жизни 'static. Статическое время жизни подразумевается и может не быть указано явно. Доступ или модификация изменяемой статической переменной — небезопасны (см. unsafe).
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

RFC для const/static, время жизни 'static

Связывание переменных

Rust предоставляет безопасность типов с помощью статической типизации. Тип переменной может быть указан при объявлении связи с переменной. Тем не менее, в большинстве случаев, компилятор сможет определить тип переменной из контекста, что часто позволяет избавиться от бремени аннотирования кода.

Значения (как и литералы) могут быть привязаны к переменным, используя оператор let.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Изменяемость

По умолчанию связывание переменных является неизменяемым, но с помощью модификатора mut изменения можно разрешить.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Компилятор будет выводить подробные сообщения об ошибках, связанных с изменяемостью.

Область видимости и затенение

Связывание переменных происходит в локальной области видимости — они ограничены существованием внутри блока. Блок — это набор инструкций, заключённый между фигурными скобками {}.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Кроме того, допускается затенение переменных.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Предварительное объявление

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Компилятор запрещает использование неинициализированных переменных, так как это привело бы к неопределённому поведению.

Заморозка

Когда данные неизменяемо привязаны к тому же имени, они замораживаются. Замороженные данные не могут быть изменены до тех пор, пока неизменяемая привязка не выйдет из области видимости:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Типы

Rust предоставляет несколько механизмов изменения или определения примитивных и пользовательских типов:

Приведение типов

Rust не предусматривает неявного (принудительного) преобразования типов между примитивами. Однако явное преобразование типов (casting) можно выполнить, используя ключевое слово as.

Правила, используемые для преобразования внутренних типов, такие же, как в языке C, за исключением тех случаев, когда преобразование типов в языке C вызывает неопределённое поведение. Поведение всех приведений между встроенными типами чётко определено в Rust.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Литералы

Числовые литералы могут быть обозначены добавлением типа в качестве суффикса. Например, чтобы указать, что литерал 42 должен иметь тип i32, необходимо написать 42i32.

Без суффикса тип литерала будет зависеть от того, как он используется. Если нет никаких ограничений, то компилятор будет использовать i32 для целочисленных литералов, а f64 — для литералов с плавающей точкой.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

В предыдущем коде используются некоторые понятия, которых мы ранее не касались. Вот краткое пояснение для нетерпеливых читателей:

  • std::mem::size_of_val является функцией, но вызывается с указанием полного пути. Код можно разделить на логические единицы, называемые модулями. В данном случае функция определена в модуле mem, а модуль mem определён в контейнере std. Подробнее см. модули и контейнеры, а также соответствующую главу в книге

Вывод типов

Движок вывода типов весьма умён. Он делает куда больше, чем просто смотрит на тип r-value при инициализации. Он также следит за тем, как используется значение после инициализации, чтобы определить его тип. Вот расширенный пример вывода типов:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Не потребовалось никакой аннотации типов переменных, компилятор счастлив, как и программист!

Псевдонимы

Оператор type используется, чтобы задать новое имя существующему типу. Имя типа должно быть в стиле UpperCamelCase, иначе компилятор выдаст предупреждение. Исключением являются примитивные типы: usize, f32 и другие.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Основное применение псевдонимов — сокращение размера кода: например, тип IoResult<T> является псевдонимом типа Result<T, IoError>.

Смотрите также:

Атрибуты

Приведение типов

Примитивные типы могут быть сконвертированы в другие при помощи приведения типов.

Rust предоставляет преобразование между пользовательскими типами (такими как, struct и enum) через использование трейтов. Общие преобразования используют трейты From и Into. Однако есть и конкретные трейты для более частных случаев, например для конвертации String.

From и Into

Типажи From и Into связаны по своей сути, и это стало частью их реализации. Если вы можете конвертировать тип А в тип В, то будет легко предположить, что мы должны быть в состоянии конвертировать тип В в тип А.

From

Типаж From позволяет типу определить, как он будет создаваться из другого типа, что предоставляет очень простой механизм конвертации между несколькими типами. Есть несколько реализаций этот типажа в стандартной библиотеке для преобразования примитивов и общих типов.

Для примера, мы можем легко конвертировать str в String

#![allow(unused)] fn main() { let my_str = "привет"; let my_string = String::from(my_str); }

Мы можем сделать нечто похожее для определения конвертации для нашего собственного типа.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Into

Трейт Into является полной противоположностью трейта From. Так что если вы реализовали для вашего типа трейт From, то трейт Into вызовет его при необходимости.

Использование типажа Into обычно требует спецификации типа, в который мы собираемся конвертировать, так как компилятор чаще всего не может это вывести. Однако это небольшой компромисс, учитывая, что данную функциональность мы получаем бесплатно.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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!.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Парсинг строки

Один из наиболее общих типов конвертации - это преобразование строки в число. Идиоматический подход это сделать при помощи функции parse и указания типа, в который будем преобразовывать, что можно сделать либо через выведение типа, либо при помощи 'turbofish'-синтаксиса. Оба варианта показаны в следующем примере.

Это преобразует строку в указанный тип при условии, что для этого типа реализован типаж FromStr. Он реализован для множества типов стандартной библиотеки. Чтобы получить эту функциональность для пользовательского типа, надо просто реализовать для этого типа типаж FromStr.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Выражения

Программы на языке Rust - это (в основном) набор последовательных операторов:

fn main() { // оператор // оператор // оператор }

Существует несколько типов операторов в Rust. Наиболее распространённые - оператор связывания и выражение, заканчивающееся ;:

fn main() { // оператор связывания let x = 5; // оператор выражения x; x + 1; 15; }

Блоки так же могут быть частью оператора выражения. Они используются в качестве r-values при присваивании. Последнее выражение в блоке будет присвоено l-value. Однако, если последнее выражение в блоке оканчивается точкой с запятой, в качестве значения будет возвращено ().

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Управление потоком

Неотъемлемой частью любого языка программирования являются управляющие конструкции: if / else , for и другие. Давайте поговорим о них в Rust.

if/else

Ветвление с помощью if-else похоже на аналоги в других языка программирования. В отличие от многих из них, логическое условие не должно быть заключено в круглые скобки, а после каждого условия должен следовать блок. Условные операторы if-else являются выражениями, и все ветки должны возвращать значения одного и того же типа.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

loop

Rust предоставляет ключевое слово loop для обозначения бесконечного цикла.

Оператор break используется, чтобы выйти из цикла в любое время, а оператор continue используется ,чтобы пропустить оставшуюся часть цикла и начать новую итерацию.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вложенность и метки

Можно прерывать выполнение внешних циклов с помощью break или continue, когда речь заходит о вложенных циклах. Для этого циклы должны быть обозначены метками вроде 'label, а метки должны быть переданы операторам break или continue.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Возврат из циклов

Одним из видов использования цикла loop является повторение операции, пока она не будет выполнена. Если операция возвращает значение, вам может потребоваться передать его в другую часть кода: поместите его после break, и оно будет возвращено выражением loop.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

while

Ключевое слово while используется для создания цикла, который будет выполняться, пока условие истинно.

Давайте напишем печально известный FizzBuzz, используя цикл while.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Цикл for

for и диапазоны

Конструкция for in может быть использована для итерации по итераторам (Iterator). Один из самых простых способов создать итератор это использовать диапазон значений a..b. Это вернёт нам значения от a (включительно) до b (исключительно) за один шаг.

Давайте напишем FizzBuzz, используя for вместо while.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Также, может быть использован диапазон a..=b, включающий оба конца. Код выше может быть записан следующим образом:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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, которая имеет ключевое отличие в зависимости от типа выполнения итераций. Разница в типе, конечно, подразумевает различные действия, которые могут быть выполнены.

Смотрите также:

Итераторы (Iterator)

match

Rust обеспечивает сопоставление с образцом с помощью ключевого слова match , которое можно использовать похожим образом, как switch в языке C. Срабатывает первая подходящая ветка, и все возможные значения должны быть перечислены.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Деструктуризация

Блок match может деструктурировать элементы в различных формах.

Кортежи

Кортежи можно деструктурировать с помощью match следующим образом:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Tuples

Перечисления

Деструктуризация enum происходит следующим образом:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

#[allow(...)], цветовая модель и перечисления

Указатели и ссылки

Для указателей нужно отметить разницу между деструктуризацией и разыменованием, так как это разные понятия, которые используются по разному начиная с языков вроде C/C++.

  • Разыменование использует *
  • Деструктуризация использует &, ref и ref mut
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Паттерн ref

Структуры

Структуры могут быть деструктурированы следующим образом:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Структуры

Ограничители шаблонов

Внутри конструкции match можно добавить ограничитель шаблонов для фильтрации возможных вариантов.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Tuples

Связывание

Косвенный доступ к переменной делает невозможным ветвление и использование переменной без повторной привязки. match предоставляет символ @ для привязки значения к имени:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вы также можете использовать привязку для "деструктурирования" вариантов enum, таких как Option:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Функции, enum и Option

if let

В некоторых случаях использование match выглядит неуклюже. Например:

#![allow(unused)] fn main() { // Создаём переменную `optional` типа `Option<i32>` let optional = Some(7); match optional { Some(i) => { println!("Это очень большая строка и `{:?}`", i); // ^ Нужно 2 отступа только для того, чтобы извлечь `i` }, _ => {}, // ^ Обязателен, так как `match` исчерпывающий. Не выглядит ли это // как зря потраченное пространство? }; }

if let намного компактнее и выразительнее для данного случая и, кроме того, позволяет рассмотреть различные варианты ошибок.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Точно так же, if let может быть использован для сравнения любого значения перечисления:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Другое преимущество if let в том, что он позволяет сопоставлять нам не параметризованные варианты перечисления. Это возможно даже если для перечисления не реализован и не выведен типаж PartialEq. В некоторых случаях, if Foo::Bar == a не скомпилируется, потому что экземпляры перечисления не могут быть равны. Однако, с if let всё будет работать.

Хотите вызов? Исправьте следующий пример с использованием if let :

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

enum, Option, и RFC

while let

Так же, как иif let, while let может сделать неудобный match более терпимым. Рассмотрим следующий пример, в котором мы увеличиваем значение i:

#![allow(unused)] fn main() { // Создадим переменную `optional` с типом `Option<i32>` let mut optional = Some(0); // Неоднократно повторим наш тест. loop { match optional { // Если `optional` деструктурируется, выполним следующий блок. Some(i) => { if i > 9 { println!("Больше 9, уходим отсюда!"); optional = None; } else { println!("`i` равен `{:?}`. Попробуем еще раз.", i); optional = Some(i + 1); } // ^ Требует 3 уровня вложенности! }, // Выходим из цикла в случае ошибки деструктуризации: _ => { break; } // ^ Зачем это нужно? Должен быть способ сделать это лучше! } } }

Использование while let делает этот пример намного приятнее:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

enum, Option, and the RFC

Функции

Функции объявляются с помощью ключевого слова fn. Их аргументы имеют явно заданный тип, как у переменных, и, если функция возвращает значение, возвращаемый тип должен быть указан после стрелки ->.

Последнее выражение в функции будет использовано как возвращаемое значение. Так же можно использовать оператор return, чтобы вернуть значение из функции раньше, даже из цикла или оператора if.

Давайте перепишем FizzBuzz используя функции!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Связанные функции и Методы

Некоторые функции связаны с определённым типом. Они бывают двух видов: связанные функции и методы. Связанные функции — это функции, которые обычно определены для типа в целом, а методы — это связанные функции, которые вызываются для конкретного экземпляра типа.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Замыкания

Замыкания в Rust, так же называемые лямбда, это функции, которые замыкают своё окружение. Для примера, замыкание, которое захватывает значение переменной x:

|val| val + x

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

Другие характеристики замыканий включают в себя:

  • использование || вместо () для аргументов.
  • опциональное ограничения тела функции ({}) для одного выражения (в противном случае обязательно).
  • возможность захвата переменных за пределами окружения
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Захват

Замыкания довольно гибкие и делают всё, что требуется, для работы с ними без дополнительных указаний. Это позволяет захватывать переменные гибко, перемещая их или заимствуя, в зависимости от ситуации. Замыкания могут захватывать переменные:

  • по ссылке: &T
  • по изменяемой ссылке: &mut T
  • по значению: T

Преимущественно, они захватывают переменные по ссылке, и используют другие способы только там, где это необходимо.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Использование move перед вертикальными линиями позволяет получить владение над захваченными переменными:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Box и std::mem::drop

Как входные параметры

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

  • Fn: замыкание захватывает по ссылке (&T)
  • FnMut: замыкание захватывает по изменяемой ссылке (&mut T)
  • FnOnce: замыкание захватывает по значению (T)

Компилятор стремится захватывать переменные наименее ограничивающим способом.

Для примера, рассмотрим аргумент, указанный как FnOnce. Это означает, что замыкание может захватывать &T, &mut T, или T, но компилятор в итоге будет выбирать в зависимости от того, как захваченные переменные используются в замыкании.

Это связано с тем, что если перемещение возможно, тогда любой тип заимствования также должен быть возможен. Отметим, что обратное не верно. Если параметр указан как Fn, то захват переменных как &mut T или T недопустим.

В следующем примере попробуйте поменять местами использование Fn, FnMut, и FnOnce, чтобы увидеть результат:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::mem::drop, Fn, FnMut, Обобщения, where и FnOnce

Анонимность типов

Замыкания временно захватывают переменные из окружающих областей видимости. Имеет ли это какие-либо последствия? Конечно. Как видите, использование замыкания в аргументах функции требует обобщённых типов, из-за особенностей реализации замыканий:

#![allow(unused)] fn main() { // `F` должен быть обобщённым типом. fn apply<F>(f: F) where F: FnOnce() { f(); } }

Когда компилятор встречает определение замыкания, он неявно создаёт новую анонимную структуру для хранения захваченных переменных, тем временем реализуя функциональность для этого неизвестного типа, с помощью одного из типажей: Fn, FnMut, или FnOnce. Этот тип присваивается переменной, которая хранится до самого вызова замыкания.

Так как этот новый тип заранее неизвестен, любое его использование в функции потребует обобщённых типов. Тем не менее, неограниченный параметр типа <T> по прежнему будет неоднозначным и недопустимым. Таким образом, ограничение по одному из типажей: Fn, FnMut, или FnOnce (которые он реализует) является достаточным для указания этого типа.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Подробный разбор, Fn, FnMut, и FnOnce

Входные функции

Так как замыкания могут использоваться в аргументах, вы можете ожидать, что то же самое можно сказать и про функции. И это действительно так! Если вы объявляете функцию, принимающую замыкание как аргумент, то любая функция, удовлетворяющая ограничениям типажа этого замыкания, может быть передана как аргумент.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Стоит отметить, что типажи Fn, FnMut и FnOnce указывают, как замыкание захватывает переменные из своей области видимости.

Смотрите также:

Fn, FnMut, и FnOnce

Как выходные параметры

Замыкания могут выступать как в качестве входных параметров, так и в качестве выходных. Однако тип анонимных замыканий по определению не известен, из-за чего для их возврата нам придётся использовать impl Trait.

Для возврата замыкания мы можем использовать такие трейты:

  • Fn
  • FnMut
  • FnOnce

Помимо этого, должно быть использовано ключевое слово move, чтобы сигнализировать о том, что все переменные захватываются по значению. Это необходимо, так как любые захваченные по ссылке значения будут удалены после выхода из функции, оставляя недопустимые ссылки в замыкании.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

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 {} }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::iter::Iterator::any

Поиск через итераторы

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; }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Iterator::find даёт ссылку на элемент. Но если вы хотите получить его индекс, используйте Iterator::position.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::iter::Iterator::find

std::iter::Iterator::find_map

std::iter::Iterator::position

std::iter::Iterator::rposition

Функции высшего порядка

Rust предоставляет Функции Высшего Порядка (ФВП). Это функции, которые получают на вход одну или несколько функций и/или выдают более полезную функцию. ФВП и ленивые итераторы придают языку Rust функциональный оттенок.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Option и Iterator реализуют значительную часть функций высшего порядка..

Расходящиеся функции

Расходящиеся функции никогда не возвращают результат. Они помечены с помощью !, который является пустым типом.

#![allow(unused)] fn main() { fn foo() -> ! { panic!("Этот вызов никогда не вернёт управление."); } }

В отличие от всех других типов, этот не может быть создан, потому что набор всех возможных значений этого типа пуст. Обратите внимание, что он отличается от типа (), который имеет ровно одно возможное значение.

Например, эта функция имеет возвращаемое значение, хотя о нём нет информации.

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. Только публичные элементы модуля могут быть доступны за пределами его области видимости.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Видимость структуры

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

generics и методы

Декларация use

Декларация use используется, чтобы связать полный путь с новым именем, что упрощает доступ.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вы можете использовать ключевое слово as, что импортировать сущности и функции под другим именем:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

super и self

Ключевые слова super и self в пути используются, чтобы устранить неоднозначность между используемыми элементами модуля.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Иерархия файлов

Модули могут быть отображены на иерархию файлов и директорий. Давайте разобьём пример с видимостью модулей на файлы:

$ 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.

Атрибуты

Атрибуты - это метаданные, применяемые к какому-либо модулю, контейнеру или их элементу. Благодаря атрибутам можно:

Когда атрибуты применяются ко всему контейнеру, их синтаксис будет #![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 можно использовать, чтобы отключить данную проверку.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Обратите внимание, что в реальных программах вы должны удалить неиспользуемый код. В этих примерах мы разрешаем оставить неиспользуемый код в некоторых местах — но это только для примера!

Контейнеры

Атрибут crate_type используется, чтобы сказать компилятору, какой контейнер является библиотекой (и каким типом библиотеки), а какой — исполняемым файлом. Атрибут crate_name используется для указания имени контейнера.

Однако важно отметить, что атрибуты crate_type и create_name не имеют значения при использовании пакетного менеджера Cargo. В виду того, что Cargo используется для большинства проектов на Rust, в реальном мире использование crate_type и crate_name достаточно ограничено.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Если мы используем атрибут crate_type, то нам больше нет необходимости передавать компилятору флаг --crate-type.

$ rustc lib.rs $ ls lib* library.rlib

cfg

Условная конфигурация возможна при помощи двух разных операторов:

  • атрибута cfg: #[cfg(...)], который указывается на месте атрибута
  • макроса cfg!: cfg!(...), который можно использовать в условных выражениях

В то время как первый атрибут включает условную компиляцию, второй преобразуется в литералы true или false, позволяя сделать проверку во время исполнения. Оба варианта используют идентичный синтаксис для аргументов.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Reference, cfg!, и macros.

Собственные условия

Некоторые условия, например target_os, предоставляются компилятором. Если мы хотим создать собственные условия, то их необходимо передать компилятору используя флаг --cfg.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Попробуйте запустить без указания флага 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 был определён как структура.

Пример ниже демонстрирует синтаксис в действии:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Структуры

Функции

Тот же набор правил применяется и к функциям: тип T становится обобщённым, когда предшествует <T>.

При использовании обобщённых функций, иногда требуется явно указывать тип данных параметров. Это может быть необходимо в случае, если вызываемая функция возвращает обобщённый тип или у компилятора недостаточно информации для вывода необходимого типа данных.

Вызов функции с явно указанными типами данных параметров выглядит так: fun::<A, B, ...>().

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Функции и структуры

Реализация

Подобно функциям, реализации требуют выполнения некоторых условий, чтобы оставаться обобщёнными.

#![allow(unused)] fn main() { struct S; // Конкретный тип `S` struct GenericVal<T>(T); // Обобщенный тип `GenericVal` // Реализация GenericVal, где мы явно указываем типы данных параметров: impl GenericVal<f32> {} // Указываем тип `f32` impl GenericVal<S> {} // Указываем тип `S`, который мы определили выше // `<T>` должен указываться перед типом, чтобы оставаться обобщённым impl<T> GenericVal<T> {} }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Функции, возвращающие ссылки, impl и struct

Типажи

Конечно типажи тоже могут быть обобщёнными. Здесь мы определяем, тот который повторно реализует типаж Drop как обобщённый метод, чтобы удалить себя и входные данные.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Drop, struct и trait

Ограничения

При работе с обобщениями параметры типа часто должны использовать типажи в качестве ограничений, чтобы определить какие функциональные возможности реализует тип. Например, в следующем примере для печати используется типаж 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]);

Другой эффект ограничения заключается в том, что обобщённые экземпляры имеют доступ к методам типажей, указанных в ограничениях. Например:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Утверждения where также могут использоваться для применения ограничений в некоторых случаях, чтобы добавить выразительности.

Смотрите также:

std::fmt, struct и trait

Пример: пустые ограничения

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::cmp::Eq, std::marker::Copy и трейты

Множественные ограничения

Множественные ограничения по типажу могут быть применены с помощью +. Разные типы разделяются с помощью ,.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::fmt и trait

Утверждения 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:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

RFC, структуры, и типажи

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

Смотрите также:

struct

Ассоциированные элементы

"Ассоциированные элементы" относятся к набору правил, касающихся элементов различных типов. Это расширение для обобщённых типажей, которое позволяет им определить новый элемент внутри себя.

Каждый такой элемент называется ассоциированным типом и предоставляет упрощённый шаблон использования, когда trait является обобщённым для своего контейнера.

Смотрите также:

RFC

Проблема

trait, являющийся обобщённым для своего контейнера, есть требование к спецификации типа - пользователи trait должны специфицировать все обобщённые типы.

В примере ниже, trait Contains позволяет использовать обобщённые типы A и B. Затем этот типаж реализуется для типа Container, в котором A и B специфицированы, как i32, чтобы их можно было использовать в функции fn difference().

Потому что Contains имеет обобщение, мы должны явно указать все обобщённые типы для fn difference(). На практике, мы хотим выразить A и B через входной параметр C. Как вы можете увидеть в следующем разделе, ассоциированные типы предоставляют именно эту возможность.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

struct и trait

Ассоциированные типы

Использование "ассоциированных типов" улучшает общую читаемость кода через локальное перемещение внутренних типов в типаж в качестве выходных типов. Синтаксис для объявления trait будет следующим:

#![allow(unused)] fn main() { // `A` и `B` определены в типаже при помощи ключевого слова `type`. // (Обратите внимание: в данном контексте `type` отличается `type`, который // используется в псевдонимах). trait Contains { type A; type B; // Обновлённый синтаксис для обращения к этим двум ассоциированным типам. fn contains(&self, &Self::A, &Self::B) -> bool; } }

Обратите внимание, что функции, использующие trait Contains больше не требуют указания A или B:

// Без использования ассоциированных типов fn difference<A, B, C>(container: &C) -> i32 where C: Contains<A, B> { ... } // С использованием ассоциированных типов fn difference<C: Contains>(container: &C) -> i32 { ... }

Давайте перепишем пример их предыдущего раздела с использованием ассоциированных типов:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

PhantomData-параметры

Параметры фантомного типа - единственное, что не отображается во время выполнения, но проверяется статически (и только статически) во время компиляции.

Типы данных могут использовать дополнительные обобщённые типы в качестве параметров-маркеров или для выполнения проверки типов во время компиляции. Эти дополнительные параметры не сохраняют значения и не имеют поведения во время выполнения.

В следующем примере мы совместили std::marker::PhantomData и концепцию параметров фантомных типов для создания кортежей разных типов.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

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>; ... }

Вся реализация:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Заимствование (&), ограничения (X: Y), перечисления, impl & self, перегрузка, ref, типажи (X for Y) и кортежные структуры.

Правила области видимости

Области видимости играют важную роль во владении, заимствовании и времени жизни. То есть, они указывают компилятору, когда заимствования действительны, когда ресурсы могут быть освобождены, и когда переменные создаются или уничтожаются.

RAII

Переменные в Rust не только держат данные в стеке, они также могут владеть ресурсами; к примеру, Box<T> владеет памятью в куче. Поскольку Rust строго придерживается идиоме RAII, то когда объект выходит за зону видимости, вызывается его деструктор, а ресурс, которым он владеет освобождается.

Такое поведение защищает от багов, связанных с утечкой ресурсов. Вам больше никогда не потребуется вручную освобождать память или же беспокоиться об её утечках! Небольшой пример:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Конечно, мы можем убедиться, что в нашей программе нет ошибок с памятью, используя 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 выходит за пределы области действия, будет вызван пользовательский деструктор.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Упаковка

Владение и перемещение

Поскольку переменные ответственны за освобождение своих ресурсов, ресурсы могут иметь лишь одного владельца. Это ограничение предотвращает возможность высвобождения ресурсов более одно раза. Обратите внимание, что не все переменные владеют своим ресурсом (например, ссылки).

При присваивании (let x = y) или при передаче функции аргумента по значению (foo(x)), владение ресурсами передаётся. В языке Rust это называется перемещением.

После перемещения ресурсов, переменная, владевшая ресурсами ранее, не может быть использована. Это предотвращает создание висячих указателей.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Изменяемость

Изменяемость данных может быть изменена при передаче владения.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Заимствование

Большую часть времени мы хотим обращаться к данным без получения владения над ними. Для этого Rust предоставляет механизм заимствования Вместо передачи объектов по значению (T), объекты могут быть переданы по ссылке (&T).

Компилятор статически гарантирует, что ссылки всегда указывают на допустимые объекты посредством проверки заимствований. К примеру, исходный объект не может быть уничтожен, пока существуют ссылки на него.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Изменяемость

Изменяемые данные могут быть заимствованы с возможностью изменения при помощи &mut T. Это называется изменяемая ссылка и даёт заимствующему возможность чтения и записи. В отличие от неё, &T заимствует данные через неизменяемую ссылку и заимствующий может читать данные, но не может модифицировать их:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

static

Алиасинг

Данные могут быть заимствованы без возможности изменения любое количество раз, но пока такое заимствование существует, оригинальные данные не могут быть заимствованы с возможностью изменения. С другой стороны, одновременно может быть только одно изменяемое заимствование. Исходные данные могут быть снова заимствованы только после того, как изменяемая ссылка выйдет из области видимости.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ref паттерн

Когда мы используем сопоставление с образцом или деструктурируем при помощи let, можно использовать ключевое слово ref для получения ссылки на поле структуры или кортежа. Пример ниже показывает несколько случаев, когда это может быть полезно:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Времена жизни

Время жизни - это конструкция, которую компилятор (или более конкретно, его анализатор заимствований) использует, чтобы убедиться, что все заимствования действительны. В частности время жизни переменной начинается с момента её создания и заканчивается когда она уничтожается. Времена жизни и области видимости упоминаются часто вместе, но они не совпадают.

Возьмём, например, случай когда мы заимствуем переменную через &. Срок действия заимствования определяется местом его объявления. В результате, заимствование действительно до тех пор, пока оно не закончится или пока кредитор не будет уничтожен. Однако, область заимствования определяется местом использования ссылки.

В следующем примере и в остальной части этого раздела мы увидим, как времена жизни связаны с областями видимости, а также как они различаются.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Обратите внимание, что для меток времени жизни не назначаются имена или типы. Это ограничивает то, как время жизни будет использоваться, как мы увидим далее.

Явное аннотирование

Анализатор заимствований использует явные аннотации времён жизни для определения того, как долго ссылки будут действительны. В случаях, когда времена жизни не скрыты1, Rust требует их явного аннотирования, чтобы определить какое у ссылки должно быть время жизни. Для явного аннотирования времени жизни используется синтаксис с символом апострофа, как тут:

foo<'a> // `foo` имеет параметр времени жизни `'a`

Подобно замыканиям, явное использование времён жизни требует обобщённого параметра. Кроме того, такой синтаксис показывает, что время жизни foo не может превышать 'a. Явная аннотация для типа имеет форму &'a T, где 'a уже задана.

В случаях со множественными временами жизни, синтаксис будет подобен следующему:

foo<'a, 'b> // `foo` имеет параметры времён жизни `'a` и `'b`

В данном случае, время жизни foo не может превышать ни 'a, ни 'b.

Рассмотрим следующий пример, в котором используется явная аннотация времён жизни:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

сокрытие позволяет скрыть аннотации времён жизни, но они всё же присутствуют.

Смотрите также:

Обобщения и замыкания

Функции

Сигнатуры функции с указанием времени жизни имеют некоторые ограничения:

  • любая ссылка должна иметь аннотированное время жизни
  • любая возвращаемая ссылка должна иметь то же время жизни, что входящая ссылка или static.

Кроме того, обратите внимание, что возврат ссылок из функции, которая не имеет ссылок во входных аргументах, запрещён, если он приведёт к возвращению ссылок на недопустимые данные. В следующем примере показаны некоторые действительные формы функции со временем жизни:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

функции

Методы

Методы аннотируются аналогично функциям:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Методы

Структуры

Аннотирование времени жизни в структурах аналогично функциям:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Структуры

Типажи

Аннотирование времён жизни для методов типажей в основном похоже на аннотирование в функциях. Обратите внимание, что impl также может иметь аннотацию времени жизни.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

trait

Ограничения

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

  1. T: 'a: Все ссылки в T должны пережить время жизни 'a.
  2. T: Trait + 'a: Тип T должен реализовать типаж Trait и все ссылки на T должны пережить 'a.

Пример ниже демонстрирует синтаксис в действии и использует его после ключевого слова where:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Обобщения, ограничения в обобщениях и множественные ограничения в обобщениях

Приведение (coercion)

Длинное время жизни может быть приведено к короткому, благодаря чему всё работает нормально внутри области видимости, хотя кажется, что не должно. Это достигается за счёт того что компилятор Rust выполняет приведение времён жизни и за счёт объявления разницы между ними разницы:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Static

В Rust есть несколько зарезервированных имён времени жизни. Одно из них — 'static . Вы можете столкнуться с ним в двух случаях:

#![allow(unused)] fn main() { // Ссылка с временем жизни 'static: let s: &'static str = "hello world"; // 'static как часть ограничения типажа: fn generic<T>(x: T) where T: 'static {} }

Оба они похожи, но немного отличаются, что часто вызывает путаницу при изучении Rust. Вот несколько примеров для каждой ситуации:

Время жизни ссылки

'static как время жизни ссылки означает, что данные, на которые указывает ссылка, живут в течение всего времени жизни работающей программы. В тоже время, этот срок может быть сокращён принудительно.

Есть два способа создать переменную с временем жизни 'static, и оба они лежат в области "только для чтения" бинарного файла:

  • Создание константы с ключевым словом static .
  • Создание строкового литерала, который имеет тип &'static str.

Рассмотрим следующий пример, который показывает оба метода:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Ограничение типажа

Как ограничение типажа, это означает, что тип не содержит нестатических ссылок. Например. получатель может хранить тип столько, сколько захочет, и тип никогда не станет недействительным, пока получатель его не удалит.

Важно понимать, что это означает, что любые владеющие данные всегда проходят проверку на ограничение времени жизни 'static, но ссылка на эти владеющие данные обычно не проходит:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Компилятор скажет вам:

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

Смотрите также:

'static константы

Сокрытие

Некоторые шаблоны времён жизни достаточно общие и поэтому анализатор заимствований может позволить вам опустить их чтобы ускорить написание кода и увеличить его читаемость. Это известно как сокрытие времён жизни. Сокрытие появилось в Rust, исключительно из-за того, что они применяются к общим шаблонам.

Следующий код показывает несколько примеров сокрытия. Для более полного описания сокрытия, обратитесь к главе про [a0}сокрытие времён жизни в TRPL.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

сокрытие

Типажи (трейты)

Типаж (trait, трейт) - это набор методов, определённых для неизвестного типа: Self. Они могут получать доступ к другим методам, которые были объявлены в том же типаже.

Типажи могут быть реализованы для любых типов данных. В примере ниже, мы определили группу методов Animal. Типаж Animal реализован для типа данных Sheep, что позволяет использовать методы из Animal внутри Sheep.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Атрибут Derive

Компилятор способен предоставить основные реализации для некоторых типажей с помощью атрибута #[derive]. Эти типажи могут быть реализованы вручную, если необходимо более сложное поведение.

Ниже приводится список выводимых типажей:

  • Типажи сравнения:Eq, PartialEq, Ord, PartialOrd
  • Clone, для создания T из &T с помощью копии.
  • Copy, чтобы создать тип семантикой копирования, вместо семантики перемещения.
  • Hash, чтобы вычислить хеш из &T.
  • Default, чтобы создать пустой экземпляр типа данных.
  • Debug, чтобы отформатировать значение с помощью {:?}.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

derive

Возврат типажа с dyn

Компилятору Rust нужно знать сколько места занимает результат каждой функции. Это обозначает, что все ваши функции имеют конкретный тип результата. В отличие от других языком, если у вас есть типаж, например Animal, то вы не можете написать функцию, которая вернёт Animal, по той причине, что разные реализации этого типажа будут занимать разное количество памяти.

Однако есть простой обходной путь. Вместо не посредственного возврата типажа-объекта, наши функции могут возвращать Box, который содержит некоторую реализацию Animal. box - это просто ссылка на какую-то память в куче. Так как размер ссылки известен статически и компилятор может гарантировать, что она указывает на аллоцированную в куче реализацию, мы можем вернуть типаж из нашей функции!

Rust пытается быть предельно явным, когда он выделяет память в куче. Так что если ваша функция возвращает указатель-на-типаж-в-куче, вы должны дописать к возвращаемому типу ключевое слово dyn, например Box<dyn Animal>.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Перегрузка операторов

В Rust, множество операторов могут быть перегружены с помощью типажей. То есть, некоторые операторы могут использоваться для выполнения различных задач на основе вводимых аргументов. Это возможно, потому что операторы являются синтаксическим сахаром для вызова методов. Например, оператор + в a + b вызывает метод add (как в a.add(b)). Метод add является частью типажа Add. Следовательно, оператор + могут использовать все, кто реализуют типаж Add.

Список типажей, таких как Add, которые перегружают операторы, доступен здесь.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Add, Syntax Index

Типаж Drop

Типаж Drop имеет только один метод: drop, который вызывается автоматически, когда объект выходит из области видимости. Основное применение типажа Drop заключается в том, чтобы освободить ресурсы, которыми владеет экземпляр реализации.

Box, Vec, String, File, и Process - это некоторые примеры типов, которые реализуют типаж Drop для освобождения ресурсов. Типаж Drop также может быть реализован вручную для любых индивидуальных типов данных.

В следующем примере мы добавим вывод в консоль к функции drop, чтобы было видно, когда она вызывается.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Итераторы

Типаж Iterator используется для итерирования по коллекциям, таким как массивы.

Типаж требует определить метод next, для получения следующего элемента. Данный метод в блоке impl может быть определён вручную или автоматически (как в массивах и диапазонах).

Для удобства использования, например в цикле for, некоторые коллекции превращаются в итераторы с помощью метода .into_iterator().

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

impl Trait

Если ваша функция возвращает тип, реализующий MyTrait, вы можете записать возвращаемый тип как -> impl MyTrait. Это может достаточно сильно упростить сигнатуру вашей функции!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Что более важно, некоторые типы в Rust не могут быть записаны. Например, каждое замыкание имеет свой собственный безымянный тип. До появления синтаксиса impl Trait, чтобы вернуть замыкание, вы должны были аллоцировать её в куче. Но теперь вы можете сделать это всё статически, например так:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вы также можете использовать impl Trait для возврата итератора, который использует замыкания map или filter! Это упрощает использование map и filter. Из-за того, что замыкание не имеет имени, вы не можете явно записать возвращаемый тип для функции, возвращающей итератор с замыканием. Но с impl Trait вы можете сделать это:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Типаж Clone

При работе с ресурсами, стандартным поведением является передача их (ресурсов) в ходе выполнения или вызов функции. Однако, иногда нам нужно также объявить копию ресурса.

Типаж Clone помогает нам сделать именно это. Чаще всего, мы можем использовать метод .clone() объявленный типажом Clone.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Супертрейты

В Rust нет "наследования", но вы можете объявить трейт, который будет надмножеством для другого. Например:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Глава "The Rust Programming Language" о супертрейтах

Устранение неоднозначности в перекрывающихся трейтах

Тип может реализовывать много разных трейтов. Что если два трейта будут требовать метод с одним и тем же именем? например, много трейтов могут иметь метод get(), которые так же могут иметь разные возвращаемые типы!

Хорошие новости: благодаря тому, что каждая реализация трейта имеет собственный impl-блок, становится яснее для какого трейта мы написали метод get.

А что будет, когда придёт время вызвать эти методы? Чтобы устранить неоднозначность, мы можем использовать полное имя метода (Fully Qualified Syntax).

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Глава "The Rust Programming Language" о полном имени методов (Fully Qualified syntax)

macro_rules!

Rust предоставляет мощную систему макросов, которая позволяет использовать метапрограммирование. Как вы могли видеть в предыдущих главах, макросы выглядят как функции, но их имя заканчивается восклицательным знаком (!). Вместо вызова функции, макросы расширяются в исходный код, который впоследствии компилируется с остальной частью программы. Однако, в отличие от макросов на C и других языках, макросы Rust расширяются в абстрактные синтаксические деревья, а не в подстановку строк, поэтому Вы не получаете неожиданных ошибок приоритета операций.

Макросы создаются с помощью макроса macro_rules!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Так почему же макросы полезны?

  1. Не повторяйтесь. Есть много случаев, когда вам может понадобиться подобная функциональность в нескольких местах, но с различными типами. Чаще всего написание макроса - это полезный способ избежать повторения кода. (Подробнее об этом позже)

  2. Предметно-ориентированные языки. Макросы позволяют определить специальный синтаксис для конкретной цели. (Подробнее об этом позже)

  3. Вариативные интерфейсы. Иногда вы хотите объявить интерфейс, принимающий переменное число аргументов. Например, println!, принимающий такое же число аргументов, сколько объявлено в строке с форматом. (Подробнее об этом позже)

Синтаксис

В следующем подразделе мы посмотрим как в Rust объявить макрос. Есть три основные идеи:

Указатели

Аргументы макроса имеют префикс знака доллара $ и тип аннотируется с помощью указателей фрагмента:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Это список всех указателей:

  • block
  • expr используют для обозначения выражений
  • ident используют для обозначения имени переменной/функции
  • item
  • literal используется для литеральных констант
  • pat (образец)
  • path
  • stmt (единственный оператор)
  • tt (единственное дерево лексем)
  • ty (тип)
  • vis (спецификатор видимости)

Полный список указателей, вы можете увидеть в Rust Reference.

Перегрузка

Макросы могут быть перегружены, принимая различные комбинации аргументов. В этом плане, macro_rules! может работать аналогично блоку сопоставления (match):

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Повторение

Макросы могут использовать знак + в списке аргументов, чтобы указать, какие аргументы могут повторяться хоть один раз, или знак *, чтобы указать, какие аргументы могут повторяться ноль или несколько раз.

В следующем примере, шаблон, окружённый $(...),+ будет сопоставлять одно или несколько выражений, разделённых запятыми. Также обратите внимание, что точка с запятой является необязательной в последнем случае.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

DRY (Не повторяйся)

Макросы позволяют писать DRY код, путём разделения общих частей функций и/или набор тестов. Вот пример, который реализует и тестирует операторы +=, *= и -= на Vec<T>:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ 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 для калькулятора. Я хотел бы предоставить выражение и вывести результат в консоль.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вывод:

1 + 2 = 3 (1 + 2) * (3 / 4) = 0

Это очень простой пример, но можно разработать и гораздо более сложные интерфейсы, такие как lazy_static или clap.

Также обратите внимание на две пары скобок в макросе. Внешняя пара является частью синтаксиса macro_rules!, в дополнение к () или [].

Вариативные интерфейсы

Интерфейсы с переменным числом параметров (вариативные интерфейсы) принимают произвольное число аргументов. Например, println! может принимать произвольное число аргументов, как определено в формате строки.

Мы можем расширить наш макрос calculate! из предыдущей главы, чтобы он имел вариативный интерфейс:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вывод:

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 в случае ошибки:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Option и unwrap

В последнем примере мы показали, что можем вызвать сбой программы по своему желанию. Мы сказали нашей программе вызвать panic, если мы выпьем сладкий лимонад. Но что, если ожидаем какой-то напиток, но не получаем его? Этот случай тоже плохой, так что и он должен быть обработан!

Мы могли бы сравнить значение с пустой строкой ("") так же, как мы сделали это с лимонадом. Поскольку мы используем Rust, пусть компилятор сам укажет нам случаи, когда напитка нет.

Перечисление (enum) из стандартной библиотеки (std), называющееся Option<T>, используется, когда значение может отсутствовать. Оно может находиться в одном из двух состояний:

  • Some(T): элемент типа T найден
  • None: элемент не найден

Эти случаи могут быть или явно обработаны с помощью match, или неявно с unwrap. Неявная обработка либо вернёт внутренний элемент, либо вызовет panic.

Обратите внимание, что можно вручную настроить сообщение выдаваемое при панике с помощью expect, в противном случае unwrap оставляет нам менее понятный вывод, чем явная обработка. В следующем примере явная обработка даёт более контролируемый результат, при этом сохраняется возможность паниковать, если это необходимо.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Разворачивание Option с ?

Вы можете развернуть Option с использованием match, но часто проще бывает использовать оператор?. Если x - Option, то выражениеx? вернёт значение переменной, если x - Some, в противном же случае оно завершит выполнение текущей функции и вернёт None.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Чтобы ваш код был более читаемым, вы можете составить цепочку из нескольких ?.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Комбинаторы: map

match - возможный метод для работы с Option. Однако постоянное его использование может быть утомительным, особенно с операциями, которые получают только проверенные данные. В этом случае можно использовать комбинаторы, которые позволяют управлять потоком выполнения в модульном режиме.

Option имеет встроенный метод, зовущийся map(), комбинатор для простого преобразования Some -> Some и None -> None. Для большей гибкости, несколько вызовов map() могут быть связаны друг с другом в цепочку.

В следующем примере, process() заменяет все предшествующие ей функции, оставаясь, при этом, компактной:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Замыкания, 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().

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Замыкания, 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():

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

При неудаче, parse() оставляет на с ошибкой, с которой unwrap() вызывает panic. Дополнительно, panic завершает нашу программу и предоставляет неприятное сообщение об ошибке.

Для повышения качества наших сообщений об ошибка, мы должны более явно указать возвращаемый тип и рассмотреть возможной явной обработки ошибок.

Использование Result в main

Также Result может быть возвращаемым типом функции main, если это указано явно. Обычно функция main имеют следующую форму:

fn main() { println!("Hello World!"); }

Однако main также может и возвращать тип Result. Если ошибка происходит в пределах функции main, то она возвращает код ошибки и выводит отладочное представление ошибки (используя типаж Debug). Следующий пример показывает такой сценарий и затрагивает аспекты, описанные в последующем разделе.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

map для Result

Паника в предыдущем примере делает код ненадёжным. Обычно, мы хотим вернуть ошибку вызывающей стороне, чтобы уже она решала, как с ней поступить.

Первое, что нам нужно знать - это с каким типом ошибки мы работаем. Для определения типа Err, мы посмотрим на parse(), реализованную с типажом FromStr для i32. В результате, тип Err указан как ParseIntError.

В примере ниже, простой match делает код более громоздким.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

К счастью, map, and_then многие другие комбинаторы Option также реализованы и для Result. Документация по Result содержит полный их список.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Псевдонимы для Result

Как насчёт случая, когда мы хотим использовать конкретный тип Result много раз? Напомним, что Rust позволяет нам создавать псевдонимы. Мы можем удобно объявить псевдоним для конкретного Result.

Особенно полезным может быть создание псевдонимов на уровне модулей. Ошибки, найденные в конкретном модуле, часто имеют один и тот же тип Err, поэтому один псевдоним может лаконично объявить все ассоциированные Results. Это настолько полезно, что библиотека std обеспечивает даже один: io::Result!

Ниже приведён краткий пример для демонстрации синтаксиса:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

io::Result

Ранний выход

В предыдущем примере мы явно обработали ошибки при помощи комбинаторов. Другой способ сделать это - использовать комбинацию выражения match и раннего выхода.

Таким образом мы просто можем остановить работу функции и вернуть ошибку, если она произошла. Для некоторых, такой код будет легче в чтении и написании. Посмотрите код из предыдущего примера, переписанный с использованием раннего выхода:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

На данный момент, мы изучили обработку ошибок при помощи комбинаторов и раннего выхода. Мы хотим избежать паники, но явная обработка всех ошибок достаточно громоздка.

В следующем разделе, мы познакомимся с ? для случаев, где нам просто хотим сделать unwrap без возможности вызова panic.

Представляем: ?

Иногда мы хотим получить простоту unwrap, но без panic. До текущего момента unwrap заставлял нас делать всё больше и больше, в то время как мы хотели только извлечь переменную. Для этих целей был введён ?.

При обнаружении Err, можно выполнить два действия:

  1. panic!, который мы решили по возможности избегать
  2. return так как возврат Err говорит о том, что мы её не обрабатывали

? почти1 эквивалентен unwrap, который при Err делает return вместо panic. Давайте посмотрим как мы можем упростить наш пример, использующий комбинаторы:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Макрос try!

До появления ?, аналогичная функциональность была доступна через макрос try!. Сейчас рекомендуется использовать оператор ?, но вы до сих пор можете найти try!, когда просматриваете старый код. Функция multiply из предыдущего примера с использованием try! будет выглядеть следующим образом:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

Посмотрите главу "Другие способы использования ?" для большей информации.

Несколько типов ошибок

Предыдущие примеры всегда были очень удобны: Result взаимодействовали с другими Result, а Option - с другими Option.

Иногда Option необходимо взаимодействовать с Result, или Result<T, Error1> с Result<T, Error2>. В этих случаях, нам нужно управлять этими разными типами ошибок таким образом, чтобы можно было их компоновать и легко взаимодействовать с ними.

В следующем коде, два варианта unwrap генерируют разные типы ошибок. Vec::first возвращает Option, в то время как parse::<i32> возвращает Result<i32, ParseIntError>:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

В следующих главах мы рассмотрим различные стратегии обработки этих типов проблем.

Извлечение Result из Option

Наиболее простой способ обработки ошибок разных типов - это встраивание их друг в друга.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Бывает, мы хотим приостановить работу при ошибке (как при помощи оператора ?), но продолжать работать, если Option None. Есть пара комбинаторов, которые поменяют местами Result и Option.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Объявление типа ошибки

Иногда для упрощения кода необходимо скрыть все типы ошибок за какой-то одной ошибкой. Мы скроем их за пользовательской ошибкой.

Rust позволяет нам определить наш собственный тип ошибок. В общем случае "хороший" тип ошибки должен:

  • Представлять разные ошибки с таким же типом
  • Предоставлять хорошее сообщение об ошибке пользователю
  • Легко сравниваться с другими типами
    • Хорошо: Err(EmptyVec)
    • Плохо: Err("Пожалуйста, используйте вектор хотя бы с одним элементом".to_owned())
  • Содержать информацию об ошибке
    • Хорошо: Err(BadChar(c, position))
    • Плохо: Err("+ не может быть использован в данном месте".to_owned())
  • Хорошо сочетаться с другими ошибками
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Упаковка ошибок (Box)

Чтобы написать простой код и при этом использовать оригинальные ошибки, необходимо упаковать (Box) их. Минусом данного способа является то, что тип ошибок известен только во время выполнения программы, а не определён статически.

Стандартная библиотека помогает упаковывать наши ошибки. Это достигается за счёт того, что для Box реализована конвертация из любого типа, реализующего типаж Error, в типаж-объект Box<Error> через From.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

Динамическая диспетчеризация и типаж 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:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Сейчас код выглядит довольно чисто. По сравнению с panic, это похоже на замену вызова unwrap на ? за исключением того, что возвращаемый тип будет Result. В результате, он может быть обработан уровнем выше.

Смотрите также:

From::from и ?

Оборачивание ошибок

Альтернативой упаковке ошибок является оборачивание их в ваш собственный тип.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Это добавляет чуть больше шаблонного кода для обработки ошибок и может быть не нужно всем приложениям. Есть библиотеки, которые могут избавить вас от написания этого шаблонного кода.

Смотрите также:

From::from и Enums

Итерирование по Result

При работе метода Iter::map может случиться ошибка, например:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Давайте рассмотрим стратегии обработки этого.

Игнорирование неудачных элементов с filter_map()

filter_map вызывает функцию и отфильтровывает результаты, вернувшие None.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Сбой всей операции с collect()

Result реализует FromIter так что вектор из результатов (Vec<Result<T, E>>) может быть преобразован в результат с вектором (Result<Vec<T>, E>). Если будет найдена хотя бы одна Result::Err, итерирование завершится.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Та же самая техника может использоваться с Option.

Сбор всех корректных значений и ошибок с помощью partition()

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Если вы посмотрите на результаты работы, вы заметите, что они всё ещё обёрнуты в Result. Потребуется немного больше шаблонного кода, чтобы получить нужный результат.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Типы стандартной библиотеки

Стандартная библиотека (std) предоставляет множество пользовательских типов, которые значительно расширяют примитивы. Некоторые из них:

  • расширяемые строки String: "hello world"
  • динамический массивы: [1, 2, 3]
  • опциональные типы: Option<i32>
  • типы для обработки ошибок: Result<i32, i32>
  • указатели на объекты в куче: Box<i32>

Смотрите также:

Примитивы и стандартная библиотека

Box, стек и куча

Все значения в Rust по умолчанию располагаются на стеке. Значения могут быть упакованы (созданы в куче) при помощи Box<T>. Box - это умный указатель на расположенное в куче значение типа T. Когда Box покидает область видимости, вызывается его деструктор, который уничтожает внутренний объект, и занятая им память в куче освобождается.

Упакованные значения могут быть разыменованы с помощью операции *. Эта операция убирает один уровень косвенности.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вектора

Вектора - это массивы изменяемого размера. Их размер, как и у срезов, не известен во время компиляции, но он может расти или уменьшаться в любое время. Вектора представляются при помощи 3 параметров:

  • указатель на данные
  • длина
  • вместимость

Вместимость показывает сколько памяти зарезервировано для вектора. Вектор может расти до тех пор, пока его длина меньше вместимости. Если при следующей вставке порог может быть превышен, под вектор выделяется больше памяти и данные переносятся в новый вектор.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Подробную информацию о методах объекта Vec можно почитать в разделе модуля std::vec

Строки

В Rust есть два типа строк: String и &str.

String сохраняется как вектор байт (Vec<u8>), но с гарантией, что это всегда будет действительная UTF-8 последовательность. String выделяется в куче, расширяемая и не заканчивается нулевым байтом (не null-terminated).

&str - это срез (&[u8]), который всегда указывает на действительную UTF-8 последовательность, и является отображением String, так же как и &[T] - отображение Vec<T>.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Больше методов str и String вы можете найти в описании модулей std::str и std::string.

Литералы и экранирование

Есть несколько способов написать строковый литерал со специальными символами в нём. Все способы приведут к одной и той же строке, так что лучше использовать тот способ, который легче всего написать. Аналогично все способы записать строковый литера из байтов в итоге дадут &[u8; N].

Обычно специальные символы экранируются с помощью обратной косой черты: \. В этом случае вы можете добавить в вашу строку любые символы, даже непечатаемые и те, которые вы не знаете как набрать. Если вы хотите добавить обратную косую черту, экранируйте его с помощью ещё одной: \\.

Строковые или символьные разделители литералов (кавычки, встречающиеся внутри другого литерала, должны быть экранированы: "\"", '.'.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Иногда приходится экранировать слишком много символов или легче записать строку как она есть. В этот момент в игру вступают сырые строковые литералы.

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.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Result

Раньше мы видели, что в качестве возвращаемого значения из функции, которая может завершиться с ошибкой, можно использовать перечисление Option, в котором None будет обозначать неудачу. Однако иногда важно понять почему операция потерпела неудачу. Для этого у нас есть перечисление Result.

Перечисление Result<T, E> имеет два варианта:

  • Ok(value), который обозначает, что операция успешно завершилась, и оборачивает значение (value), возвращаемое операцией (value имеет тип T).
  • Err(why), который показывает, что операция потерпела неудачу, оборачивает значение ошибки (причину, why), которое (надеемся) описывает причину неудачи. why имеет тип E.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

?

Разбор цепочки результатов с использованием match может стать довольно неопрятной, к счастью, с помощью оператора ? можно сделать разбор снова красивым. ? используется в конце выражения, возвращающего Result и эквивалентен выражению match, в котором ветка Err(err) разворачивается в Err(From::from(err)), а ветка Ok(ok) во внутреннее значение (ok).

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Обязательно посмотрите документацию, так как есть много методов для работы с Result.

panic!

Макрос panic! используется для генерации паники и раскрутки стека. Во время раскрутки стека, среда выполнения возьмёт на себя всю ответственность по освобождению ресурсов, которыми владеет текущий поток, вызывая деструкторы всех объектов.

Так как в данном случае мы имеем дело с однопоточной программой, panic! заставит программу вывести сообщение с ошибкой и завершится.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Давайте убедимся, что 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() для получения хэш-карты с начальной вместимостью по умолчанию (рекомендуется).

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Для большей информации о том, как работает хеширование и хэш-карты (который иногда называются хэш-таблицами), вы можете обратиться к 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, давайте попробуем реализовать очень простую систему авторизации пользователей:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

HashSet

Рассмотрим HashSet как HashMap в котором мы заботимся только о ключах (в действительности, HashSet<T> - это просто адаптер к HashMap<T, ()>).

"Какой в этом смысл?", - спросите вы. - "Я бы мог просто хранить ключи в Vec."

Уникальная особенность HashSet в том, что он гарантирует, что в нём не содержится повторяющихся элементом. Это условие выполняет любой набор (set). HashSet - всего лишь одна реализация (смотрите также: BTreeSet).

Если вы вставите значение, которое уже содержится в HashSet, (например, новое значение равно существующему значению и они оба имеют одинаковый хэш), то новое значение заменит старое.

Это хорошо подходит для случаев, когда вы не хотите иметь в коллекции больше одного "чего-либо" или когда вам необходимо знать имеете ли вы что-либо.

Но наборы могут делать гораздо более.

Наборы имеют 4 основные операции (все вызовы вернут итератор):

  • union: получить все уникальные элементы из обоих наборов.

  • difference: получить все элементы, представленные в первом наборе, но отсутствующие во втором.

  • intersection: получить только те элементы, которые присутствуют в обоих наборах.

  • symmetric_difference: получить элементы содержащиеся либо только в первом наборе, либо только во втором, но не в обоих (xor).

Попробуем эти методы в следующем примере:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(Пример адаптирован из документации)

Rc

Когда необходимо множественное владение, может использоваться Rc (счётчик ссылок). Rc отслеживает количество ссылок, то есть количество владельцев значения, сохранённого внутри Rc.

Счётчик ссылок в Rc увеличивается на 1 каждый
раз, когда Rc клонируется, и уменьшается на 1, когда
любой из клонов Rc выходит из области видимости и удаляется. Когда
количество ссылок в Rc становится равным нулю,
т.е. когда владельцев больше нет, и сам Rc, и значение внутри него удаляются.

При клонировании Rc никогда не делается глубокая копия. Клонирование лишь создаёт ещё один указатель на обёрнутое значение и увеличивает счётчик.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Смотрите также:

std::rc and std::sync::arc.

Разное в стандартной библиотеке

Стандартная библиотека предоставляет много других типов, позволяющих работать с такими вещами как например:

  • Потоки
  • Каналы
  • Операции файлового ввода/вывода

Они расширяют возможности, которые предоставляют примитивы.

Смотрите также:

примитивы и стандартная библиотека

Потоки

Rust предоставляет механизм для создания собственных потоков операционной системы через функцию spawn. Аргументом этой функции является замыкание, которое принимает владение захваченным ею окружением.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Эти потоки будут запланированы ОС.

Пример: map-reduce

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

Стандартная библиотека предоставляет отличные примитивы для работы потоками из коробки. Они в сочетании с концепцией владения и правилами алиасинга в Rust, автоматически предотвращают гонки данных.

Правила алиасинга (одна уникальная ссылка на запись или много ссылок на чтение) автоматически не позволяет вам манипулировать состоянием, которое видно другим потокам. (Где синхронизация необходима, есть примитивы синхронизации, такие как mutex (мьютексы) или channel (каналы).)

В этом примере мы вычислим сумму всех цифр в блоке чисел. Мы сделаем это, разбив куски блока на разные потоки. Каждый поток будет суммировать свой крошечный блок цифр, и впоследствии мы будем суммировать промежуточные суммы, полученные каждым потоком.

Обратите внимание на то, что хоть мы и передаём ссылки через границы потоков, Rust понимает, что мы только передаём неизменяемые ссылки, которые можно только читать, и что из-за этого не может быть никакой небезопасности и гонок данных. Так как мы перемещаем (move) сегменты данных в поток, Rust также уверен, что данные будут жить до тех пор, пока поток не завершится, и висящих указателей не появится.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Назначения

Не стоит позволять числу наших потоков быть зависимом от введённых пользователем данных. Что если пользователь решит вставить много пробелов? Мы действительно хотим создать 2000 потоков? Измените программу так, чтобы данные разбивались на ограниченное число блоков, объявленных статической константой в начале программы.

Смотрите также:

Каналы

Rust предоставляет асинхронные каналы (channel) для взаимодействия между потоками. Каналы обеспечивают однонаправленную передачу информации между двумя конечными точками: отправителем (Sender) и получателем (Receiver).

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Path

Структура Path представляет пути к файлу в файловой системе. Есть два вида Path: posix::Path, для UNIX - подобных систем, и windows::Path, для Windows. В прелюдии экспортируется соответствующий платформозависимый вариант Path.

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

Обратите внимание, что внутренне представление Path не является UTF-8 строкой, но вместо этого хранит вектор байт (Vec<u8>). Следовательно, преобразование Path в &str не бесплатно и может закончиться неудачей (возвращается Option).

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Не забудьте проверить остальные методы Path (posix::Path или windows::Path) и структуры Metadata.

Смотрите также:

OsStr и Metadata.

Файловый ввод-вывод

Структура File представляет открытый файл (она является обёрткой над файловым дескриптором) и даёт возможность чтения/записи этого файла.

Из-за того, что многие вещи могут пойти не так в процессе файлового ввода-вывода, все методы File возвращают тип io::Result<T>, который является псевдонимом для Result<T, io::Error>.

Это делает явными ошибки всех операций ввода-вывода. Благодаря этому, программист может увидеть все пути отказов и обрабатывать их упреждающей форме.

open

Статический метод open может использоваться для открытия файла в режиме только для чтения.

Структура File владеет ресурсом, файловым дескриптором, и заботится о том, чтобы он был закрыт, когда структура удаляется из памяти.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вот ожидаемый результат:

$ echo "Hello World!" > hello.txt $ rustc open.rs && ./open hello.txt содержит: Hello World!

(Рекомендуем протестировать предыдущий пример при различных условиях сбоев: файл hello.txt не существует или hello.txt не читаемый и другое)

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 - это строитель процесса.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(Рекомендуется попробовать предыдущий пример с неправильным флагом обращения к 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) }

Смотрите также:

cfg!

Аргументы программы

Стандартная библиотека

Аргументы командной строки могут быть доступны при помощи std::env::args, который возвращает итератор, который выдаёт String для каждого аргумента:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ ./args 1 2 3 Мой путь ./args. У меня 3 аргумента: ["1", "2", "3"].

Крейты

В качестве альтернативы, существует несколько крейтов, которые предоставляют дополнительную функциональность при создании приложений командной сроки. Rust Cookbook показывает лучшие практики, как использовать один из самых популярных крейтов для аргументов командной строки, clap.

Парсинг аргументов

Сопоставление может быть использовано для разбора простых аргументов:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ ./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 поддерживает указание дополнительных зависимостей для тестов:

Смотрите также:

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<()>, что позволяет использовать в них ?! Это может сделать их более краткими.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Для дополнительной информации смотрите "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.

#![allow(unused)] fn main() { pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 2), 4); } #[test] fn test_add_hundred() { assert_eq!(add(100, 2), 102); assert_eq!(add(2, 100), 102); } #[test] #[ignore] fn ignored_test() { assert_eq!(add(0, 0), 0); } } }
$ 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) } }

Смотрите также:

Интеграционное тестирование

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

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 имеют схожую функциональность, но ссылки всегда безопасны, потому что они гарантированно указывают на достоверные данные за счёт механизма проверки заимствований. Разыменование же сырого указателя можно выполнить только через небезопасный блок.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Вызов небезопасных функций

Некоторые функции могут быть объявлены как unsafe, то есть за корректность этого кода несёт ответственность программист, а не компилятор. Пример - это метод std::slice::from_raw_parts, который создаст срез из указателя на первый элемент и длины.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Для 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

Некоторые темы не совсем имеют отношение к тому, как вы программируете. Вместо этого они расскажут вам об инструментах или инфраструктуре, которые пойдут на пользу всем. Среди этих тем:

Документация

Используйте cargo doc для сборки документации в target/doc.

Используйте cargo test для запуска всех тестов (включая документационные тесты) и cargo test --doc для запуска только документационных тестов.

Эти команды, по мере необходимости, будут соответствующим образом вызывать rustdocrustc).

Документационные комментарии

Документационные комментарии очень полезны для больших проектов, требующих документирования. Эти комментарии компилируются в документацию при запуске rustdoc. Они обозначаются как /// и поддерживают Markdown.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Для запуска тестов сначала соберите код как библиотеку, а затем укажите rustdoc где найти эту библиотеку, чтобы он мог подключить её к каждому документационному тесту:

$ rustc doc.rs --crate-type lib $ rustdoc --test --extern doc="libdoc.rlib" doc.rs

Смотрите также:

Playpen