Skip to main content

Источник.

Основы

Комментарии и документирование кода

Комментарии

// Строчные комментарии
/* Блочные комментарии */

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

Старайтесь избегать использования блочных комментариев.

Комментарии документации (doc comments)

Команда cargo doc генерирует документацию проекта с помощью rustdoc. Для генерации документации используются док-комментарии.

Обычно мы добавляем док-комментарии в библиотечные крейты (library crates). Внутри док-комментариев можно использовать Markdown.

/// Строчные комментарии; документируют следующий элемент
/** Блочные комментарии; документируют следующий элемент */

//! Строчные комментарии; документируют вложенный элемент
/*! Блочные комментарии; документируют вложенный элемент !*/

Пример:

/// Этот модуль содержит тесты; внешний комментарий
mod tests {
// ...
}

mod tests {
//! Этот модуль содержит тесты; внутренний комментарий
// ...
}

Ключевое слово mod используется для модулей. Мы обсудим это позже.

Док-атрибуты (doc attributes)

Док-атрибуты являются альтернативой док-комментариям. Мы используем их для управления rustdoc. Подробнее о док-атрибутах можно почитать здесь.

В следующем примере каждый комментарий эквивалентен соответствующему док-атрибуту:

/// Внешний комментарий
#[doc = "Внешний комментарий"]

//! Внутренний комментарий
#![doc = "Внутренний комментарий"]

Атрибут - это общие метаданные в свободной форме, которые интерпретируются в соответствии с названием, соглашением, версиями языка и компилятора. Синтаксис:

  • внешний атрибут: #[attr]
  • внутренний атрибут: #![attr]

Перед тем, как двигаться дальше...

  • Используйте //! только для написания документации крейта. Для блоков mod используйте /// снаружи блоков. Взгляните на использование док-комментариев //! и /// в популярных крейтах на crates.io. Например, взгляните на serde/src/lib.rs и rand/src/lib.rs
  • выполните команду cargo new hello_lib --lib для создания образца крейта и замените код в файле src/lib.rs на следующий:
//! Простой крейт Hello World

/// Эта функция возвращает приветствие; 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!");
}
}

Затем выполните cargo doc --open для генерации документации и ее открытия в вашем дефолтном браузере.

Переменные, константы и статики

  • В Rust переменные по умолчанию являются иммутабельными (неизменными/неизменяемыми) (immutable), поэтому они называются привязками переменных (variable bindings). Для объявления мутируемой (изменяемой) (mutable) переменной используется ключевое слово mut.
  • Rust - это статически типизированный язык: типы данных (data types) проверяются во время компиляции. Однако это не означает, что типы всех переменных должны указываться явно. Компилятор "смотрит" на использование переменной и устанавливает для нее лучший тип. Но для констант (constants) и статики (statics) типы должны указываться явно. Типы указываются после двоеточие (:).

В следующих примерах мы используем такие типы данных, как bool, i32, i64 и f64. Мы обсудим это позже.

Переменные

Для объявления переменной используется ключевое слово let. Название (имя) переменной может быть привязано к значению или функции. Также поскольку левая часть выражения привязки является "паттерном" (pattern) мы можем привязывать несколько названий к нескольким значениям или функциям.

// Иммутабельная переменная
let a; // Объявление (declaration); без типа данных
a = 5; // Присвоение/присваивание значения (assignment)

let b: i8; // Объявление; с типом данных
b = 5;

let t = true; // Объявление + присвоение; без типа данных
let f: bool = false; // Объявление + присвоение; с типом данных

// Несколько переменных
let (x, y) = (1, 2); // x = 1 и y = 2

// Мутабельная переменная
let mut z = 5;
z = 6;

// Значением переменной становится результат выражения
let z = { x + y }; // z = 3
// Затенение/перезапись переменной (variable shadowing) (см. ниже)
let z = {
let x = 1;
let y = 2;

x + y
}; // z = 3

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

Константы

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

// Название в стиле SCREAMING_SNAKE_CASE
// Тип указывается явно
const CONST_VAR: i32 = 5;

Статики

Ключевое слово static используется для определения объекта типа "глобальная переменная". Для каждого значения существует только один экземпляр такого объекта. Значение находится в фиксированном месте в памяти.

static STATIC_VAR: i32 = 5;

Старайтесь всегда использовать const вместо static для определения констант. Привязка места в памяти к константе требуется очень редко. Использование const позволяет выполнять такие оптимизации, как распространение константы (constant propagation) не только в вашем крейте, но также в зависимых/подчиненных крейтах.

Затенение переменных (variable shadowing)

Иногда возникает необходимость преобразовать значение переменной из одних единиц в другие для дальнейшей обработки. Rust позволяет повторно объявлять переменные с другими типами данных и/или другими настройками мутабельности. Это называется затенением.

fn main() {
let x: f64 = -20.48; // float - число с плавающей точкой
// Явное приведение/преобразование типа
let x: i64 = x.floor() as i64; // int - целое число
println!("{}", x); // -21

let s: &str = "hello"; // &str - строковый срез
// Неявное преобразование типа
let s: String = s.to_uppercase(); // String - строка
println!("{}", s) // HELLO
}

Перед тем, как двигаться дальше...

  • Для названий переменных используется стиль snake_case, а для названий констант и статик - SCREAMING_SNAKE_CASE
  • обычно константы и статики определяются в начале файла снаружи функции (после импорта модулей/объявлений use)
const PI: f64 = 3.14159265359;

fn main() {
println!("Значение π: {}", PI);
}

Функции

Именованные функции

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

Hello world

fn main() {
println!("Hello, world!");
}

Передача аргументов

fn print_sum(a: i8, b: i8) {
println!("Cумма: {}", a + b);
}

Возврат значения

// Без ключевого слова `return`. Возвращается только последнее выражение
fn plus_one(a: i32) -> i32 {
a + 1
// В конце этой строки отсутствует `;`
// Это выражение эквивалентно `return a + 1;`
}
// С ключевым словом `return`
fn plus_two(a: i32) -> i32 {
return a + 2;
// Ключевое слово `return` следует использовать только для условного/раннего возврата (early return).
// Использование `return` в последнем выражении считается плохой практикой
}

Указатели на функцию (function pointers), использование в качестве типа данных

fn main() {
// Без объявлений типов
let p1 = plus_one;
let x = p1(5); // 6

// С объявлениями типов
let p1: fn(i32) -> i32 = plus_one;
let x = p1(5); // 6
}

fn plus_one(a: i32) -> i32 {
a + 1
}

Замыкания (closures)

  • Также известны как анонимные или лямбда-функции
  • типы аргументов и возвращаемого значения являются опциональными

Именованная функция без замыкания

fn main() {
let x = 2;
println!("{}", get_square_value(x));
}

fn get_square_value(i: i32) -> i32 {
i * i
}

С опциональными объявлениями типов аргумента и возвращаемого значения

fn main() {
let x = 2;
// Аргументы передаются внутри | |, а тело выражения оборачивается в { }
let square = |i: i32| -> i32 {
i * i
};
println!("{}", square(x));
}

Без объявлений типов

fn main() {
let x = 2;
let square = |i| i * i; // { } являются опциональными для однострочных замыканий
println!("{}", square(x));
}

С опциональными объявлениями типов; создание + вызов

fn main() {
let x = 2;
let x_square = |i: i32| -> i32 { i * i }(x); // { } являются обязательными при одновременном создании и вызове
println!("{}", x_square);
}

Без объявлений типов; создание + вызов

fn main() {
let x = 2;
let x_square = |i| -> i32 { i * i }(x); // тип возвращаемого значения является обязательным
println!("{}", x_square);
}

Примитивные типы данных

bool

true или false.

let x = true;
let y: bool = false;

char

Единичное скалярное значение Юникода.

// Кавычки должны быть одинарными
let x = 'x';
let y: char = '😎';

Для поддержки Юникода char занимает не 1, а 4 байта (32 бита).

i8, i16, i32, i64, i128

8, 16, 32, 64 и 128-битные целые числа фиксированного размера со знаком (+/-).

ТипMINMAX
i8-128127
i16-3276832767
i32-21474836482147483647
i64-92233720368547758089223372036854775807
i128-170141183460469231731687303715884105728170141183460469231731687303715884105727

Минимальное и максимальное значения рассчитываются по формуле: от -(2ⁿ⁻¹) до 2ⁿ⁻¹-1. Для получения минимального и максимального значения типа можно использовать методы min_value() и max_value(), соответственно, например, i8::min_value().

let x = 10; // дефолтным целочисленным типом в Rust является `i32`
let y: i8 = -128;

u8, u16, u32, u64, u128

8, 16, 32, 64 и 128-битные целые числа фиксированного размера без знака (0/+).

ТипMINMAX
u80255
u16065535
u3204294967295
u64018446744073709551615
u1280340282366920938463463374607431768211455

Минимальное и максимальное значения рассчитываются по формуле: от 0 до 2ⁿ-1. Для получения минимального и максимального значения типа также можно использовать методы min_value() и max_value(), соответственно, например, u8::max_value().

isize, usize

Целочисленные типы со знаком и без знака размером с указатель (pointer sized). Реальный битовый размер зависит от архитектуры компьютера, для которого выполняется компиляция программы. По умолчанию размеры равны 32 битам на 32-битных платформах и 64 битам на 64-битных платформах.

f32, f64

Числа с плавающей запятой размером 32 и 64 бита (числа с десятичной точкой). Rust следует стандарту IEEE для двоичной арифметики с плавающей запятой. Тип f32 аналогичен типу float (одинарная точность) в других языках программирования, а тип f64 - типу double (двойная точность).

let x = 1.5; // дефолтным "плавающим" типом в Rust является `f64`
let y: f64 = 2.0;

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

массив (array)

Список элементов одинакового типа фиксированного размера.

let a = [1, 2, 3];
let a: [i32; 3] = [1, 2, 3]; // [тип; количество элементов]

let b: [i32; 0] = []; // пустой массив

let mut c: [i32; 3] = [1, 2, 3];
c[0] = 2;
c[1] = 4;
c[2] = 6;

println!("{:?}", c); // [2, 4, 6]
println!("{:#?}", c);
// [
// 2,
// 4,
// 6,
// ]

let d = [0; 5]; // [0, 0, 0, 0, 0]
let e = ["x"; 5]; // ["x", "x", "x", "x", "x"]

Массивы по умолчанию являются иммутабельными. Даже при использовании ключевого слова mut количество элементов массива не может быть изменено.

Векторы (vectors) являются динамическими/расширяемыми (growable) массивами. Они могут содержать элементы любого типа, но все элементы должны быть одинакового типа.

Кортеж (tuple)

Упорядоченный список элементов разных или одинакового типа фиксированного размера.

let a = (1, 1.5, true, 'a');
let a: (i32, f64, bool, char) = (1, 1.5, true, 'a');

let mut b = (1, 1.5);
b.0 = 2;
b.1 = 3.0;

println!("{:?}", b); // (2, 3.0)
println!("{:#?}", b);
// (
// 2,
// 3.0,
// )

let (c, d) = b; // c = 2, d = 3.0
let (e, _, _, f) = a; // e = 1, f = 'a'

let g = (0,); // одноэлементный кортеж
let h = (b, (2, 4), 5); // ((2, 3.0), (2, 4), 5)

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

Срез/фрагмент (slice)

Ссылка (reference) динамического размера на другую структуру данных.

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

let a: [i32; 4] = [1, 2, 3, 4]; // родительский массив

let b: &[i32] = &a; // срез всего массива
let c = &a[0..4]; // от первого до четвертого (не включая) элемента
let d = &a[..]; // срез всего массива

let e = &a[1..3]; // [2, 3]
let f = &a[1..]; // [2, 3, 4]
let g = &a[..3]; // [1, 2, 3]

str

Безразмерная (unsized) последовательность UTF-8 фрагментов строк Юникода.

let a = "Hello, world."; // a: &'static str
let b: &str = "こんにちは, 世界!";

Это иммутабельный/статически выделенный срез, содержащий последовательность кодовых точек UTF-8 неизвестного размера, хранящуюся где-то в памяти. &str используется для заимствования (borrow) и присвоения всего массива данной переменной.

Функция

p1 - это указатель на функцию plus_one():

fn main() {
let p1: fn(i32) -> i32 = plus_one;
let x = p1(5); // 6
}

fn plus_one(a: i32) -> i32 {
a + 1
}

Перед тем, как двигаться дальше...

  • В Rust дефолтным целочисленным типом является i32, а дефолтным плавающим типом - f64
let i = 10;   // let i: i32 = 10;
let f = 3.14; // let f: f64 = 3.14;
  • тип числа также может определяться в виде суффикса. Для улучшения читаемости длинные числа могут разделяться _
let a = 5i8; //let a: i8 = 5;

let b = 100_000_000; // let b = 100000000;
// `_` могут размещаться произвольно, например, 10000_0000 - тоже валидное число

let pi = 3.141_592_653_59f64; // let pi: f64 = 3.14159265359

const PI: f64 = 3.141_592_653_59; // тип констант и статик должен указываться после названия переменной
  • в Rust существует несколько строковых типов. Тип String - это строка, выделенная в куче (heap-allocated). Она является расширяемой с гарантированной кодировкой UTF-8. По общему правилу, когда нам необходимо владение, следует использовать String, когда нужно заимствовать строку, следует использовать &str
  • Тип String может быть сгенерирован из &str с помощью методов to_string() или String::from(). Метод as_str() позволяет конвертировать String в &str
let s: &str = "Hello"; // &str

let s = s.to_string(); // String
let s = String::from(s); // String

let s = s.as_str(); // &str

Операторы

Арифметические операторы

+ - * / %

let a = 5;
let b = a + 1; // 6
let c = a - 1; // 4
let d = a * 2; // 10
let e = a / 2; // 2, а не 2.5
let f = a % 2; // 1

let g = 5.0 / 2.0; // 2.5

Операторы сравнения

== != < > <= >=

let a = 1;
let b = 2;

let c = a == b; // false
let d = a != b; // true
let e = a < b; // true
let f = a > b; // false
let g = a <= a; // true
let h = a >= a; // true

let i = true > false; // true
let j = 'a' > 'A'; // true

Логические операторы

! && ||

let a = true;
let b = false;

let c = !a; // false
let d = a && b; // false
let e = a || b; // true

В целочисленных типах ! инвертирует отдельные биты в представление значения в виде дополнения до двух (two’s complement representation).

let a = !-2; // 1
let b = !-1; // 0
let c = !0; // -1
let d = !1; // -2

Побитовые операторы

& | ^ << >>

let a = 1;
let b = 2;

let c = a & b; // 0 (01 && 10 -> 00)
let d = a | b; // 3 (01 || 10 -> 11)
let e = a ^ b; // 3 (01 != 10 -> 11)
let f = a << b; // 4 (добавляем b нулей в конец a -> '01'+'00' -> 100)
let g = a >> b; //0 (удаляем b битов с конца a -> '01'-'10' -> 0)

Операторы присваивания и составные (compound) операторы присваивания

let mut a = 2;

a += 5; // 2 + 5 = 7
a -= 2; // 7 - 2 = 5
a *= 5; // 5 * 5 = 25
a /= 2; // 25 / 2 = 12 not 12.5
a %= 5; // 12 % 5 = 2

a &= 2; // 10 && 10 -> 10 -> 2
a |= 5; // 010 || 101 -> 111 -> 7
a ^= 2; // 111 != 010 -> 101 -> 5
a <<= 1; // '101'+'0' -> 1010 -> 10
a >>= 2; // '1010'-'10' -> 10 -> 2

Оператор преобразования типа (type casting)

as

let a = 15;
let b = (a as f64) / 2.0; // 7.5

Операторы заимствования и разыменования

& &mut *

Операторы & и &mut используются для заимствования (borrow), а оператор * - для разыменования (dereference). Мы обсудим их позже.

Перед тем, как двигаться дальше...

  • Объединение/соединение (concatenation) строк:
let (s1, s2) = ("some", "thing"); // обе переменные имеют тип `&str`

// Типами остальных переменных является `String`
let s = String::from(s1) + s2; // String + &str

let mut s = String::from(s1); // String
s.push_str(s2); // + &str

let s = format!("{}{}", s1, s2); // &str/String + &str/String

let s = [s1, s2].concat(); // `&str` или массив `String`

Потоки управления

if else

  • if
let age = 13;

if age < 18 {
println!("Hello, child!"); // код печатает это
}
  • if и else
let i = 7;

if i % 2 == 0 {
println!("Четное");
} else {
println!("Нечетное"); // код печатает это
}
  • let
let age: u8 = 13;
let is_below_eighteen = if age < 18 { true } else { false }; // true
  • еще примеры
// Простой пример
let team_size = 7;

if team_size < 5 {
println!("Small");
} else if team_size < 10 {
println!("Medium"); // код печатает это
} else {
println!("Large");
}
// Отрефакторим предыдущий пример
let team_size = 7;
let team_size_in_text;

if team_size < 5 {
team_size_in_text = "Small";
} else if team_size < 10 {
team_size_in_text = "Medium";
} else {
team_size_in_text = "Large";
}

println!("Current team size : {}", team_size_in_text); // Current team size : Medium
// Еще раз отрефакторим
let team_size = 7;
let team_size = if team_size < 5 {
"Small" // ⭐️ no ;
} else if team_size < 10 {
"Medium"
} else {
"Large"
};

println!("Current team size : {}", team_size); // Current team size : Medium

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

match

Ключевое слово match позволяет выполнять поиск совпадения (сопоставление с образцом) (pattern matching):

let tshirt_width = 20;
let tshirt_size = match tshirt_width {
16 => "S", // проверяет 16
17 | 18 => "M", // проверяет 17 и 18
19 ..= 21 => "L", // проверяет от 19 до 21 (включительно)
22 => "XL",
_ => "Not Available",
};

println!("{}", tshirt_size); // L
let is_allowed = false;
let list_type = match is_allowed {
true => "Full",
false => "Restricted"
// Дефолтное/_ условие может быть пропущено,
// поскольку `is_allowed` имеет логическое значение и
// все возможности исчерпаны
};

println!("{}", list_type); // Restricted
let marks_paper_a: u8 = 25;
let marks_paper_b: u8 = 30;

let output = match (marks_paper_a, marks_paper_b) {
(50, 50) => "Full marks for both papers",
(50, _) => "Full marks for paper A",
(_, 50) => "Full marks for paper B",
(x, y) if x > 25 && y > 25 => "Good",
(_, _) => "Work hard"
};

println!("{}", output); // Work hard

loop

Ключевое слово loop позволяет объявлять бесконечные циклы (похоже на while true):

loop {
println!("Loop forever!");
}
// Для управления циклом используются ключевые слова `break` и `continue`
let mut a = 0;

loop {
if a == 0 {
println!("Skip Value : {}", a);
a += 1;
// Пропускаем итерацию
continue;
} else if a == 2 {
println!("Break At : {}", a);
// Прерываем цикл
break;
}

println!("Current Value : {}", a);
a += 1;
}
// Для именования циклов используются метки (labels)
let mut b1 = 1;

'outer_loop: loop { // устанавливаем метку `outer_loop`
let mut b2 = 1;

'inner_loop: loop {
println!("Current Value : [{}][{}]", b1, b2);

if b1 == 2 && b2 == 2 {
break 'outer_loop; // прерываем `outer_loop`
} else if b2 == 5 {
break;
}

b2 += 1;
}

b1 += 1;
}

while

let mut a = 1;

while a <= 10 {
println!("Current value : {}", a);
a += 1; // в Rust отсутствуют операторы `++` и `--`
}
// `break` и `continue`
let mut b = 0;

while b < 5 {
if b == 0 {
println!("Skip value : {}", b);
b += 1;
continue;
} else if b == 2 {
println!("Break At : {}", b);
break;
}

println!("Current value : {}", b);
b += 1;
}
// Метки
let mut c1 = 1;

'outer_while: while c1 < 6 { // устанавливаем метку `outer_while`
let mut c2 = 1;

'inner_while: while c2 < 6 {
println!("Current Value : [{}][{}]", c1, c2);
if c1 == 2 && c2 == 2 { break 'outer_while; } // прерываем `outer_while`
c2 += 1;
}

c1 += 1;
}

for

// от 0 до 10 (не включая); В JavaScript это выглядит как `for (let i = 0; i < 10; i++)`
for i in 0..10 {
println!("Current value : {}", i);
}
// от 1 до 10 (включительно); В JavaScript это выглядит как `for (let i = 1; i <= 10; i++)`
for i in 1..=10 {
println!("Current value : {}", i);
}
// `break` и `continue`
for b in 0..6 {
if b == 0 {
println!("Skip Value : {}", b);
continue;
} else if b == 2 {
println!("Break At : {}", b);
break;
}

println!("Current value : {}", b);
}
// Метки
'outer_for: for c1 in 1..6 { // устанавливаем метку `outer_for`

'inner_for: for c2 in 1..6 {
println!("Current Value : [{}][{}]", c1, c2);
if c1 == 2 && c2 == 2 { break 'outer_for; } // прерываем `outer_for`
}

}
// Работа с массивами/векторами
let group : [&str; 4] = ["Mark", "Larry", "Bill", "Steve"];

for n in 0..group.len() { // group.len() = 4 -> 0..4, вообще проверять `group.len()` на каждой итерации - плохая практика
println!("Current Person : {}", group[n]);
}

for person in group.iter() { // `group.iter()` превращает массив в простой итератор
println!("Current Person : {}", person);
}

Больше, чем основы

Векторы (vectors)

Как вы помните, массив - это список элементов одинакового типа фиксированного размера. Даже при использовании ключевого слова mut, количество элементов массива не может быть изменено. Вектор - это своего рода динамический массив, но все его элементы также должны быть одинакового типа.

Вектор имеет общий (generic) тип Vec<T>. T - любой тип, например, типом вектора 32-битных целых чисел будет Vec<i32>. Данные вектора хранятся в динамически выделяемой куче.

Создание пустого вектора

let mut a = Vec::new(); // с помощью статического метода `new()`
let mut b = vec![]; // с помощью макроса `vec!`

Создание вектора с типом данных

let mut a2: Vec<i32> = Vec::new();
let mut b2: Vec<i32> = vec![];
let mut b3 = vec![1i32, 2, 3]; // с помощью суффикса первого элемента

let mut b4 = vec![1, 2, 3];
let mut b5: Vec<i32> = vec![1, 2, 3];
let mut b6 = vec![1i32, 2, 3];
let mut b7 = vec![0; 10]; // десять нулей

Доступ и изменение данных

// Доступ и изменение существующих данных
let mut c = vec![5, 4, 3, 2, 1];
c[0] = 1;
c[1] = 2;
//c[6] = 2; ошибка - индекс за пределами допустимого диапазона (0..6)
println!("{:?}", c); //[1, 2, 3, 2, 1]

// `push` и `pop`
let mut d: Vec<i32> = Vec::new();
d.push(1); // [1] : добавляем элемент в конец
d.push(2); // [1, 2]
d.pop(); // [1] : удаляем последний элемент

// Емкость (capacity) и повторное выделение памяти (reallocation)
let mut e: Vec<i32> = Vec::with_capacity(10);
println!("Length: {}, Capacity : {}", e.len(), e.capacity()); // Length: 0, Capacity : 10

// Для этого не требуется повторное выделение памяти
for i in 0..10 {
e.push(i);
}
// А для этого требуется
e.push(11);

По сути, вектор представляет 3 вещи:

  • указатель на данные (pointer)
  • количество элементов (длина - length)
  • емкость (capacity) - пространство, доступное будущим элементам

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

Тип данных String - это вектор байт в кодировке UTF-8 ([u8]). Однако символы строки не доступны по индексу из-за особенностей кодировки.

Векторы могут быть использованы совместно с итераторами 3 способами:

let mut v = vec![1, 2, 3, 4, 5];

for i in &v {
println!("Ссылка на {}", i);
}

for i in &mut v {
println!("Мутабельная ссылка на {}", i);
}

for i in v {
println!("Забирает владение вектором и его элементом {}", i);
}

Структуры (structs)

Структуры используются для инкапсуляции связанных свойств в один унифицированный тип данных.

По соглашению структуры именуются в стиле PascalCase.

Существует 3 варианта структур:

  1. C-подобные (C-like) структуры:
  • разделенные запятыми пары ключ: значение
  • список в фигурных скобках
  • похожи на классы (без методов) в ООП-языках
  • поскольку поля имеют названия, они доступны через точечную нотацию
  1. Кортежные (tuple) структуры
  • разделенные запятыми значения
  • круглые скобки, как у кортежей
  • похожи на именованные кортежи
  1. Пустые (unit) структуры
  • не имеют членов
  • определяют новый тип, похожий на пустой кортеж (())
  • редко используются, полезны в дженериках (generics)

Говоря об ООП в Rust, следует отметить, что атрибуты и методы объектов размещаются отдельно в структурах и трейтах (traits). Структуры содержат только атрибуты, трейты - только методы. Они объединяются с помощью impl (реализаций). Мы обсудим это позже.

C-подобные структуры

// Объявление структуры
struct Color {
red: u8,
green: u8,
blue: u8
}

fn main() {
// Создание экземпляра
let black = Color { red: 0, green: 0, blue: 0 };

// Доступ к полям через точки
println!("Black = rgb({}, {}, {})", black.red, black.green, black.blue); // Black = rgb(0, 0, 0)

// Структуры являются иммутабельными по умолчанию,
// ключевое слово `mut` позволяет сделать их мутабельными.
// Это не относится к количеству или типу полей
let mut link_color = Color { red: 0, green: 0, blue: 255 };
link_color.blue = 238;
println!("Link Color = rgb({}, {}, {})", link_color.red, link_color.green, link_color.blue); //Link Color = rgb(0, 0, 238)

// Копируем элементы из другого экземпляра
let blue = Color { blue: 255, ..link_color };
println!("Blue = rgb({}, {}, {})", blue.red, blue.green, blue.blue); // Blue = rgb(0, 0, 255)

// Деструктурируем экземпляр с помощью привязки `let`.
// Это не уничтожает `blue`
let Color { red: r, green: g, blue: b } = blue;
println!("Blue = rgb({}, {}, {})", r, g, b); // Blue = rgb(0, 0, 255)

// Создаем экземпляр с помощью функции
let midnightblue = get_midnightblue_color();
println!("Midnight Blue = rgb({}, {}, {})", midnightblue.red, midnightblue.green, midnightblue.blue); // Midnight Blue = rgb(25, 25, 112)

// Деструктурируем экземпляр с помощью привязки `let`.
let Color { red: r, green: g, blue: b } = get_midnightblue_color();
println!("Midnight Blue = rgb({}, {}, {})", r, g, b); // Midnight Blue = rgb(25, 25, 112)
}

fn get_midnightblue_color() -> Color {
Color {red: 25, green: 25, blue: 112}
}

Кортежные структуры

Когда кортежная структура содержит только один элемент, мы называем ее паттерном newtype (newtype pattern), потому что такая структура помогает создавать новые типы.

struct Color(u8, u8, u8);
struct Kilometers(i32);

fn main() {
// Создаем новый экземпляр
let black = Color(0, 0, 0);

// Деструктурируем экземпляр с помощью привязки `let`.
// Это не разрушает `black`
let Color(r, g, b) = black;
println!("Black = rgb({}, {}, {})", r, g, b); // Black = rgb(0, 0, 0);

// Newtype pattern
let distance = Kilometers(20);
// Деструктурируем экземпляр с помощью привязки `let`.
let Kilometers(distance_in_km) = distance;
println!("The distance: {} km", distance_in_km); // The distance: 20 km
}

Пустые структуры

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

struct Electron;

fn main() {
let x = Electron;
}

Перечисления (enums)

Перечисление - это единичный тип. Оно содержит варианты (variants), т.е. возможные значения перечисления, например:

enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}

// `Day` - это перечисление
// Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday - варианты

Варианты доступны через нотацию ::, например, Day::Sunday.

Каждый вариант может:

  • быть пустым (не содержать данных)
  • содержать упорядоченные безымянные данные (кортежный вариант)
  • содержать именованные данные (структурный вариант)
enum FlashMessage {
Success, // пустой (unit) вариант
Warning{ category: i32, message: String }, // структурный вариант
Error(String) // кортежный вариант
}

fn main() {
let mut form_status = FlashMessage::Success;
print_flash_message(form_status);

form_status = FlashMessage::Warning { category: 2, message: String::from("Поле X является обязательным") };
print_flash_message(form_status);

form_status = FlashMessage::Error(String::from("Ошибка подключения"));
print_flash_message(form_status);
}

fn print_flash_message(m : FlashMessage) {
// Сопоставление с образцом (pattern matching)
match m {
FlashMessage::Success =>
println!("Форма успешно отправлена"),
FlashMessage::Warning { category, message } => // деструктуризация, названия полей должны совпадать
println!("Предупреждение: {} - {}", category, message),
FlashMessage::Error(msg) =>
println!("Ошибка: {}", msg)
}
}

Дженерики (generics)

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

Суть заключается в том, что вместо объявления конкретного типа данных, мы используем прописную букву (идентификатор в стиле PascalCase), например, вместо x: u8 мы пишем x: T. Однако нам нужно сообщить компилятору о том, что T - это общий тип (может быть любым типом), поэтому мы добавляем <T> после названия функции, например.

Генерализация функций

fn takes_anything<T>(x: T) { // `x` имеет тип `T`, `T` - это общий тип
// ...
}

fn takes_two_of_the_same_things<T>(x: T, y: T) { // `x` и `y` имеют одинаковый тип
// ...
}

fn takes_two_things<T, U>(x: T, y: U) { // `x` и `y` имеют разные типы
// ...
}

Генерализация структур

struct Point<T> {
x: T,
y: T,
}

fn main() {
let point_a = Point { x: 0, y: 0 }; // `T` становится `i32`
let point_b = Point { x: 0.0, y: 0.0 }; // `T` становится `f64`
}

// При добавлении реализации для общей структуры параметр типа также должен быть добавлен после ключевого слова `impl`
// impl<T> Point<T> {

Генерализация перечислений

enum Option<T> {
Some(T),
None,
}

enum Result<T, E> {
Ok(T),
Err(E),
}

Option и Result - это специальные общие типы, определенные в стандартной библиотеке Rust:

  • опциональное значение (Option) может быть некоторым значением (Some) или отсутствовать (None)
  • результат (Result) может быть успехом (Ok) или ошибкой (Err)

Примеры использования Option

// ---
fn get_id_by_username(username: &str) -> Option<usize> {
// Если имя пользователя знакомо системе, возвращаем `userId`
return Some(userId);
// иначе
None
}

// ---
struct Task {
title: String,
assignee: Option<Person>,
}

// Вместо `Person`, мы используем `Option<Person>`,
// поскольку задача может не быть никому назначена

// ---
// При использовании `Option` в качестве типа возвращаемого функцией значения,
// мы можем использовать `match` для перехвата соответствующего значения
fn main() {
let username = "anonymous";
match get_id_by_username(username) {
None => println!("Пользователь не найден"),
Some(i) => println!("Идентификатор пользователя: {}", i)
}
}

Примеры использования Result

Тип Option - это способ, которым система типов Rust выражает возможность отсутствия значения. Тип Result - это способ, которым система типов Rust выражает возможность ошибки.

// ---
fn get_word_count_from_file(file_name: &str) -> Result<u32, &str> {
// Если файл не найден в файловой системе, возвращаем ошибку
return Err("Файл не найден")
// иначе, считаем и возвращаем количество слов
// let mut word_count: u32 = ...;
Ok(word_count)
}

// ---
// Здесь мы также можем использовать `match`
fn main() {
let mut file_name = "file_a";
match get_word_count_from_file(file_name) {
Ok(i) => println!("Количество слов: {}", i),
Err(e) => println!("Ошибка: {}", e)
}
}

Для Option и Result реализовано большое количество полезных методов (полистайте официальную документацию).

Реализации и трейты (impls & traits)

Реализации (impls) используются для определения методов структур и перечислений.

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

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

Реализации без трейтов

struct Player {
first_name: String,
last_name: String,
}

impl Player {
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}

fn main() {
let player_1 = Player {
first_name: "Rafael".to_string(),
last_name: "Nadal".to_string(),
};

println!("Player 01: {}", player_1.full_name());
}

// Тип и его реализация должны находиться в одном крейте

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

Реализации с трейтами, но без дефолтных методов

struct Player {
first_name: String,
last_name: String,
}

trait FullName {
fn full_name(&self) -> String;
}

impl FullName for Player {
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}

fn main() {
let player_2 = Player {
first_name: "Roger".to_string(),
last_name: "Federer".to_string(),
};

println!("Player 02: {}", player_2.full_name());
}

// В отличие от функций, трейты могут содержать константы и типы

Реализации с трейтами и дефолтными методами

trait Foo {
fn bar(&self);
fn baz(&self) { println!("We called baz."); }
}

Как видите, методы принимают первый специальный параметр - сам тип. Он может быть self (значение в стеке - владение), &self (ссылка на значение) или &mut self (мутабельная ссылка).

Реализации с ассоциированными функциями

Некоторые языки поддерживают статические методы. Такие методы вызываются на самом классе, а не на его экземпляре. В Rust такие методы называются ассоциированными функциями (associated functions). При их вызове на структуре используется :: вместо ., например, Person::new("Elon Musk Jr");:

struct Player {
first_name: String,
last_name: String,
}

impl Player {
// Ассоциированная функция - метод структуры/статический метод
fn new(first_name: String, last_name: String) -> Player {
Player {
first_name: first_name,
last_name: last_name,
}
}

// Метод экземпляра
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}

fn main() {
let player_name = Player::new("Serena".to_string(), "Williams".to_string()).full_name();
println!("Player: {}", player_name);
}

// Мы используем нотацию `::` для `new()` и нотацию `.` для `full_name()`

// Вместо использования `new()` и `full_name()` по-отдельности,
// мы можем использовать цепочку методов, например, `player.add_points(2).get_point_count();`

Трейты с дженериками

trait From<T> {
fn from(T) -> Self;
}
impl From<u8> for u16 {
//...
}
impl From<u8> for u32{
//...
}

Наследование трейтов

trait Person {
fn full_name(&self) -> String;
}

trait Employee: Person { // `Employee` наследует от `Person`
fn job_title(&self) -> String;
}

trait ExpatEmployee: Employee + Expat { // `ExpatEmployee` наследует от `Employee` и `Expat`
fn additional_tax(&self) -> f64;
}

Трейт-объекты

Хотя Rust предпочитает статическую отправку (static dispatch), он также поддерживает динамическую отправку (dynamic dispatch) через механизм под названием "трейт-объекты" (trait objects).

Динамическая отправка - это процесс выбора реализации полиморфной операции (метода или функции) для вызова во время выполнения (runtime).

trait GetSound {
fn get_sound(&self) -> String;
}

struct Cat {
sound: String,
}
impl GetSound for Cat {
fn get_sound(&self) -> String {
self.sound.clone()
}
}

struct Bell {
sound: String,
}
impl GetSound for Bell {
fn get_sound(&self) -> String {
self.sound.clone()
}
}

fn make_sound<T: GetSound>(t: &T) {
println!("{}!", t.get_sound())
}

fn main() {
let kitty = Cat { sound: "Meow".to_string() };
let the_bell = Bell { sound: "Ding Dong".to_string() };

make_sound(&kitty); // Meow!
make_sound(&the_bell); // Ding Dong!
}

Сложная часть

Владение (ownership)

fn main() {
let a = [1, 2, 3];
let b = a;
println!("{:?} {:?}", a, b); // [1, 2, 3] [1, 2, 3]
}

fn main() {
let a = vec![1, 2, 3];
let b = a;
println!("{:?} {:?}", a, b); // ошибка; использование перемещенного (moved) значения: `a`
}

В этих примерах мы пытаемся присвоить b значение a. В обоих блоках код почти одинаковый, разница лишь в типах данных. Во втором случае возникает ошибка. Это связано с владением (ownership).

Что такое владение?

Привязки переменных владеют тем, к чему они привязаны. Данные могут одновременно иметь только одного владельца. Когда привязка выходит за пределы области видимости (scope), Rust освобождает связанные ресурсы. Так Rust обеспечивает безопасность работы с памятью.

Копируемые и перемещаемые типы

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

  1. Копируемым (copy type)
    • данные копируются и присваиваются или передаются
    • состояние владения исходных данных устанавливается в состояние "скопировано"
    • характерно в основном для примитивных типов
  2. Перемещаемым (move type)
    • данные перемещаются в новую привязку и становятся недоступными через оригинальную привязку
    • состояние владения исходных данных устанавливается в состояние "перемещено"
    • характерно для непримитивных типов

Поведение типа определяется реализованными на нем трейтами. По умолчанию привязки переменных имеют "семантику перемещения" (move semantics). Однако если для типа реализован трейт core::marker::Copy, он имеет "семантику копирования".

Таким образом, во втором примере объект вектора перемещается в b, и a лишается владения для доступа к нему.

Заимствование (borrowing)

В реальных приложениях мы чаще всего передаем переменные функциям или присваиваем их другим переменным. В этих случаях мы ссылаемся (referencing) на оригинальные привязки, заимствуем (borrow) их данные.

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

Существует 2 типа заимствования:

  1. Общее/распределенное (shared) заимствование (&T)
    • данные могут быть заимствованы одним или несколькими пользователями, но не должны модифицироваться, т.е. доступны только для чтения
  2. Мутабельное заимствование (&mut T)
    • данные могут заимствоваться и модифицироваться одновременно только одним пользователем

Правила заимствования

  1. Данные могут быть заимствованы только как общее заимствование или как мутабельное заимствование, но не как то и другое одновременно.
  2. Заимствование применяется как к копируемым, так и к перемещаемым типам.
  3. Необходимо соблюдать правила времен жизни (lifetimes). Мы обсудим это позже.
fn main() {
let mut a = vec![1, 2, 3];
let b = &mut a; // &mut заимствование `a` начинается здесь
// v
// ... // v
// ... // v
} // &mut заимствование `a` заканчивается здесь


fn main() {
let mut a = vec![1, 2, 3];
let b = &mut a; // &mut заимствование `a` начинается здесь
// ...

println!("{:?}", a); // пытаемся получить доступ к `a` как к общему заимствованию, получаем ошибку
} // &mut заимствование `a` заканчивается здесь


fn main() {
let mut a = vec![1, 2, 3];
{
let b = &mut a; // &mut заимствование `a` начинается здесь
// ...
} // &mut заимствование `a` заканчивается здесь

println!("{:?}", a); // `a` доступна как общее заимствование
}

Примеры общего заимствования

fn main() {
let a = [1, 2, 3];
let b = &a;
println!("{:?} {}", a, b[0]); // [1, 2, 3] 1
}


fn main() {
let a = vec![1, 2, 3];
let b = get_first_element(&a);

println!("{:?} {}", a, b); // [1, 2, 3] 1
}

fn get_first_element(a: &Vec<i32>) -> i32 {
a[0]
}

Примеры мутабельного заимствования

fn main() {
let mut a = [1, 2, 3];
let b = &mut a;
b[0] = 4;
println!("{:?}", b); // [4, 2, 3]
}


fn main() {
let mut a = [1, 2, 3];
{
let b = &mut a;
b[0] = 4;
}

println!("{:?}", a); // [4, 2, 3]
}


fn main() {
let mut a = vec![1, 2, 3];
let b = change_and_get_first_element(&mut a);

println!("{:?} {}", a, b); // [4, 2, 3] 4
}

fn change_and_get_first_element(a: &mut Vec<i32>) -> i32 {
a[0] = 4;
a[0]
}

Времена жизни (lifetimes)

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

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

Управление памятью - это управление ресурсами применительно к памяти компьютера. До середины 1990-х в большинстве языков программирования использовалось ручное управление памятью (manual memory management, MMM), которое требовало от программиста предоставления явных инструкций по определению и освобождению/удалению (deallocate) неиспользуемых объектов/мусора (garbage). В 1959 John McCarthy изобрел сборку мусора (garbage collection, GC) - разновидность автоматического управления памятью (automatic memory management, AMM). Сборщик мусора автоматически (без участия программиста) определяет неиспользуемую память и освобождает ее. Похожий функционал предоставляет автоматический подсчет ссылок (automatic reference counting, ARC), используемый в Objective-C и Swift.

Что такое время жизни?

В Rust

  • у ресурса может быть только один владелец в одно время. При выходе за пределы области видимости, Rust удаляет его
  • когда мы хотим повторно использовать ресурс, мы ссылаемся на него, т.е. заимствуем его содержимое
  • при работе с ссылками, мы должны указать аннотации времени жизни (lifetime annotations) для предоставления компилятору инструкций о том, как долго ссылочные ресурсы должны жить
  • поскольку аннотации времени жизни делают код более многословным, для того, чтобы сделать некоторые общие паттерны более эргономичными, Rust позволяет исключать (elided)/опускать эти аннотации в определениях fn. В этом случае компилятор присваивает времена жизни неявно

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

  • В отличие от C и C++, Rust, как правило, не уничтожает (drop) значения явно
  • в отличие от GC, Rust не выполняет вызовы освобождения (deallocation calls), когда данные больше не используются
  • Rust выполняет вызовы освобождения, когда данные вот-вот выйдут за пределы области видимости, а затем обеспечивает отсутствие ссылок на этот ресурс

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

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

При использовании ссылок

  1. В объявлениях функций
  • времена жизни ссылочных входных и выходных параметров указываются после &, например: (x: &'a mut str), -> &'a str
  • времена жизни указываются после названия функции как общие типы, например: fn foo<'a>(){}, fn bar<'a, 'b>(){}
// Без параметров, возвращается ссылка
fn function<'a>() -> &'a str {}

// Один параметр
fn function<'a>(x: &'a str) {}

// Один параметр и возвращаемое значение, оба имеют одинаковое время жизни.
// Результат должен жить, как минимум, также долго, как параметр
fn function<'a>(x: &'a str) -> &'a str {}

// Несколько параметров, только один параметр и результат имеют общее время жизни.
// Результат должен жить, как минимум, также долго, как параметр `y`
fn function<'a>(x: i32, y: &'a str) -> &'a str {}

// Несколько параметров и результат с общим временем жизни
// Результат должен жить, как минимум, также долго, как оба параметра
fn function<'a>(x: &'a str, y: &'a str) -> &'a str {}

// Несколько параметров с разными временами жизни
// Результат должен жить, как минимум, также долго, как параметр `x`
fn function<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {}
  1. В объявлениях структур или перечислений
  • времена жизни ссылочных элементов указываются после &
  • времена жизни указываются как общие типы после названий структур/перечислений
// Один элемент
// Данные `x` должны жить, как минимум, также долго, как структура
struct Struct<'a> {
x: &'a str
}

// Несколько элементов
// Данные `x` и `y` должны жить, как минимум, также долго, как структура
struct Struct<'a> {
x: &'a str,
y: &'a str
}

// Перечисление с одним элементом/вариантом
// Данные варианта должны жить, как минимум, также долго, как перечисление
enum Enum<'a> {
Variant(&'a Type)
}
  1. В реализациях и трейтах
struct Struct<'a> {
x: &'a str
}
// Да, целых 3 общих типа
impl<'a> Struct<'a> {
fn function<'a>(&self) -> &'a str {
self.x
}
}

struct Struct<'a> {
x: &'a str,
y: &'a str
}
impl<'a> Struct<'a> {
fn new(x: &'a str, y: &'a str) -> Struct<'a> { // `<'a>` после `new` можно опустить, поскольку оно есть у `impl`
Struct {
x : x,
y : y
}
}
}

// Одно и тоже
impl<'a> Trait<'a> for Type
impl<'a> Trait for Type<'a>

Неявное выведение времени жизни

Неявное выведение времени жизни (lifetime elision) - это процесс автоматического определения времен жизни общих паттернов компилятором.

В настоящее время неявное выведение времени жизни поддерживается только для fn. В будущем оно также будет поддерживаться для impl.

Времена жизни определений fn могут быть выведены неявно, если

  • только один параметр из списка передается по ссылке
  • параметром является &self или &mut self
fn triple(x: &u64) -> u64 { // только один параметр передается по ссылке
x * 3
}

fn filter(x: u8, y: &str) -> &str { // только один параметр передается по ссылке
if x > 5 { y } else { "invalid inputs" }
}

struct Player<'a> {
id: u8,
name: &'a str
}
impl<'a> Player<'a> { // неявное выведение времени жизни для `impl` пока не поддерживается
fn new(id: u8, name: &str) -> Player { // только один параметр передается по ссылке
Player {
id : id,
name : name
}
}

fn heading_text(&self) -> String { // параметром является `&self` (или `&mut self`)
format!("{}: {}", self.id, self.name)
}
}

fn main() {
let player1 = Player::new(1, "Serena Williams");
let player1_heading_text = player1.heading_text()
println!("{}", player1_heading_text);
}

В процессе неявного выведения времени жизни

  • каждый аргумент, передаваемый по ссылке, получает отдельное время жизни: (x: &str, y: &str) -> <'a, 'b>(x: &'a str, y: &'b str)
  • если список параметров содержит только один параметр, передаваемый по ссылке, его время жизни присваивается возвращаемому функцией значению: (x: i32, y: &str) -> &str -> <'a>(x: i32, y: &'a str) -> &'a str
  • даже если по ссылке передается несколько аргументов, но одним из аргументов является &self или &mut self, время жизни этого аргумента становится временем жизни возвращаемого методом значения: impl Impl { fn function(&self, x: &str) -> &str {} } -> impl<'a> Impl<'a> { fn function(&'a self, x: &'b str) -> &'a str {} }
  • во всех остальных случаях время жизни должно указываться явно

Аннотации 'static

Аннотация времени жизни 'static является зарезервированной. Такие ссылки являются валидными на протяжении всей работы программы. Они сохраняются в сегменте данных исполняемого файла и имеют глобальную область видимости (не могут выйти за пределы области видимости).

 // Константа с временем жизни `'static`

static N: i32 = 5;

let a = "Hello, world."; // a: &'static str

fn index() -> &'static str { // не нужно указывать `<'static>` после названия функции
"Hello, world!"
}

Еще несколько примеров

fn greeting<'a>() -> &'a str {
"Hi!"
}

fn fullname<'a>(fname: &'a str, lname: &'a str) -> String {
format!("{} {}", fname, lname)
}

struct Person<'a> {
fname: &'a str,
lname: &'a str
}
impl<'a> Person<'a> {
fn new(fname: &'a str, lname: &'a str) -> Person<'a> { // `<'a>` после `new` можно опустить, поскольку оно есть у `impl`
Person {
fname : fname,
lname : lname
}
}

fn fullname(&self) -> String {
format!("{} {}", self.fname , self.lname)
}
}

fn main() {
let player = Person::new("Serena", "Williams");
let player_fullname = player.fullname();

println!("Player: {}", player_fullname);
}

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

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

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

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

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