Pull to refresh
1965.44
Timeweb Cloud
То самое облако

Большая шпаргалка по Rust. 2/2

Level of difficultyMedium
Reading time39 min
Views6.4K
Original author: Dumindu Madunuwan



Hello world!


Представляю вашему вниманию вторую часть большой шпаргалки по Rust.


Первая часть.


Другой формат, который может показаться вам более удобным.


Обратите внимание: шпаргалка рассчитана на людей, которые хорошо знают любой современный язык программирования, а не на тех, кто только начинает кодить 😉


Также настоятельно рекомендуется хотя бы по диагонали прочитать замечательный Учебник по Rust (на русском языке).


Содержание



Организация кода


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


  1. Функции.
  2. Модули (modules)
    • встроенные модули
    • привязанные к файлу
    • привязанные к иерархии директорий
  3. Крейты (crates)
    • файл lib.rs в том же исполняемом крейте
    • зависимый крейт, определенный в файле Cargo.toml с помощью
    • относительного пути
    • ссылки на репозиторий Git
    • ссылки на crates.io
  4. Рабочие пространства (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 необходимо сделать следующее:


  1. Создать аккаунт на crates.io и сгенерировать токен API.
  2. Выполнить команду 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();
}

Хорошим примером использования рабочих пространств является директория с исходным кодом самого Rustrust-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::envhome_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, eprintstderr)
  • 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!




Tags:
Hubs:
Total votes 28: ↑27 and ↓1+26
Comments2

Articles

Information

Website
timeweb.cloud
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Timeweb Cloud