Hello world!
Представляю вашему вниманию вторую часть большой шпаргалки по Rust.
Другой формат, который может показаться вам более удобным.
Обратите внимание: шпаргалка рассчитана на людей, которые хорошо знают любой современный язык программирования, а не на тех, кто только начинает кодить 😉
Также настоятельно рекомендуется хотя бы по диагонали прочитать замечательный Учебник по Rust (на русском языке).
Содержание
Организация кода
Когда блок кода становится большим, он должен быть разделен на маленькие части определенным образом. Rust
поддерживает несколько уровней организации кода:
- Функции.
- Модули (modules)
- встроенные модули
- привязанные к файлу
- привязанные к иерархии директорий
- Крейты (crates)
- файл
lib.rs
в том же исполняемом крейте - зависимый крейт, определенный в файле
Cargo.toml
с помощью - относительного пути
- ссылки на репозиторий
Git
- ссылки на
crates.io
- файл
- Рабочие пространства (workspaces) — позволяют управлять несколькими крейтами как одним проектом.
Функции
Функции являются первым уровнем организации кода любой программы:
fn main() {
greet(); // делает одну вещь
ask_location(); // делает другую вещь
}
fn greet() {
println!("Hello!");
}
fn ask_location() {
println!("Where are you from?");
}
В том же файле могут определяться юнит-тесты (unit tests):
fn main() {
greet();
}
fn greet() -> String {
"Hello, world!".to_string()
}
#[test] // атрибут `test` является индикатором функции тестирования
fn test_greet() {
assert_eq!("Hello, world!", greet())
}
// Тестовые функции должны размещаться внутри тестового модуля с помощью атрибута `#[cfg(test)]`.
// Этот модуль компилируется только при запуске тестов. Мы обсудим это позже.
Атрибут — это общие метаданные с свободной форме, интерпретируемые в соответствии с названием, соглашением, версиями языка и компилятора.
Модули (modules)
В том же файле
Код и данные группируются в модуль и хранятся в одном файле:
fn main() {
greetings::hello();
}
mod greetings {
// По умолчанию все, что находится в модуле, является приватным (закрытым)
pub fn hello() { // ключевое слово `pub` позволяет сделать функцию публичной (открытой), т.е. доступной внешнему коду
println!("Hello, world!");
}
}
Модули могут быть вложенными:
fn main() {
phrases::greetings::hello();
}
mod phrases {
pub mod greetings {
pub fn hello() {
println!("Hello, world!");
}
}
}
Приватные функции могут вызываться из своего модуля или из дочерних модулей:
// Вызов приватной функции из своего модуля
fn main() {
phrases::greet();
}
mod phrases {
// Публичная функция
pub fn greet() {
hello(); // или `self::hello();`
}
// Приватная функция
fn hello() {
println!("Hello, world!");
}
}
// Вызов приватной функции родительского модуля
fn main() {
phrases::greetings::hello();
}
mod phrases {
fn private_fn() {
println!("Hello, world!");
}
pub mod greetings {
pub fn hello() {
super::private_fn();
}
}
}
Ключевое слово self
используется для ссылки на текущий модуль, а ключевое слово super
— для ссылки на родительский модуль. super
также может использоваться для получения доступа к функциям верхнего уровня/корневым (root) функциям из модуля:
fn main() {
greetings::hello();
}
fn hello() {
println!("Hello, world!");
}
mod greetings {
pub fn hello() {
super::hello();
}
}
Тесты лучше писать внутри модуля tests
— они будут компилироваться только при запуске тестов:
fn greet() -> String {
"Hello, world!".to_string()
}
#[cfg(test)] // компилируются только при запуске тестов
mod tests {
use super::greet; // импортируем корневую функцию `greet()`
#[test]
fn test_greet() {
assert_eq!("Hello, world!", greet());
}
}
В другом файле, но в той же директории
// main.rs
mod greetings; // импортируем модуль `greetings`
fn main() {
greetings::hello();
}
// greetings.rs
// Код не нужно оборачивать в объявление `mod`, поскольку модулем является сам файл
pub fn hello() { // функция должна быть публичной, чтобы быть доступной извне
println!("Hello, world!");
}
Если мы обернем код в объявление mod
, модуль станет вложенным:
// main.rs
mod phrases;
fn main() {
phrases::greetings::hello();
}
// phrases.rs
pub mod greetings { // модуль должен быть публичным для доступа извне
pub fn hello() {
println!("Hello, world!");
}
}
В другом файле и другой директории
Файл mod.rs
в корне директории модуля является входной точкой (entrypoint) модуля директории. Другие файлы в директории являются субмодулями (submodules) модуля директории:
// main.rs
mod greetings;
fn main() {
greetings::hello();
}
// greetings/mod.rs
pub fn hello() {
println!("Hello, world!");
}
Если мы обернем код в объявление mod
, модуль станет вложенным:
// main.rs
mod phrases;
fn main() {
phrases::greetings::hello();
}
// phrases/mod.rs
pub mod greetings {
pub fn hello() {
println!("Hello, world!");
}
}
Другие файлы в директории являются субмодулями mod.rs
:
// main.rs
mod phrases;
fn main() {
phrases::hello()
}
// phrases/mod.rs
mod greetings;
pub fn hello() {
greetings::hello()
}
// phrases/greetings.rs
pub fn hello() {
println!("Hello, world!");
}
Если phrases/greetings.rs
должен быть доступен за пределами модуля директории, его следует импортировать как публичный модуль:
// main.rs
mod phrases;
fn main() {
phrases::greetings::hello();
}
// phrases/mod.rs
pub mod greetings; // `pub mod` вместо `mod`
// phrases/greetings.rs
pub fn hello() {
println!("Hello, world!");
}
Нельзя импортировать дочерние модули сразу в main.rs
, поэтому мы не можем использовать mod phrases::greetings;
в main.rs
. Но функцию hello()
можно повторно экспортировать (re-export) в модуле phrases/mod.rs
и вызывать как phrases::hello()
в main.rs
:
// phrases/greetings.rs
pub fn hello() {
println!("Hello, world!");
}
// phrases/mod.rs
pub mod greetings;
pub use self::greetings::hello; // повторный экспорт `greetings::hello()`
// main.rs
mod phrases;
fn main() {
phrases::hello(); // `hello()` можно вызывать напрямую из `phrases`
}
Таким образом, внешний интерфейс не обязательно должен совпадать с внутренней организацией кода. Мы подробно обсудим использование use
позже.
Крейты (crates)
Крейты — это тоже самое, что пакеты (packages) в некоторых других языках. Крейты компилируются индивидуально. Если у крейта есть дочерние модули, они объединяются с крейтом и компилируются в один файл.
Крейт может быть бинарным (двоичным) (binary) или библиотечным (library). src/main.rs
— это корень крейта/входная точка бинарного крейта, src/lib.rs
— входная точка библиотечного крейта.
lib.rs в бинарном крейте
При создании бинарного крейта, мы можем вынести основной функционал в файл src/lib.rs
и использовать его как библиотеку в src/main.rs
. Этот паттерн является довольно распространенным.
// Предположим, что мы выполнили такие команды
cargo new greetings
touch greetings/src/lib.rs
// Это привело к генерации таких файлов
greetings
├── Cargo.toml
└── src
├── lib.rs
└── main.rs
// greetings/src/lib.rs
pub fn hello() {
println!("Hello, world!");
}
// greetings/src/main.rs
extern crate greetings;
// Начиная с версии Rust 2018, ключевое слово `extern crate` стало необязательным, и крейты автоматически импортируются при их добавлении в зависимости проекта в файле Cargo.toml.
// Здесь вместо `extern crate` можно использовать `use`
fn main() {
greetings::hello();
}
Для того, чтобы иметь возможность тестировать этот функционал, пример необходимо переписать следующим образом:
// greetings/src/lib.rs
pub fn hello() -> String {
//! Это возвращает String `Hello, world!`
("Hello, world!").to_string()
}
// Тест для `hello()`
#[test] // индикатор тестовой функции
fn test_hello() {
assert_eq!(hello(), "Hello, world!");
}
// Тесты для `hello()`, идиоматический способ
#[cfg(test)] // компилируется только при запуске тестов
mod tests { // тесты отделены от кода
use super::hello; // импортируем корневую функцию `hello()`
#[test]
fn test_hello() {
assert_eq!(hello(), "Hello, world!");
}
}
В lib.rs
можно подключать другие файлы:
// Предположим, что мы выполнили такие команды
cargo new phrases
touch phrases/src/lib.rs
touch phrases/src/greetings.rs
// Это привело к генерации таких файлов
phrases
├── Cargo.toml
└── src
├── greetings.rs
├── lib.rs
└── main.rs
// phrases/src/greetings.rs
pub fn hello() {
println!("Hello, world!");
}
// phrases/src/lib.rs
pub mod greetings; // импортируем модуль `greetings` как публичный
// phrases/src/main.rs
use phrases;
fn main() {
phrases::greetings::hello();
}
Зависимый крейт в Cargo.toml
Когда кода в файле lib.rs
становится слишком много, мы можем вынести его в отдельный библиотечный крейт и использовать в качестве зависимости основного крейта. Как упоминалось раннее, зависимость может быть определена с помощью относительного пути, ссылки на репозиторий Git
или crates.io
.
Относительный путь
// Предположим, что мы выполнили такие команды
cargo new phrases
cargo new phrases/greetings --lib
// Это привело к генерации таких файлов
phrases
├── Cargo.toml
├── greetings
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── src
└── main.rs
// phrases/Cargo.toml
[package]
name = "phrases"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]
[dependencies]
// Относительный путь к зависимому крейту
greetings = { path = "greetings" }
// phrases/greetings/src/lib.rs
pub fn hello() {
println!("Hello, world!");
}
// phrases/src/main.rs
use greetings;
fn main() {
greetings::hello();
}
Ссылка на репозиторий
// Cargo.toml
[dependencies]
// Последний коммит в мастер ветку
rocket = { git = "https://github.com/SergioBenitez/Rocket" }
// Последний коммит в определенную ветку
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "v0.3" }
// Определенный тег
rocket = { git = "https://github.com/SergioBenitez/Rocket", tag = "v0.3.2" }
// Последняя ревизия
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "8183f636305cef4adaa9525506c33cbea72d1745" }
crates.io
Сначала создадим простой крейт "Hello world" и загрузим его на crates.io.
// Предположим, что мы выполнили такие команды
cargo new test_crate_hello_world --lib
// Это привело к генерации таких файлов
test_crate_hello_world
├── Cargo.toml
└── src
└── lib.rs
// test_crate_hello_world/Cargo.toml
[package]
name = "test_crate_hello_world"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]
description = "A Simple Hello World Crate"
repository = "https://github.com/dumindu/test_crate_hello_world"
keywords = ["hello", "world"]
license = "Apache-2.0"
[dependencies]
// test_crate_hello_world/src/lib.rs
//! A Simple Hello World Crate
/// Эта функция возвращает приветствие; `Hello, world!`
pub fn hello() -> String {
("Hello, world!").to_string()
}
#[cfg(test)]
mod tests {
use super::hello;
#[test]
fn test_hello() {
assert_eq!(hello(), "Hello, world!");
}
}
Док-комментарии //!
используются для написания документации уровня крейта и модуля. В других местах мы должны использовать ///
за пределами блока. При загрузке крейта на crates.io
, cargo
генерирует документацию на основе этих док-комментариев и размещает ее на docs.rs.
Поля description
и license
являются обязательными.
Для публикации этого крейта на crates.io
необходимо сделать следующее:
- Создать аккаунт на
crates.io
и сгенерировать токен API. - Выполнить команду
cargo login <token>
с этим токеном и затем командуcargo publish
.
Команда cargo publish
выполняет подкоманду cargo package
для упаковки крейта в формат, поддерживаемый crates.io
.
Наш крейт называется test_crate_hello_world
, так что его можно найти по адресу https://crates.io/crates/test_crate_hello_world и https://docs.rs/test_crate_hello_world.
crates.io
поддерживает файлы с описанием (readme
). Ссылку на файл с описанием необходимо указать в Cargo.toml
: readme="README.md"
.
Подключаем наш крейт к другому крейту в качестве зависимости:
// Предположим, что мы выполнили такую команду
cargo new greetings
// Это привело к генерации таких файлов
greetings
├── Cargo.toml
└── src
└── main.rs
// greetings/Cargo.toml
[package]
name = "greetings"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]
[dependencies]
// Подключаем крейт
test_crate_hello_world = "0.1.0"
// greetings/src/main.rs
use test_crate_hello_world;
fn main() {
println!("{}", test_crate_hello_world::hello());
}
По умолчанию cargo
ищет зависимости на crates.io
, поэтому в Cargo.toml
достаточно указать название крейта и его версию. Зависимости скачиваются и компилируются при выполнении команды cargo build
.
Рабочие пространства (workspaces)
При росте кодовой базы часто приходится работать с несколькими крейтами в одном проекте. Rust
поддерживает это через рабочие пространства. Мы можем анализировать (cargo check
), собирать, запускать тесты или генерировать документацию для всех крейтов за один раз путем выполнения команд cargo
в корне проекта.
При работе с несколькими крейтами велика вероятность наличия общих зависимостей. Во избежание скачивания и компиляции одной и той же зависимости несколько раз Rust
использует общую директорию сборки (shared build directory) при выполнении cargo build
в корне проекта.
Создадим простую библиотеку и бинарный крейт.
Выполняем следующие команды:
mkdir greetings
touch greetings/Cargo.toml
cargo new greetings/lib --lib
cargo new greetings/examples/hello
Это приводит к генерации следующих файлов:
greetings
├── Cargo.toml
├── examples
│ └── hello
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── lib
├── Cargo.toml
└── src
└── lib.rs
Редактируем следующие файлы:
// greetings/Cargo.toml - определяем рабочее пространство и его членов
[workspace]
members = [
"lib",
"examples/hello"
]
// greetings/lib/Cargo.toml - меняем название пакета на `greetings`
[package]
name = "greetings"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]
[dependencies]
// greetings/lib/src/lib.rs - добавляем простую функцию
pub fn hello() {
println!("Hello, world!");
}
// greetings/examples/hello/Cargo.toml - добавляем библиотеку `greetings` как зависимость
[package]
name = "hello"
version = "0.1.0"
authors = ["Dumindu Madunuwan"]
[dependencies]
greetings = { path = "../../lib" }
// greetings/examples/hello/src/main.rs - импортируем библиотеку `greetings` и вызываем ее функцию
use greetings;
fn main() {
greetings::hello();
}
Хорошим примером использования рабочих пространств является директория с исходным кодом самого Rust
— rust-lang/rust.
Use
Рассмотрим основные случаи использования ключевого слова use
.
Привязка полного пути к новому названию
В основном use
используется для привязки (bind) полного пути элемента к новому названию. Это делается для того, чтобы пользователю не нужно было каждый раз вводить полный путь.
mod phrases {
pub mod greetings {
pub fn hello() {
println!("Hello, world!");
}
}
}
fn main() {
phrases::greetings::hello(); // полный путь
}
// Создаем синоним (alias) для модуля
use phrases::greetings;
fn main() {
greetings::hello();
}
// Создаем синоним для элемента модуля
use phrases::greetings::hello;
fn main() {
hello();
}
// Переименовываем элемент модуля с помощью ключевого слова `as`
use phrases::greetings::hello as greet;
fn main() {
greet();
}
Импорт элементов в область видимости
Это похоже на создание синонимов.
fn hello() -> String {
"Hello, world!".to_string()
}
#[cfg(test)]
mod tests {
use super::hello; // импортируем функцию `hello()` в область видимости
#[test]
fn test_hello() {
assert_eq!("Hello, world!", hello()); // без `use` функцию можно вызвать через `super::hello()`
}
}
По умолчанию объявления use
используют абсолютные пути, но ключевые слова self
и super
делают путь относительным текущего модуля.
Аналогичным образом use
используется для импорта элементов других крейтов, включая std
— стандартную библиотеку Rust
:
// Импорт элементов
use std::fs::File;
fn main() {
File::create("empty.txt").expect("Can not create the file!");
}
// Импорт модуля и элементов
use std::fs::{self, File}; // `use std::fs; use std::fs::File;`
fn main() {
fs::create_dir("some_dir").expect("Can not create the directory!");
File::create("some_dir/empty.txt").expect("Cannot create the file!");
}
// Импорт нескольких элементов
use std::fs::File;
use std::io::{BufReader, BufRead}; // `use std::io::BufReader; use std::io::BufRead;`
fn main() {
let file = File::open("src/hello.txt").expect("File not found");
let buf_reader = BufReader::new(file);
for line in buf_reader.lines() {
println!("{}", line.unwrap());
}
}
use
импортирует в область видимости только то, что определено, а не все элементы модуля или крейта. Это повышает эффективность программ.
Повторный экспорт
Специальный случай — pub use
. При создании модуля в нем можно экспортировать функции из другого модуля, чтобы они были доступны из вашего модуля напрямую. Это называется повторным экспортом (re-export).
// main.rs
mod phrases;
fn main() {
phrases::hello(); // непрямая связь
}
// phrases/mod.rs
pub mod greetings;
pub use self::greetings::hello; // повторный экспорт `greetings::hello()`
// phrases/greetings.rs
pub fn hello() {
println!("Hello, world!");
}
Этот паттерн является очень распространенным в больших библиотеках. Он помогает скрыть сложность внутренней структуры библиотеки от пользователей. Пользователям совсем не обязательно знать о внутреннем устройстве библиотеки для ее использования.
Std, примитивы и прелюдии
В Rust
элементы языка реализованы не только крейтом std
(стандартной библиотекой), но и самим компилятором, например:
- примитивы — определяются компилятором, методы реализуются
std
на примитивах - макросы — определяются как компилятором, так и
std
std
состоит из модулей в соответствии со сферой применения.
Хотя примитивы реализуются компилятором, стандартная библиотека реализует их самые полезные методы. Некоторые редко используемые языковые элементы примитивов хранятся в соответствующих модулях std
. Вот почему мы можем видеть char
, str
и целочисленные типы как в примитивах, так и в модулях std
.
Примитивы
// Определяются компилятором, методы реализуются `std`
bool, char, slice, str
i8, i16, i32, i64, i128, isize
u8, u16, u32, u64, u128, usize
f32, f64
array, tuple
pointer, fn, reference
Макросы (стандартные)
// Определяются как компилятором, так и `std`
print, println, eprint, eprintln
format, format_args
write, writeln
concat, concat_idents, stringify // concat_idents - экспериментальное API (доступно только в ночной версии (nightly) Rust)
include, include_bytes, include_str
assert, assert_eq, assert_ne
debug_assert, debug_assert_eq, debug_assert_ne
try, panic, compile_error, unreachable, unimplemented
file, line, column, module_path
env, option_env
cfg
select, thread_local // select - экспериментальное API
vec
Модули std
char, str
i8, i16, i32, i64, i128, isize
u8, u16, u32 ,u64, u128, usize
f32, f64
num
vec, slice, hash, heap, collections // heap - экспериментальное API
string, ascii, fmt
default
marker, clone, convert, cmp, iter
ops, ffi
option, result, panic, error
io
fs, path
mem, thread, sync
process, env
net
time
os
ptr, boxed, borrow, cell, any, rc
prelude
intrinsics // экспериментальное API
raw // экспериментальное API
При изучении исходного кода Rust
, можно заметить, что директория src является рабочим пространством (workspace). Хотя оно содержит много библиотечных крейтов, изучив Cargo.toml, легко определить, что основными крейтами являются rustc (компилятор) и libstd (std
). В libstd/lib.rs
модули повторно экспортируются с помощью pub use
, оригинальной локацией большинства модулей std
является src/libcore.
Несколько важных модулей std
:
std::io
— инструменты для работы с вводом/выводомstd::fs
— инструменты для работы с файловой системойstd::path
— инструменты для работы с кроссплатформенными путямиstd::env
— инструменты для работы с переменными окружения процессовstd::mem
— инструменты для работы с памятьюstd::net
— инструменты для работы сTCP
/UDP
std::os
— инструменты для работы с операционной системойstd::thread
— инструменты для работы с нативными потокамиstd::collections
— инструменты для работы с коллекциями (HasMap
,HashSet
и др.)
Подробнее о модулях std
можно почитать здесь.
Прелюдии
Не все модули std
автоматически загружаются в каждую программу Rust
, а только их часть. Эта часть называется прелюдией (prelude). Прелюдия импортирует следующее:
// Повторный экспорт операторов
pub use marker::{Copy, Send, Sized, Sync};
pub use ops::{Drop, Fn, FnMut, FnOnce};
// Повторный экспорт функции
pub use mem::drop;
// Повторный экспорт типов и трейтов
pub use boxed::Box;
pub use borrow::ToOwned;
pub use clone::Clone;
pub use cmp::{PartialEq, PartialOrd, Eq, Ord};
pub use convert::{AsRef, AsMut, Into, From};
pub use default::Default;
pub use iter::{Iterator, Extend, IntoIterator};
pub use iter::{DoubleEndedIterator, ExactSizeIterator};
pub use option::Option::{self, Some, None};
pub use result::Result::{self, Ok, Err};
pub use slice::SliceConcatExt;
pub use string::{String, ToString};
pub use vec::Vec;
Полный список элементов прелюдии можно найти здесь.
Технически Rust
вставляет
extern crate std
— в корень каждого крейтаuse std::prelude::*
— в каждый модуль
Концепция прелюдий является очень популярной среди библиотек Rust
. Некоторые модули std
(например, std::io
) и множество библиотек (например, diesel
) содержат модули prelude
.
Прелюдии используются для создания единого места для импорта всех важных компонентов, необходимых для использования библиотеки. Они не загружаются автоматически, если мы не импортировали их вручную. Только std::prelude
автоматически импортируется во все программы Rust
.
Обработка ошибок
Умный компилятор
Почему компилятор?
Основную работу по предотвращению ошибок в программах Rust
выполняет компилятор. Он анализирует код во время компиляции и выдает ошибки и предупреждения, если код не соответствует правилам управления памятью или аннотаций времен жизни.
#[allow(unused_variables)] // атрибут линтинга (lint attribute), используемый для подавления (supress) предупреждений о неиспользуемых переменных (`b`)
fn main() {
let a = vec![1, 2, 3];
let b = a;
println!("{:?}", a);
}
// Ошибка времени компиляции (compile-time error)
error[E0382]: use of moved value: `a`
--> src/main.rs:6:22
|
3 | let b = a;
| - value moved here
4 |
5 | println!("{:?}", a);
| ^ value used here after move
|
= note: move occurs because `a` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
// Вместо `#[allow(unused_variables)]`, можно использовать `let _b = a;` на строке 4.
// Также можно использовать `let _ =` для полного игнорирования возвращаемых значений
Компилятор проверяет не только проблемы, связанные с временем жизни или управлением памятью, но и распространенные ошибки, допускаемые разработчиками, например:
struct Color {
r: u8,
g: u8,
b: u8,
}
fn main() {
let yellow = Color {
r: 255,
g: 255,
// Такого поля не существует
d: 0,
};
println!("Yellow = rgb({},{},{})", yellow.r, yellow.g, yellow.b);
}
// Ошибка компиляции
error[E0560]: struct `Color` has no field named `d`
--> src/main.rs:11:9
|
11 | d: 0,
| ^ field does not exist - did you mean `b`?
error: aborting due to previous error
For more information about this error, try `rustc --explain E0560`.
Описание ошибки
Сообщения об ошибках в примерах очень информативны, и мы можем легко увидеть, где находится ошибка. Если сообщения об ошибке не позволяет определить проблему, можно выполнить команду rustc --explain <код ошибки>
, которая покажет тип ошибки и способы ее решения, включая простые примеры кода.
Например, вот результат выполнения команды rustc --explain E0571
:
// Инструкция `break` с аргументом используется не в цикле `loop`
A `break` statement with an argument appeared in a non-`loop` loop.
// Пример кода с ошибкой
Example of erroneous code:
```
let result = while true {
if satisfied(i) {
break 2*i; // error: `break` with value from a `while` loop
}
i += 1;
};
```
// Суть в том, что `break` может использоваться только в цикле, объявленном с помощью `loop`
The `break` statement can take an argument (which will be the value of the loop
expression if the `break` statement is executed) in `loop` loops, but not
`for`, `while`, or `while let` loops.
Make sure `break value;` statements only occur in `loop` loops:
```
let result = loop { // ok!
if satisfied(i) {
break 2*i;
}
i += 1;
};
```
Объяснения ошибок можно найти в индексе ошибок компилятора Rust
. Например, об ошибке E0571
можно прочитать здесь.
Паника
panic!()
- В некоторых случаях, когда возникает ошибка, мы не можем ничего сделать, чтобы ее обработать (ошибка не должна была произойти). Такие ошибки называются неисправимыми (unrecoverable errors)
- кроме того, когда мы не используем многофункциональный отладчик или правильные "логи" (logs), иногда нам нужно отладить код, выйдя из программы на определенной строке кода, распечатав определенное сообщение или значение переменной, чтобы понять текущий поток программы
В этих двух случаях мы используем макрос panic!()
.
panic!()
выполняется в потоке. Это означает, что паника в одном потоке не влияет на другие потоки.
Выход из программы на определенной строке
fn main() {
// ...
// Если необходимо выполнить отладку на этой строке
panic!();
}
// Ошибка компиляции
// thread 'main' panicked at 'explicit panic', src/main.rs:5:5
Выход из программы с кастомным сообщением об ошибке
#[allow(unused_mut)] // атрибут линтинга, используемый для подавления предупреждения о том, что переменная `username` не должна быть мутабельной
fn main() {
let mut username = String::new();
// Код для получения имени пользователя
if username.is_empty() {
panic!("Username is empty!");
}
println!("{}", username);
}
// Ошибка компиляции
// thread 'main' panicked at 'Username is empty!', src/main.rs:8:9
Выход из программы со значением переменной
#[derive(Debug)] // производный (derive) атрибут, используемый для реализации `std::fmt::Debug` на `Color`
struct Color {
r: u8,
g: u8,
b: u8,
}
#[allow(unreachable_code)] // атрибут линтинга, используемый для подавления предупреждения о недостижимом коде (коде, который никогда не будет выполнен)
fn main() {
let some_color: Color;
// Код для получения цвета, например
some_color = Color { r: 255, g: 255, b: 0 };
// Если здесь необходимо выполнить отладку
panic!("{:?}", some_color);
println!(
"The color = rgb({},{},{})",
some_color.r, some_color.g, some_color.b
);
}
// Ошибка компиляции
// thread 'main' panicked at 'Color { r: 255, g: 255, b: 0 }', src/main.rs:16:5
Как видите, panic!()
поддерживает стиль аргументов println!()
. По умолчанию он печатает сообщение об ошибке, путь к файлу, а также номера строки и колонки, где возникла ошибка.
unimplemented!()
Если в вашем коде есть незавершенные разделы, для обозначения таких блоков можно использовать стандартный макрос unimplemented!()
. Программа запаникует с сообщением об ошибке not yet implemented
при попытке выполнить код такого блока.
// panic!()
thread 'main' panicked at 'explicit panic', src/main.rs:6:5
thread 'main' panicked at 'Username is empty!', src/main.rs:9:9
thread 'main' panicked at 'Color { r: 255, g: 255, b: 0 }', src/main.rs:17:5
// unimplemented!()
thread 'main' panicked at 'not yet implemented', src/main.rs:6:5
thread 'main' panicked at 'not yet implemented: Username is empty!', src/main.rs:9:9
thread 'main' panicked at 'not yet implemented: Color { r: 255, g: 255, b: 0 }', src/main.rs:17:5
unreachable!()
Этот стандартный макрос используется для обозначения блоков кода, которые недоступны программе. При доступе к такому блоку программа запаникует с сообщением об ошибке internal error: entered unreachable code
.
fn main() {
let level = 22;
let stage = match level {
1..=5 => "beginner",
6..=10 => "intermediate",
11..=20 => "expert",
_ => unreachable!(),
};
println!("{}", stage);
}
// Ошибка компиляции
// thread 'main' panicked at 'internal error: entered unreachable code', src/main.rs:7:20
unreachable!()
также поддерживает кастомные сообщения об ошибках:
// С кастомным сообщением
_ => unreachable!("Custom message"),
// Ошибка компиляции
// thread 'main' panicked at 'internal error: entered unreachable code: Custom message', src/main.rs:7:20
// С данными для отладки
_ => unreachable!("level is {}", level),
// Ошибка компиляции
// thread 'main' panicked at 'internal error: entered unreachable code: level is 22', src/main.rs:7:14
assert!(), assert_eq!(), assert_ne!()
Это стандартные макросы, которые обычно используются в тестах.
assert!()
проверяет, что логическое значение является истинным. Если выражение является ложным,assert!()
паникует:
fn main() {
let f = false;
assert!(f)
}
// Ошибка компиляции
// thread 'main' panicked at 'assertion failed: f', src/main.rs:4:5
assert_eq!()
проверяет, что два выражения являются равными. Если выражения не являются равными,assert_eq!()
паникует:
fn main() {
let a = 10;
let b = 20;
assert_eq!(a, b);
}
// Ошибка компиляции
// thread 'main' panicked at 'assertion failed: `(left == right)`
// left: `10`,
// right: `20`', src/main.rs:5:5
assert_ne!()
проверяет, что два выражения НЕ являются равными. Если выражения являются равными,assert_ne!()
паникует:
fn main() {
let a = 10;
let b = 10;
assert_ne!(a, b);
}
// Ошибка компиляции
// thread 'main' panicked at 'assertion failed: `(left != right)`
// left: `10`,
// right: `10`', src/main.rs:5:5
Эти макросы также поддерживают кастомные сообщения об ошибках:
// С кастомным сообщением
fn main() {
let a = 10;
let b = 20;
assert_eq!(a, b, "a and b should be equal");
}
// Ошибка компиляции
// thread 'main' panicked at 'assertion failed: `(left == right)`
// left: `10`,
// right: `20`: a and b should be equal', src/main.rs:5:5
// С данными для отладки
fn main() {
let a = 10;
let b = 20;
let c = 40;
assert_eq!(a + b, c, "a = {} ; b = {}", a, b);
}
// Ошибка компиляции
// thread 'main' panicked at 'assertion failed: `(left == right)`
// left: `30`,
// right: `40`: a = 10 ; b = 20', src/main.rs:7:5
debug_assert!(), debug_assert_eq!(), debug_assert_ne!()
Эти макросы похожи на предыдущие. Но по умолчанию они включены только в неоптимизированных сборках (сборках для разработки). Из релизных сборок они удаляются, если не указан флаг -C debug-assertions
.
Option и Result
Во многих языках для представления отсутствующего значения используются типы null
\ nil
\ undefined
, а для обработки ошибок — исключения (exceptions). В Rust
нет ни того, ни другого. Это, в частности, позволяет предотвратить такие проблемы, как исключения нулевого указателя (null pointer exceptions), утечки конфиденциальных данных через исключения и др. Вместо этого Rust
предоставляет 2 специальных общих перечисления — Option
и Result
.
Как упоминалось ранее,
- опциональное значение (
Option
) может быть либо некоторым значением (Some
), либо отсутствовать (None
) - результат (
Result
) может быть либо успехом (Ok
), либо ошибкой (Err
)
enum Option<T> { // `T` - дженерик, принимающий любой тип значения
Some(T),
None,
}
enum Result<T, E> { // `T` и `E` - дженерики. `T` - любой тип значения, `E` - любой тип ошибки
Ok(T),
Err(E),
}
Option
и Result
входят в состав прелюдии, поэтому могут использоваться напрямую.
Option
При написании функции или типа данных
- если параметр функции является опциональным
- если функция возвращает значение (non-void), и оно может быть пустым
- если значение свойства типа данных может быть пустым
мы должны использовать Option
.
Например, если функция возвращает &str
, которая может быть пустой, типом возвращаемого значения должно быть Option<&str>
:
fn get_an_optional_value() -> Option<&str> {
// Если опциональное значение не пустое
return Some("Some value");
// иначе
None
}
Аналогично, если значение свойства типа данных является опциональным, например свойство middle_name
в структуре Name
, мы должны обернуть его в Option
:
struct Name {
first_name: String,
middle_name: Option<String>, // `middle_name` может быть пустым
last_name: String,
}
Как вы знаете, мы можем использовать сопоставление с образцом, чтобы поймать соответствующий тип возвращаемого значения (Some
/None
) посредством match
. Существует функция получения домашнего каталога текущего пользователя в std::env
— home_dir(). Поскольку в таких системах, как Linux
, не у всех пользователей есть домашний каталог, он является опциональным. Поэтому home_dir()
возвращает Option:
use std::env;
fn main() {
let home_path = env::home_dir();
match home_path {
Some(p) => println!("{:?}", p), // в песочнице Rust это напечатает `/root`
None => println!("Cannot find the home directory!"),
}
}
При использовании необязательных параметров функции нам необходимо передавать значения None
для пустых аргументов при вызове функции:
fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // `mname` является опциональным
match mname {
Some(n) => format!("{} {} {}", fname, n, lname),
None => format!("{} {}", fname, lname),
}
}
fn main() {
println!("{}", get_full_name("Galileo", "Galilei", None));
println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}
// Лучше создать структуру `Person` с полями `fname`, `lname`, `mname` и реализовать метод `full_name()` на ней
Помимо этого, Option
используется в Rust
с указателями, допускающими значение null
. Поскольку в Rust
нет нулевых указателей, типы указателей должны указывать на допустимое местоположение. Поэтому, если указатель может иметь значение null
, мы должны использовать Option<Box<T>>
.
Result
Если функция может вернуть ошибку, мы должны использовать Result
, объединяющий тип допустимого вывода (valid output) и тип ошибки. Например, если тип допустимого вывода — u64
, а тип ошибки — String
, типом возвращаемого значения должен быть Result<u64, String>
:
fn function_with_error() -> Result<u64, String> {
// Если возникла ошибка
return Err("The error message".to_string());
// иначе, возвращаем валидный вывод
Ok(255)
}
Как вы знаете, мы можем использовать сопоставление с образцом, чтобы поймать соответствующий тип возвращаемого значения (Ok
/Err
) посредством match
. В std::env
есть функция для получения значений переменных окружения — var(). Она принимает название переменной в качестве аргумента. При отсутствии указанной переменной возникает ошибка. Поэтому var()
возвращает Result<String, VarError>.
use std::env;
fn main() {
let key = "HOME";
match env::var(key) {
Ok(v) => println!("{}", v), // в песочнице Rust это напечатает `/root`
Err(e) => println!("{}", e), // это напечатает `environment variable not found`, если будет указана несуществующая переменная
}
}
is_some(), is_none(), is_ok(), is_err()
Rust
в качестве альтернативы match
предоставляет функции is_some()
, is_none()
, is_ok()
и is_err()
для определения возвращаемого типа:
fn main() {
let x: Option<&str> = Some("Hello, world!");
assert_eq!(x.is_some(), true);
assert_eq!(x.is_none(), false);
let y: Result<i8, &str> = Ok(10);
assert_eq!(y.is_ok(), true);
assert_eq!(y.is_err(), false);
}
ok(), err()
Для Result
также имеются функции ok()
и err()
. Они конвертируют Ok<T>
и Err<E>
в Some(T)
и None
, соответственно:
fn main() {
let o: Result<i8, &str> = Ok(8);
let e: Result<i8, &str> = Err("message");
assert_eq!(o.ok(), Some(8)); // Ok(v) ok = Some(v)
assert_eq!(e.ok(), None); // Err(v) ok = None
assert_eq!(o.err(), None); // Ok(v) err = None
assert_eq!(e.err(), Some("message")); // Err(v) err = Some(v)
}
unwrap() и expect()
unwrap()
- если
Option
имеет значениеSome
илиResult
имеет значениеOk
, эти значения передаются на следующий шаг - если
Option
имеет значениеNone
илиResult
имеет значениеErr
, программа паникует, в случае сErr
, с сообщением об ошибке
Этот функционал похож на такое использование match
:
fn main() {
let x;
match get_an_optional_value() {
Some(v) => x = v, // если `Some("abc")`, устанавливаем `x` в значение "abc"
None => panic!(), // если `None`, паникуем без сообщения
}
println!("{}", x); // "abc" ; если изменить `false` на `true` в `get_an_optional_value()`
}
fn get_an_optional_value() -> Option<&'static str> {
// Если опциональное значение не является пустым
if false {
return Some("abc");
}
// иначе
None
}
// Ошибка компиляции
// thread 'main' panicked at 'explicit panic', src/main.rs:5:17
fn main() {
let x;
match function_with_error() {
Ok(v) => x = v, // если `Ok(255)`, устанавливаем `x` в значение 255
Err(e) => panic!(e), // если `Err("some message")`, паникуем с сообщением "some message"
}
println!("{}", x); // 255; если изменить `true` на `false` в `function_with_error()`
}
fn function_with_error() -> Result<u64, String> {
// Если возникла ошибка
if true {
return Err("some message".to_string());
}
// иначе, возвращаем валидный вывод
Ok(255)
}
// Ошибка компиляции
// thread 'main' panicked at 'some message', src/main.rs:5:19
Тот же код, но с unwrap()
:
fn main() {
let x = get_an_optional_value().unwrap();
println!("{}", x);
}
// Ошибка компиляции
// thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', libcore/option.rs:345:21
fn main() {
let x = function_with_error().unwrap();
println!("{}", x);
}
// Ошибка компиляции
// thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "some message"', libcore/result.rs:945:5
Как видите, при использовании unwrap()
мы не получаем номер строки, где произошла паника.
expect()
Похоже на unwrap()
, но позволяет установить кастомное сообщение для паники:
fn main() {
let n: Option<i8> = None;
n.expect("empty value returned");
}
// Ошибка компиляции
// thread 'main' panicked at 'empty value returned', libcore/option.rs:989:5
fn main() {
let e: Result<i8, &str> = Err("some message");
e.expect("expect error message");
}
// Ошибка компиляции
// thread 'main' panicked at 'expect error message: "some message"', libcore/result.rs:945:5
unwrap_err() и expect_err()
Эти методы предоставляются для Result
, являются противоположностью unwrap()
и expect()
, т.е. паникуют при значениях Ok
(обычно используются в тестах). Печатают как значение Ok
, так и сообщение об ошибке:
fn main() {
let o: Result<i8, &str> = Ok(8);
o.unwrap_err();
}
// Ошибка компиляции
// thread 'main' panicked at 'called `Result::unwrap_err()` on an `Ok` value: 8', libcore/result.rs:945:5
fn main() {
let o: Result<i8, &str> = Ok(8);
o.expect_err("Should not get Ok value");
}
// Ошибка компиляции
// thread 'main' panicked at 'Should not get Ok value: 8', libcore/result.rs:945:5
unwrap_or(), unwrap_or_default() и unwrap_or_else()
Эти методы похожи на unwrap()
в части обработки Some
и Ok
значений, но отличаются от него в части обработки None
и Err
.
unwrap_or()
— в случаеNone
иErr
, на следующий шаг передается параметр этого метода
fn main() {
let v1 = 8;
let v2 = 16;
let s_v1 = Some(8);
let n = None;
assert_eq!(s_v1.unwrap_or(v2), v1); // Some(v1) unwrap_or v2 = v1
assert_eq!(n.unwrap_or(v2), v2); // None unwrap_or v2 = v2
let o_v1: Result<i8, &str> = Ok(8);
let e: Result<i8, &str> = Err("error");
assert_eq!(o_v1.unwrap_or(v2), v1); // Ok(v1) unwrap_or v2 = v1
assert_eq!(e.unwrap_or(v2), v2); // Err unwrap_or v2 = v2
}
unwrap_or_default()
— в случаеNone
иErr
, на следующий шаг передается дефолтное значение соответствующегоSome
/Ok
fn main() {
let v = 8;
let v_default = 0;
let s_v: Option<i8> = Some(8);
let n: Option<i8> = None;
assert_eq!(s_v.unwrap_or_default(), v); // Some(v) unwrap_or_default = v
assert_eq!(n.unwrap_or_default(), v_default); // None unwrap_or_default = дефолтное значение v
let o_v: Result<i8, &str> = Ok(8);
let e: Result<i8, &str> = Err("error");
assert_eq!(o_v.unwrap_or_default(), v); // Ok(v) unwrap_or_default = v
assert_eq!(e.unwrap_or_default(), v_default); // Err unwrap_or_default = дефолтное значение v
}
unwrap_or_else()
— похож наunwrap_or()
. Единственное отличие состоит в том, что на следующий шаг передается результат замыкания того же типа, что соответствующийSome
/Ok
fn main() {
let v1 = 8;
let v2 = 16;
let s_v1 = Some(8);
let n = None;
let fn_v2_for_option = || 16;
assert_eq!(s_v1.unwrap_or_else(fn_v2_for_option), v1); // Some(v1) unwrap_or_else fn_v2 = v1
assert_eq!(n.unwrap_or_else(fn_v2_for_option), v2); // None unwrap_or_else fn_v2 = v2
let o_v1: Result<i8, &str> = Ok(8);
let e: Result<i8, &str> = Err("error");
let fn_v2_for_result = |_| 16;
assert_eq!(o_v1.unwrap_or_else(fn_v2_for_result), v1); // Ok(v1) unwrap_or_else fn_v2 = v1
assert_eq!(e.unwrap_or_else(fn_v2_for_result), v2); // Err unwrap_or_else fn_v2 = v2
}
Распространение ошибки и None
panic!()
, unwrap()
и expect()
следует использовать только когда мы не можем обработать ошибку или отсутствующее значение лучшим способом. Если функция содержит выражение, которое может произвести None
или Err
- мы можем обработать их внутри этой функции
- мы можем сразу вернуть
None
илиErr
вызывающему (caller) для их обработки (это называется распространением ошибки — error propagation)
Типы None
не обязательно всегда обрабатывать. Ошибки принято возвращать вызывающему для обработки.
Оператор ?
- если
Option
имеет значениеSome
илиResult
имеет значениеOk
, значение передается на следующий шаг - если
Option
имеет значениеNone
илиResult
имеет значениеErr
, значение возвращается вызывающему
fn main() {
if complex_function().is_none() {
println!("X not exists!");
}
}
fn complex_function() -> Option<&'static str> {
let x = get_an_optional_value()?; // если `None`, сразу возвращаемся; если `Some("abc")`, устанавливаем `x` в значение "abc"
println!("{}", x); // "abc" ; если изменить `false` на `true` в `get_an_optional_value()`
Some("")
}
fn get_an_optional_value() -> Option<&'static str> {
// Если опциональное значение не является пустым
if false {
return Some("abc");
}
// иначе
None
}
fn main() {
// Функция `main` - это вызывающий функции `complex_function()`,
// поэтому ошибки `complex_function()` обрабатываются внутри `main()`
if complex_function().is_err() {
println!("Can not calculate X!");
}
}
fn complex_function() -> Result<u64, String> {
let x = function_with_error()?; // если `Err`, сразу возвращаемся; если `Ok(255)`, устанавливаем `x` в значение 255
println!("{}", x); // 255; если изменить `true` на `false` в `function_with_error()`
Ok(0)
}
fn function_with_error() -> Result<u64, String> {
// Если возникла ошибка
if true {
return Err("some message".to_string());
}
// иначе, возвращаем валидный вывод
Ok(255)
}
try!()
Оператор ?
был добавлен в Rust
версии 1.13. Макрос try!()
— это старый способ распространения ошибок. Сейчас использовать его не рекомендуется.
// Это
let x = function_with_error()?;
// Эквивалентно этому
let x = try!(function_with_error());
Распространение ошибки из main()
Начиная с Rust
версии 1.26 мы можем распространять типы Result
и Option
из функции main()
. В случае Err
печатается ее отладочное представление (Debug
). Мы обсудим это позже.
use std::fs::File;
fn main() -> std::io::Result<()> {
let _ = File::open("not-existing-file.txt")?;
Ok(()) // Дефолтным результатом вызова функции является пустой кортеж (`()`)
}
// Программа не может найти `not-existing-file.txt` и генерирует
// Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })
// В результате распространения печатается
// Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Комбинаторы (combinators)
Что такое комбинатор?
- Одно из значений слова "комбинатор" — это неформальное значение, относящееся к шаблону комбинатора (combinator pattern), стилю организации библиотек, основанному на идее объединения вещей. Обычно здесь есть некоторый тип
T
, некоторые функции для построения "примитивных" значений типаT
и некоторые "комбинаторы", которые могут комбинировать значения типаT
разными способами для создания более сложных значений типаT
. Другое определение гласит, что комбинатор — это "функция без свободных переменных (free variables)" - комбинатор — это функция, которая строит фрагменты программы из других фрагментов; программист, использующий комбинаторы, создает большую часть программы автоматически, а не реализует каждую деталь вручную (John Hughes)
В экосистеме Rust
отсутствует точное определение комбинаторов.
or()
,and()
,or_else()
,and_then()
— комбинируют два значения типаT
и возвращают значение типаT
filter()
для типовOption
- фильтрует значения типа
T
с помощью замыкания как условной функции - возвращает значение типа
T
- фильтрует значения типа
map()
,map_err()
- конвертируют тип
T
с помощью замыкания - тип данных значения внутри
T
может меняться, например,Some<&str>
может статьSome<usize>
, аErr<&str>
может статьErr<isize>
и т.д.
- конвертируют тип
map_or()
,map_or_else()
- трансформируют тип
T
, применяя к нему замыкание, и возвращают значение типаT
- для
None
иErr
применяется дефолтное значение или другое замыкание, соответственно ok_or()
,ok_or_else()
для типовOption
— трансформируют типOption
в типResult
- трансформируют тип
as_ref()
,as_mut()
— трансформируют типT
в ссылку или мутабельную ссылку, соответственно
or() и and()
Комбинируют два выражения, возвращающие Option
/Result
or()
— если одним из выражений являетсяSome
илиOk
, значение этого выражения возвращается сразуand()
— если оба выражения являютсяSome
илиOk
, возвращается значение второго выражения. Если одним из выражений являетсяNone
илиErr
, значение этого выражения возвращается сразу
fn main() {
let s1 = Some("some1");
let s2 = Some("some2");
let n: Option<&str> = None;
let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1
assert_eq!(s1.or(n), s1); // Some or None = Some
assert_eq!(n.or(s1), s1); // None or Some = Some
assert_eq!(n.or(n), n); // None1 or None2 = None2
assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1
assert_eq!(o1.or(e1), o1); // Ok or Err = Ok
assert_eq!(e1.or(o1), o1); // Err or Ok = Ok
assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2
assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2
assert_eq!(s1.and(n), n); // Some and None = None
assert_eq!(n.and(s1), n); // None and Some = None
assert_eq!(n.and(n), n); // None1 and None2 = None1
assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2
assert_eq!(o1.and(e1), e1); // Ok and Err = Err
assert_eq!(e1.and(o1), e1); // Err and Ok = Err
assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1
}
Rust
ночной версии поддерживает xor()
для типов Option
, возвращающий Some
только если одно выражение является Some
, но не оба.
or_else()
Похоже на or()
, за исключением того, что вторым выражением должно быть замыкание, возвращающее значение того же типа:
fn main() {
// or_else c Option
let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = || Some("some2"); // похоже на: let fn_some = || -> Option<&str> { Some("some2") };
let n: Option<&str> = None;
let fn_none = || None;
assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1
assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some
assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some
assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2
// or_else с Result
let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2"); // похоже на: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") };
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");
assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1
assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok
assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok
assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2
}
and_then()
Похоже на and()
, за исключением того, что вторым выражением должно быть замыкание, возвращающее значение того же типа:
fn main() {
// and_then c Option
let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = |_| Some("some2"); // похоже на: let fn_some = |_| -> Option<&str> { Some("some2") };
let n: Option<&str> = None;
let fn_none = |_| None;
assert_eq!(s1.and_then(fn_some), s2); // Some1 and_then Some2 = Some2
assert_eq!(s1.and_then(fn_none), n); // Some and_then None = None
assert_eq!(n.and_then(fn_some), n); // None and_then Some = None
assert_eq!(n.and_then(fn_none), n); // None1 and_then None2 = None1
// and_then с Result
let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2"); // похоже на: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") };
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");
assert_eq!(o1.and_then(fn_ok), o2); // Ok1 and_then Ok2 = Ok2
assert_eq!(o1.and_then(fn_err), e2); // Ok and_then Err = Err
assert_eq!(e1.and_then(fn_ok), e1); // Err and_then Ok = Err
assert_eq!(e1.and_then(fn_err), e1); // Err1 and_then Err2 = Err1
}
filter()
Обычно в языках программирования функции filter()
применяются к массивам или итераторам для создания нового массива/итератора путем фильтрации элементов с помощью функции/замыкания. Rust
также предоставляет filter() как адаптер итератора (iterator adapter) для применения замыкания к каждому элементу итератора для его преобразования в другой итератор. Однако здесь мы говорим о функционале filter()
для типов Option
.
Some
возвращается, если мы передали значение Some
, и замыкание вернуло для него true
. None
возвращается, если было передано None
или замыкание вернуло false
. Замыкание использует значение Some
как аргумент. Rust
пока не поддерживает filter()
для Result
.
fn main() {
let s1 = Some(3);
let s2 = Some(6);
let n = None;
let fn_is_even = |x: &i8| x % 2 == 0;
assert_eq!(s1.filter(fn_is_even), n); // Some(3) -> 3 нечетное -> None
assert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 четное -> Some(6)
assert_eq!(n.filter(fn_is_even), n); // None -> значение отсутствует -> None
}
map() и map_err()
Обычно в языках программирования функции map()
используются с массивами или итераторами для применения замыкания к каждому элементу массива/итератора. Rust
также предоставляет map() как адаптер итератора (iterator adapter) для применения замыкания к каждому элементу итератора для его преобразования в другой итератор. Однако здесь мы говорим о функционале filter()
для типов Option
и Result
.
map()
конвертирует типT
, применяя замыкание. Тип данных блоковSome
илиOk
может быть изменен согласно возвращаемому замыканием типу:Option<T>
->Option<U>
,Result<T, E>
->Result<U, E>
С помощью map()
модифицируются только значения Some
и Ok
. Значения Err
не модифицируются (None
вообще не содержит значения).
fn main() {
let s1 = Some("abcde");
let s2 = Some(5);
let n1: Option<&str> = None;
let n2: Option<usize> = None;
let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<usize, &str> = Ok(5);
let e1: Result<&str, &str> = Err("abcde");
let e2: Result<usize, &str> = Err("abcde");
let fn_character_count = |s: &str| s.chars().count();
assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2
assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2
assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2
}
map_err()
для типовResult
— тип данных блоковErr
может быть модифицирован согласно возвращаемому замыканием типу:Result<T, E>
->Result<T, F>
С помощью map_err()
модифицируются только значения Err
. Значения Ok
не модифицируются.
fn main() {
let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<&str, isize> = Ok("abcde");
let e1: Result<&str, &str> = Err("404");
let e2: Result<&str, isize> = Err(404);
let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; // конвертирует str в isize
assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2
}
map_or() и map_or_else()
Помните функции unwrap_or()
и unwrap_or_else()
? Эти функции немного на них похожи. Однако map_or()
и map_or_else()
применяют замыкание к значениям Some
и Ok
и возвращают значение того же типа.
map_or()
— поддерживается только дляOption
(не поддерживается дляResult
). Применяет замыкание к значениюSome
и возвращает соответствующий результат. ДляNone
возвращается значение по умолчанию
fn main() {
const V_DEFAULT: i8 = 1;
let s = Some(10);
let n: Option<i8> = None;
let fn_closure = |v: i8| v + 2;
assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);
assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);
}
map_or_else()
— поддерживается и дляOption
, и дляResult
(последний поддерживается только в ночной версииRust
). Похоже наmap_or()
, только вместо дефолтного значения в качестве первого параметра указывается замыкание
Типы None
не содержат значений. Поэтому для Option
не нужно ничего передавать в качестве аргумента замыкания. Но типы Err
содержат некоторые значения внутри. Поэтому для Result
замыкание иметь доступ к ним.
#![feature(result_map_or_else)] // включаем нестабильную возможность библиотеки 'result_map_or_else' в ночной версии Rust
fn main() {
let s = Some(10);
let n: Option<i8> = None;
let fn_closure = |v: i8| v + 2;
let fn_default = || 1; // `None` не содержит никакого значения. Не нужно ничего передавать в замыкание
assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
assert_eq!(n.map_or_else(fn_default, fn_closure), 1);
let o = Ok(10);
let e = Err(5);
let fn_default_for_result = |v: i8| v + 1; // `Err` содержит некоторое значение. Оно должно быть доступно замыканию
assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12);
assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);
}
ok_or() и ok_or_else()
Как упоминало ранее, ok_or()
и ok_or_else()
трансформируют тип Option
в тип Result
: Some
в Ok
, а None
в Err
.
ok_or()
— обязательным параметром является сообщение об ошибке дляErr
fn main() {
const ERR_DEFAULT: &str = "error message";
let s = Some("abcde");
let n: Option<&str> = None;
let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err(ERR_DEFAULT);
assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)
}
ok_or_else()
— похоже наok_or()
, только в качестве аргумента передается не сообщение об ошибке, а замыкание
fn main() {
let s = Some("abcde");
let n: Option<&str> = None;
let fn_err_message = || "error message";
let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err("error message");
assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)
}
as_ref() и as_mut()
Как упоминалось ранее, эти функции используются для заимствования (borrow) типа T
в качестве ссылки или мутабельной ссылки, соответственно.
as_ref()
— конвертируетOption<T>
вOption<&T>
, аResult<T, E>
вResult<&T, &E>
as_mut()
— конвертируетOption<T>
вOption<&mut T>
, аResult<T, E>
вResult<&mut T, &mut E>
Кастомные типы ошибки
Rust
позволяет нам создавать собственные типы Err
. Мы называем их "кастомными типами ошибки" (custom error types).
Трейт Error
Как вы знаете, трейты определяют, какой функционал должен предоставлять тип. Но нам не всегда нужно определять новые трейты для распространенного функционала, поскольку стандартная библиотека Rust
предоставляет трейты многократного использования, которые можно реализовать в наших типах. Для преобразования любого типа в тип Err
используется трейт std::error::Error:
use std::fmt::{Debug, Display};
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(Error + 'static)> { ... }
}
trait Error: Debug + Display
означает, что трейт Error
наследует от трейтов fmt::Debug
и fmt::Display
:
pub trait Display {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
pub trait Debug {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
Display
- определяет, как пользователь должен видеть эту ошибку как сообщение/вывод, ориентированный на пользователя
- обычно печатается с помощью
println!("{}")
илиeprintln!("{}")
(print
— этоstdin
,eprint
—stderr
)
Debug
- определяет, как следует отображать ошибку при отладке/выводе, ориентированном на программиста
- обычно печатается с помощью
println!("{:?}")
илиeprintln!("{:?}")
- для красивой печати может использоваться
println!("{:#?}")
илиeprintln!("{:#?}")
source()
- низкоуровневый источник ошибки, если таковой имеется
- является опциональным
Реализация простейшего кастомного типа ошибки с помощью std::error::Error
:
use std::fmt;
// Кастомный тип ошибки; может быть любым типом, определенным в текущем крейте.
// Для упрощения примера здесь мы используем пустую структуру
struct AppError;
// Реализуем `std::fmt::Display` для `AppError`
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "An Error Occurred, Please Try Again!") // user-facing output
}
}
// Реализуем `std::fmt::Debug` для `AppError`
impl fmt::Debug for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{{ file: {}, line: {} }}", file!(), line!()) // programmer-facing output
}
}
// Простая функция для генерации `AppError`
fn produce_error() -> Result<(), AppError> {
Err(AppError)
}
fn main() {
match produce_error() {
Err(e) => eprintln!("{}", e), // An Error Occurred, Please Try Again!
_ => println!("No error"),
}
eprintln!("{:?}", produce_error()); // Err({ file: src/main.rs, line: 17 })
}
Надеюсь, вы поняли основные моменты. Реализуем кастомный тип ошибки с кодом ошибки (code
) и сообщением об ошибке (message
):
use std::fmt;
struct AppError {
code: usize,
message: String,
}
// Разные сообщения об ошибке в соответствии с `AppError.code`
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let err_msg = match self.code {
404 => "Sorry, Cannot find the Page!",
_ => "Sorry, something is wrong! Please Try Again!",
};
write!(f, "{}", err_msg)
}
}
// Уникальный формат для отладочного вывода
impl fmt::Debug for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"AppError {{ code: {}, message: {} }}",
self.code, self.message
)
}
}
fn produce_error() -> Result<(), AppError> {
Err(AppError {
code: 404,
message: String::from("Page not found"),
})
}
fn main() {
match produce_error() {
Err(e) => eprintln!("{}", e), // Sorry, Cannot find the Page!
_ => println!("No error"),
}
eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found })
eprintln!("{:#?}", produce_error());
// Err(
// AppError { code: 404, message: Page not found }
// )
}
Стандартная библиотека Rust
предоставляет не только повторно используемые трейты, но также позволяет волшебным образом генерировать реализации для нескольких трейтов через атрибут #[derive]
. Rust
поддерживает derive std::fmt::Debug
для предоставления дефолтного форматирования сообщений об отладке. Поэтому мы можем опустить реализацию std::fmt::Debug
для кастомных типов ошибки и использовать #[derive(Debug)]
перед struct
:
use std::fmt;
#[derive(Debug)] // выводим `std::fmt::Debug` для `AppError`
struct AppError {
code: usize,
message: String,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let err_msg = match self.code {
404 => "Sorry, Cannot find the Page!",
_ => "Sorry, something is wrong! Please Try Again!",
};
write!(f, "{}", err_msg)
}
}
fn produce_error() -> Result<(), AppError> {
Err(AppError {
code: 404,
message: String::from("Page not found"),
})
}
fn main() {
match produce_error() {
Err(e) => eprintln!("{}", e), // Sorry, Cannot find the Page!
_ => println!("No error"),
}
eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found })
eprintln!("{:#?}", produce_error());
// Err(
// AppError {
// code: 404,
// message: "Page not found"
// }
// )
}
Для struct
#[derive(Debug)]
печатает название структуры и список разделенных запятыми названий полей и их значений в фигурных скобках.
Трейт From
При написании реальных программ нам приходится одновременно иметь дело с разными модулями, разными std
и сторонними крейтами. В каждом крейте используются свои типы ошибок. Однако если мы используем собственный тип ошибки, нам следует преобразовать эти ошибки в наш тип. Для этих преобразований мы можем использовать стандартный крейт std::convert::From
:
pub trait From<T>: Sized {
fn from(_: T) -> Self;
}
Как вы знаете, функция String::from()
используется для создания String
из &str
. На самом деле это также реализация крейта std::convert::From
.
Реализация std::convert::From
для кастомного типа ошибки:
use std::fs::File;
use std::io;
#[derive(Debug)]
struct AppError {
kind: String, // тип ошибки
message: String, // сообщение об ошибке
}
// Реализация `std::convert::From` для `AppError`; из `io::Error`
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError {
kind: String::from("io"),
message: error.to_string(),
}
}
}
fn main() -> Result<(), AppError> {
let _file = File::open("nonexistent_file.txt")?; // это генерирует `io::Error`, но поскольку возвращаемым типом является `Result<(), AppError>`, `io::Error` конвертируется в `AppError`
Ok(())
}
// Ошибка времени выполнения (runtime error)
// Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
File::open("nonexistent.txt")?
генерирует io::Error
, но поскольку возвращаемым типом является Result<(), AppError>
, io::Error
конвертируется в AppError
. Из-за распространения ошибки из функции main()
печатается отладочное (Debug
) представление Err
.
Пример обработки нескольких типов ошибки:
use std::fs::File;
use std::io::{self, Read};
use std::num;
#[derive(Debug)]
struct AppError {
kind: String,
message: String,
}
// Реализация `std::convert::From` для `AppError`; из `io::Error`
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError {
kind: String::from("io"),
message: error.to_string(),
}
}
}
// Реализация `std::convert::From` для `AppError`; из `num::ParseIntError`
impl From<num::ParseIntError> for AppError {
fn from(error: num::ParseIntError) -> Self {
AppError {
kind: String::from("parse"),
message: error.to_string(),
}
}
}
fn main() -> Result<(), AppError> {
let mut file = File::open("hello_world.txt")?; // если файл не может быть открыт, генерируется `io::Error`, которая конвертируется в `AppError`
let mut content = String::new();
file.read_to_string(&mut content)?; // если файл не может быть прочитан, генерируется `io::Error`, которая конвертируется в `AppError`
let _number: usize;
_number = content.parse()?; // если содержимое файла не может быть преобразовано в `usize`, генерируется `num::ParseIntError`, которая конвертируется в `AppError`
Ok(())
}
// Несколько возможных ошибок времени выполнения
// Если файла `hello_world.txt` не существует
// Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
// Если у пользователя нет разрешения для доступа к файлу `hello_world.txt`
// Error: AppError { kind: "io", message: "Permission denied (os error 13)" }
// Если файл `hello_world.txt` содержит не числовой контент, например "Hello, world!"
// Error: AppError { kind: "parse", message: "invalid digit found in string" }
Это конец второй части и шпаргалки, в целом.
Happy coding!