Rust на примерах

translated

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

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

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

Привет, мир

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

// Эта строка — комментарий, она будет проигнорирована компилятором
// Протестировать код можно нажав на кнопку "Run",
// которая находится в правом верхнем углу,
// или же можно использовать клавиатуру, нажав сочетание клавиш "Ctrl + Enter"

// Этот код можно редактировать не стесняясь, дерзайте!
// Всегда можно вернуть оригинальный код, нажав на кнопку "Reset". Она также находится в правом верхнем углу, но левее

// Это главная функция. С неё начинается исполнение любой программы
fn main() {
    // Следующий код будет исполнен в момент, когда будет запущен исполняемый файл

    // Напечатаем текст в консоли
    println!("Привет, мир!");
}

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

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

$ rustc hello.rs

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

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

Задание

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

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

Комментарии

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

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

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

    • /// Генерация документации для функции.
    • //! Генерация документации для модуля.
fn main() {
    // This is an example of a line comment
    // There are two slashes at the beginning of the line
    // And nothing written inside these will be read by the compiler

    // println!("Hello, world!");

    // Run it. See? Now try deleting the two slashes, and run it again.

    /*
     * This is another type of comment, a block comment. In general,
     * line comments are the recommended comment style. But
     * block comments are extremely useful for temporarily disabling
     * chunks of code. /* Block comments can be /* nested, */ */
     * so it takes only a few keystrokes to comment out everything
     * in this main() function. /*/*/* Try it yourself! */*/*/
     */

    /*
    Note: The previous column of `*` was entirely for style. There's
    no actual need for it.
    */

    // You can manipulate expressions more easily with block comments
    // than with line comments. Try deleting the comment delimiters
    // to change the result:
    let x = 5 + /* 90 + */ 5;
    println!("Is `x` 10 or 100? x = {}", x);
}

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

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

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

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

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

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

fn main() {
    // `{}` автоматически будет заменено на
    // аргументы. Они будут преобразованы в строку.
    println!("{} дней", 31);

    // Без суффиксов, 31 является i32. Можно изменить тип 31,
    // используя суффикс.

    // Существует множество способов работы с форматированным выводом. Можно указать
    // позицию для каждого аргумента.
    println!("{0}, это {1}. {1}, это {0}", "Алиса", "Боб");

    // Так же можно именовать аргументы.
    println!("{subject} {verb} {object}",
             object="ленивую собаку",
             subject="быстрая коричневая лиса",
             verb="прыгает через");

    println!("{} из {:b} людей знают, что такое двоичный код, а остальные нет.", 1, 2);

    // Можно выравнивать текст, сдвигая его на указанную ширину.
    // Данный макрос отобразит в консоли
    // "     1". 5 пробелов и "1".
    println!("{number:>width$}", number=1, width=6);

    // Можно добавить к цифрам пару нулей. Данный макрос выведет "000001".
    println!("{number:0>width$}", number=1, width=6);

    // Компилятор обязательно проверит, что в макрос передано правильное количество
    // аргументов.
    println!("Меня зовут {0}, {1} {0}", "Бонд");
    // ИСПРАВЬТЕ ^ Добавьте недостающий аргумент: "Джеймс"

    // Создаём структуру, которая хранит в себе `i32`. Назовём её `Structure`.
    #[allow(dead_code)]
    struct Structure(i32);

    // Однако, пользовательские типы данных, например, как эта структура
    // требуют более сложной обработки для вывода. Данный код не будет работать.
    println!("Эта структура `{}` не хочет выводится на экран...", Structure(3));
    // ИСПРАВЬТЕ ^ Закомментируйте эту строку.
}

std::fmt содержит в себе много типажей, которые управляют отображением текста. Базовая форма двух самых важных рассмотрена ниже:

  • fmt::Debug: Использует маркер {:?}. Форматирует текст для отладочных целей.
  • fmt::Display: Использует маркер {}. Форматирует текст в более элегантном,удобном для пользователя стиле.

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

Реализация типажа fmt::Display автоматически предоставляет реализацию типажа ToString, который позволяет нам конвертировать наш тип в String.

Задания

  • Исправьте две ошибки в коде выше (смотрите ИСПРАВЬТЕ), чтобы код компилировался без ошибок
  • Добавьте макрос println!, который выводит: Pi is roughly 3.142 с помощью управления количеством знаков после запятой. Для выполнения данного задания создайте переменную, которая будет хранить в себе значение числа Пи: let pi = 3.141592. (Подсказка: вам необходимо ознакомиться с документацией по std::fmt, чтобы узнать, как отобразить в консоли только часть знаков после запятой).

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

std::fmt, макросы, struct, и trait

Debug

Все типы, которые будут использовать типажи форматирования std::fmt, требуют их реализации для возможности печати. Автоматическая реализация предоставлена только для типов из стандартной библиотеки (std). Все остальные типы должны иметь собственную реализацию.

C помощью типажа fmt::Debug это сделать очень просто. Все типы могут выводить (автоматически создавать, derive) реализацию fmt::Debug. Сделать подобное с fmt::Display невозможно, он должен быть реализован вручную.


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

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

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

// Вывод и реализация `fmt::Debug` для `Structure`.
// `Structure` - это структура, которая содержит в себе один `i32`.
#[derive(Debug)]
struct Structure(i32);

// Добавим структуру `Structure` в структуру `Deep`.
// Реализуем для `Deep` возможность вывода с помощью fmt::Debug`.
#[derive(Debug)]
struct Deep(Structure);

fn main() {
    // Вывод с помощью `{:?}` аналогичен `{}`.
    println!("{:?} месяцев в году.", 12);
    println!("{1:?} {0:?} - это имя {actor:?}.",
             "Слэйтер",
             "Кристиан",
             actor="актёра");

    // `Structure` теперь можно напечатать!
    println!("Теперь {:?} будет выведена на экран!", Structure(3));

    // Проблема с `выводом (derive)`, в том, что у нас не будет контроля
    // над тем, как будет выглядеть результат.
    // Что, если мы хотим напечатать просто `7`?
    println!("А теперь напечатаем {:?}", Deep(Structure(7)));
}

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

#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
    age: u8
}

fn main() {
    let name = "Peter";
    let age = 27;
    let peter = Person { name, age };

    // Pretty print
    println!("{:#?}", peter);
}

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

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

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

Display

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


#![allow(unused)]
fn main() {
// Импортируем (с помощью `use`) модуль `fmt`, чтобы мы могли его использовать.
use std::fmt;

// Определяем структуру, для которой будет реализован `fmt::Display`.
// Это простая кортежная структура c именем `Structure`, которая хранит в себе `i32`.
struct Structure(i32);

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

Вывод fmt::Display может быть более чистым, чем fmt::Debug, но может быть проблемой для стандартной библиотеки (std). Как нестандартные типы должны отображаться? Например, если std предоставляет единый стиль вывода для Vec<T>, каким этот вывод должен быть? Любой из этих двух?

  • Vec<path>: /:/etc:/home/username:/bin (разделитель :)
  • Vec<number>: 1,2,3 (разделитель ,)

Нет, потому что не существует идеального стиля вывода для всех типов, поэтому std не может его предоставить. fmt::Display не реализован для Vec<T> или для других обобщённых контейнеров. Для этих случаев подойдёт fmt::Debug.

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

use std::fmt; // Импортируем `fmt`

// Структура, которая хранит в себе два числа.
// Вывод типажа `Debug` добавлен для сравнения с `Display`.
#[derive(Debug)]
struct MinMax(i64, i64);

// Реализуем `Display` для `MinMax`.
impl fmt::Display for MinMax {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Используем `self.номер`, чтобы получить доступ к каждому полю структуры.
        write!(f, "({}, {})", self.0, self.1)
    }
}

// Объявим структуру с именованными полями, для сравнения
#[derive(Debug)]
struct Point2D {
    x: f64,
    y: f64,
}

// По аналогии, реализуем `Display` для Point2D
impl fmt::Display for Point2D {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Обращаться к полям структуры Point2D будет по имени
        write!(f, "x: {}, y: {}", self.x, self.y)
    }
}

fn main() {
    let minmax = MinMax(0, 14);

    println!("Сравниваем структуры:");
    println!("Display: {}", minmax);
    println!("Debug: {:?}", minmax);

    let big_range =   MinMax(-300, 300);
    let small_range = MinMax(-3, 3);

    println!("Большой диапазон - {big} и маленький диапазон {small}",
             small = small_range,
             big = big_range);

    let point = Point2D { x: 3.3, y: 7.2 };

    println!("Сравниваем точки:");
    println!("Display: {}", point);
    println!("Debug: {:?}", point);

    // Ошибка. Типажи `Debug` и `Display` были реализованы, но `{:b}`
    // необходима реализация `fmt::Binary`. Следующий код не сработает.
    // println!("Как выглядит Point2D в виде двоичного кода: {:b}?", point);
}

Итак, fmt::Display был реализован, но fmt::Binary нет, следовательно не может быть использован. std::fmt имеет много таких типажей и каждый из них требует свою реализацию. Это более подробно описано в документации к std::fmt.

Задание

После того, как запустите код, представленный выше, используйте структуру Point2D как пример и добавьте новую структуру Complex, чтобы вывод был таким:

Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }

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

derive, std::fmt, макросы, struct, trait и use

Пример: список

Реализовать fmt::Display для структуры, в которой каждый элемент должен обрабатываться последовательно, не так-то просто. Проблема в том, что write! каждый раз возвращает fmt::Result. Для правильного обращения с этим необходимо обрабатывать все результаты. Для этой цели Rust предоставляет оператор ?.

Использование ? для write! выглядит следующим образом:

// Попробуй исполнить `write!`, чтобы узнать, вернется ли ошибка. Если будет ошибка — верни её.
// Если нет, то продолжи.
write!(f, "{}", value)?;

С помощью оператора ? реализация fmt::Display для Vec довольно простая:

use std::fmt; // Импортируем модуль `fmt`.

// Определяем структуру с именем `List`, которая хранит в себе `Vec`.
struct List(Vec<i32>);

impl fmt::Display for List {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Получаем значение с помощью индекса кортежа
        // и создаём ссылку на `vec`.
        let vec = &self.0;

        write!(f, "[")?;

        // Пройдёмся по каждому `v` в `vec`.
        // Номер итерации хранится в `count`.
        for (count, v) in vec.iter().enumerate() {
            // Для каждого элемента, кроме первого, добавим запятую
            // до вызова `write!`. Используем оператор `?` или `try!`,
            // чтобы вернуться при наличии ошибок.
            if count != 0 { write!(f, ", ")?; }
            write!(f, "{}", v)?;
        }

        // Закроем открытую скобку и вернём значение `fmt::Result`
        write!(f, "]")
    }
}

fn main() {
    let v = List(vec![1, 2, 3]);
    println!("{}", v);
}

Задание

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

[0: 1, 1: 2, 2: 3]

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

for, ref, Result, struct, ?, и vec!

Форматирование

Мы видели, что форматирование задаётся макросом форматирования:

  • format!("{}", foo) -> "3735928559"
  • format!("0x{:X}", foo) ->"0xDEADBEEF"
  • format!("0o{:o}", foo) -> "0o33653337357"

Одна и та же переменная (foo) может быть отображена по разному в зависимости от используемого типа аргумента: X, o или неопределённый.

Функционал форматирования реализован благодаря типажу, и для каждого типа аргумента существует свой. Наиболее распространённый типаж для форматирования — Display, который работает без аргументов: например {}.

use std::fmt::{self, Formatter, Display};

struct City {
    name: &'static str,
    // Широта
    lat: f32,
    // Долгота
    lon: f32,
}

impl Display for City {
    // `f` — это буфер, данный метод должен записать в него форматированную строку
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' };
        let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' };

        // `write!` похож на `format!`, только он запишет форматированную строку
        // в буфер (первый аргумент функции)
        write!(f, "{}: {:.3}°{} {:.3}°{}",
               self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c)
    }
}

#[derive(Debug)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

fn main() {
    for city in [
        City { name: "Дублин", lat: 53.347778, lon: -6.259722 },
        City { name: "Осло", lat: 59.95, lon: 10.75 },
        City { name: "Ванкувер", lat: 49.25, lon: -123.1 },
    ].iter() {
        println!("{}", *city);
    }
    for color in [
        Color { red: 128, green: 255, blue: 90 },
        Color { red: 0, green: 3, blue: 254 },
        Color { red: 0, green: 0, blue: 0 },
    ].iter() {
        // Поменяйте {:?} на {}, когда добавите реализацию
        // типажа fmt::Display
        println!("{:?}", *color)
    }
}

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

Задание

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

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

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

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

std::fmt

Примитивы

Rust предоставляет доступ к большому количеству примитивов:

Скалярные типы

  • знаковые целочисленные: i8, i16, i32, i64 и isize (размер указателя)
  • беззнаковые целочисленные: u8, u16, u32, u64 и usize (размер указателя)
  • вещественные: f32, f64
  • char скалярное значение Unicode, например: 'a', 'α' и '∞' (4 байта каждый)
  • bool: true или false
  • единичный тип (), значение которого так же ()

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

Составные типы

  • массивы, например [1, 2, 3]
  • кортежи, например (1, true)

Переменные всегда должны быть аннотированы. Числам можно указать определённый тип с помощью суффикса, иначе будет присвоен тип по умолчанию. Целочисленные значения по умолчанию i32, а вещественные f64. Стоит заметить, что Rust также умеет выводить типы из контекста.

fn main() {
    // Переменные могут быть аннотированы.
    let logical: bool = true;

    let a_float: f64 = 1.0;  // Обычная аннотация
    let an_integer   = 5i32; // Суффиксная аннотация

    // Этим переменным будет присвоен тип по умолчанию.
    let default_float   = 3.0; // `f64`
    let default_integer = 7;   // `i32`
    
    // Тип также может быть выведен из контекста.
    let mut inferred_type = 12; // Тип i64 выводится из другой строки
    inferred_type = 4294967296i64;
    
    // Значение изменяемой переменной может быть изменено.
    let mut mutable = 12; // Изменяемое `i32`
    mutable = 21;
    
    // Ошибка! Тип переменной изменить нельзя.
    mutable = true;
    
    // Переменные могут быть переопределены с помощью затенения.
    let mutable = true;
}

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

стандартная библиотека (std), mut, вывод типов и затенение

Литералы и операторы

Целочисленное 1, вещественное 1.2, символ 'a', строка "abc", логическое true и единичный тип () могут быть выражены с помощью литералов.

Целочисленные значения так же могут быть выражены с помощью шестнадцатеричного, восьмеричного или двоичного обозначения используя соответствующие префиксы: 0x, 0o или 0b.

Для улучшения читаемости числовых литералов можно использовать подчёркивания, например 1_000 тоже самое, что и 1000, и 0.000_001 равно 0.000001.

Нам необходимо указать компилятору какой тип для литерала мы используем. Сейчас мы используем суффикс u32, чтобы указать, что литерал - беззнаковое целое число 32-х бит и суффикс i32 - знаковое целое 32-х битное число.

Доступные операторы и их приоритет в Rust такой же как и в других C-подобных языках.

fn main() {
    // Целочисленное сложение
    println!("1 + 2 = {}", 1u32 + 2);

    // Целочисленное вычитание
    println!("1 - 2 = {}", 1i32 - 2);
    // ЗАДАНИЕ ^ Попробуйте изменить `1i32` на `1u32`
    // чтобы убедится насколько важен тип данных

    // Булева логика
    println!("true И false будет {}", true && false);
    println!("true ИЛИ false будет {}", true || false);
    println!("НЕ true будет {}", !true);

    // Побитовые операции
    println!("0011 И 0101 будет {:04b}", 0b0011u32 & 0b0101);
    println!("0011 ИЛИ 0101 будет {:04b}", 0b0011u32 | 0b0101);
    println!("0011 исключающее ИЛИ 0101 будет {:04b}", 0b0011u32 ^ 0b0101);
    println!("1 << 5 будет {}", 1u32 << 5);
    println!("0x80 >> 2 будет 0x{:x}", 0x80u32 >> 2);

    // Использование подчёркивания для улучшения читаемости!
    println!("Один миллион записан как {}", 1_000_000u32);
}

Кортежи

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

// Кортежи могут быть использованы как аргументы функции
// и как возвращаемые значения
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // `let` можно использовать для создания связи между кортежем и переменной
    let (integer, boolean) = pair;

    (boolean, integer)
}

// Это структура используется для задания
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);

fn main() {
    // Кортеж с множеством различных типов данных
    let long_tuple = (1u8, 2u16, 3u32, 4u64,
                      -1i8, -2i16, -3i32, -4i64,
                      0.1f32, 0.2f64,
                      'a', true);

    // К значениям переменных внутри кортежа можно обратиться по индексу
    println!("первое значение длинного кортежа: {}", long_tuple.0);
    println!("второе значение длинного кортежа: {}", long_tuple.1);

    // Кортежи могут содержать в себе кортежи
    let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);

    // Кортежи можно напечатать
    println!("кортеж из кортежей: {:?}", tuple_of_tuples);
    
    // Но длинные Кортежи не могут быть напечатаны
    // let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
    // println!("слишком длинный кортеж: {:?}", too_long_tuple);
    // TODO ^ Раскомментируйте выше 2 строки, чтобы увидеть ошибку компилятора

    let pair = (1, true);
    println!("pair хранит в себе {:?}", pair);

    println!("перевёрнутая pair будет {:?}", reverse(pair));

    // Для создания кортежа, содержащего один элемент, необходимо написать элемент и
    // поставить запятую внутри круглых скобок.
    println!("кортеж из одного элемента: {:?}", (5u32,));
    println!("просто целочисленное значение: {:?}", (5u32));

    // Кортежи можно разобрать на части (деструктурировать) для создания связи
    let tuple = (1, "привет", 4.5, true);

    let (a, b, c, d) = tuple;
    println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);

    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);

}

Задание

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

    ( 1.1 1.2 )
    ( 2.1 2.2 )
    

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

  2. Добавьте функцию transpose, используя функцию reverse, как пример, которая принимает матрицу, как аргумент и возвращает матрицу, в которой два элемента поменялись местами. Например:

    println!("Matrix:\n{}", matrix);
    println!("Transpose:\n{}", transpose(matrix));
    

    Результат:

    Matrix:
    ( 1.1 1.2 )
    ( 2.1 2.2 )
    Transpose:
    ( 1.1 2.1 )
    ( 1.2 2.2 )
    

Массивы и срезы

Массив — это коллекция объектов одинакового типа T, расположенных в памяти непосредственно друг за другом. Массивы создаются с помощью квадратных скобок [], а их размер должен быть известен во время компиляции и является частью сигнатуры типа [T; size].

Срезы похожи на массивы, но их размер неизвестен в момент компиляции программы. Срезы представляют собой объекты, состоящие из указателя на данные и размер среза. Размер среза равен размеру usize и зависит от архитектуры процессора: например, для x86-64 он равен 64 битам. Срезы могут быть использованы для заимствования части массива и будут иметь сигнатуру типа &[T].

use std::mem;

// Эта функция заимствует срез
fn analyze_slice(slice: &[i32]) {
    println!("первый элемент среза: {}", slice[0]);
    println!("в срезе {} элементов", slice.len());
}

fn main() {
    // Массив фиксированного размера (указывать сигнатуру типа необязательно)
    let xs: [i32; 5] = [1, 2, 3, 4, 5];

    // Все элементы могут быть инициализированы одним и тем же значением
    let ys: [i32; 500] = [0; 500];

    // Индекс начинается с 0
    println!("первый элемент массива: {}", xs[0]);
    println!("второй элемент массива: {}", xs[1]);

    // `len` возвращает длину массива
    println!("размер массива: {}", xs.len());

    // Память для массивов выделяется в стеке
    println!("массив занимает {} байт", mem::size_of_val(&xs));

    // Массивы могут быть автоматически заимствованы как срез
    println!("заимствуем весь массив, используя срез");
    analyze_slice(&xs);

    // Срезы могут указывать на часть массива
    // Они имеют форму [starting_index..ending_index]
    // starting_index — это первая позиция в срезе
    // ending_index — на 1 больше, чем последняя позиция в срезе
    println!("заимствуем часть массива как срез");
    analyze_slice(&ys[1 .. 4]);

    // Выход за границу массива заставит компилятор паниковать.
    // Не надо так.
    println!("{}", xs[5]);
}

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

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

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

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

Структуры

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

  • Кортежная структура, которая на самом деле является именованным кортежем.
  • Классическая C-структура
  • Единичная структура, которая не имеет полей, но может быть полезна для обобщённых типов.
#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}

// unit-структура
struct Unit;

// Кортежная структура
struct Pair(i32, f32);

// Структура с двумя полями
struct Point {
    x: f32,
    y: f32,
}

// Структуры могут быть использованы в качестве полей другой структуры
#[allow(dead_code)]
struct Rectangle {
    // Прямоугольник может быть определён по расположению в пространстве
    // его верхнего левого и нижнего правого углов
    top_left: Point,
    bottom_right: Point,
}

fn main() {
    // Создадим структуру при помощи сокращённой инициализации полей
    let name = String::from("Peter");
    let age = 27;
    let peter = Person { name, age };

    // Распечатаем отладочную информацию о структуре
    println!("{:?}", peter);


    // Инициализируем `Point`
    let point: Point = Point { x: 10.3, y: 0.4 };

    // Получаем доступ к полям структуры
    println!("координаты точки: ({}, {})", point.x, point.y);

    // Создадим новую точку, используя синтаксис обновления структуры и нашу
    // существующую точку
    let bottom_right = Point { x: 5.2, ..point };

    // `bottom_right.y` будет тем же самым, что и `point.y`, так как мы взяли
    // это поле из `point`
    println!("вторая точка: ({}, {})", bottom_right.x, bottom_right.y);

    // Деструктурируем структуру при помощи `let`
    let Point { x: left_edge, y: top_edge } = point;

    let _rectangle = Rectangle {
        // создание структуры также является выражением
        top_left: Point { x: left_edge, y: top_edge },
        bottom_right: bottom_right,
    };

    // Создадим unit-структуру
    let _unit = Unit;

    // Создадим кортежную структуру
    let pair = Pair(1, 0.1);

    // Доступ к полям кортежной структуры
    println!("pair содержит {:?} и {:?}", pair.0, pair.1);

    // Деструктурируем кортежную структуру
    let Pair(integer, decimal) = pair;

    println!("pair содержит {:?} и {:?}", integer, decimal);
}

Задание

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

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

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

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

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

// Создаём `enum` для классификации web-событий. Обратите внимание,
// как имена и информация о типе определяют вариант:
// `PageLoad != PageUnload` и `KeyPress(char) != Paste(String)`.
// Все они разные и независимые.
enum WebEvent {
    // `enum` может быть как `unit-подобным`,
    PageLoad,
    PageUnload,
    // так и кортежной структурой,
    KeyPress(char),
    Paste(String),
    // или С-подобной структурой.
    Click { x: i64, y: i64 },
}

// Функция, которая получает на вход `WebEvent` и ничего не возвращает.
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("страница загружена"),
        WebEvent::PageUnload => println!("страница выгружена"),
        // Извлечём `c` из `enum`.
        WebEvent::KeyPress(c) => println!("нажата '{}'.", c),
        WebEvent::Paste(s) => println!("нажата \"{}\".", s),
        // Разберём `Click` на `x` и `y`.
        WebEvent::Click { x, y } => {
            println!("кликнуто на x={}, y={}.", x, y);
        },
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('x');
    // `to_owned()` создаст `String` из строкового среза.
    let pasted  = WebEvent::Paste("мой текст".to_owned());
    let click   = WebEvent::Click { x: 20, y: 80 };
    let load    = WebEvent::PageLoad;
    let unload  = WebEvent::PageUnload;

    inspect(pressed);
    inspect(pasted);
    inspect(click);
    inspect(load);
    inspect(unload);
}

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

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

enum VeryVerboseEnumOfThingsToDoWithNumbers {
    Add,
    Subtract,
}

// Создаётся псевдоним типа
type Operations = VeryVerboseEnumOfThingsToDoWithNumbers;

fn main() {
    // Мы можем обратиться к каждому варианту перечисления через его
    // псевдоним, а не через его длинное неудобное имя.
    let x = Operations::Add;
}

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

enum VeryVerboseEnumOfThingsToDoWithNumbers {
    Add,
    Subtract,
}

impl VeryVerboseEnumOfThingsToDoWithNumbers {
    fn run(&self, x: i32, y: i32) -> i32 {
        match self {
            Self::Add => x + y,
            Self::Subtract => x - y,
        }
    }
}

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

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

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

Декларация use

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

// Атрибут, который убирает предупреждения компилятора
// о неиспользуемом коде
#![allow(dead_code)]

enum Status {
    Rich,
    Poor,
}

enum Work {
    Civilian,
    Soldier,
}

fn main() {
    // Используем `use` для каждого из вариантов, чтобы они были доступны
    // без указания области видимости.
    use Status::{Poor, Rich};
    // Автоматически используем `use` для каждого из вариантов в `Work`.
    use Work::*;

    // Эквивалентно `Status::Poor`.
    let status = Poor;
    // Эквивалентно `Work::Civilian`.
    let work = Civilian;

    match status {
        // Обратите внимание, как используются варианты из перечисления `Status`
        // благодаря `use`
        Rich => println!("У богатого куча денег!"),
        Poor => println!("У бедняка денег нет, но он держится..."),
    }

    match work {
        // И снова используем варианты напрямую.
        Civilian => println!("Гражданин работает!"),
        Soldier  => println!("Солдаты служат!"),
    }
}

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

match и use

С-подобные

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

// Атрибут, который убирает предупреждения компилятора
// о неиспользуемом коде
#![allow(dead_code)]

// enum с неявным дискриминатором (начинается с 0)
enum Number {
    Zero,
    One,
    Two,
}

// enum с явным дискриминатором
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

fn main() {
    // `enums` может быть преобразован в целочисленное значение.
    println!("нулевой элемент {}", Number::Zero as i32);
    println!("первый элемент {}", Number::One as i32);

    println!("красный цвет #{:06x}", Color::Red as i32);
    println!("голубой цвет #{:06x}", Color::Blue as i32);
}

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

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

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

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

use List::*;

enum List {
    // Cons: Кортежная структура, которая хранит элемент
    // и указатель на следующий узел
    Cons(u32, Box<List>),
    // Nil: Узел, обозначающий конец связанного списка
    Nil,
}

// Методы могут быть присоединены к перечислению
impl List {
    // Создаём пустой список
    fn new() -> List {
        // `Nil` имеет тип `List`
        Nil
    }

    // Функция, которая принимает список и возвращает тот же список,
    // но с новым элементом в начале
    fn prepend(self, elem: u32) -> List {
        // `Cons` также имеет тип `List`
        Cons(elem, Box::new(self))
    }

    // Возвращаем длину списка
    fn len(&self) -> u32 {
        // `self` должен быть сопоставлен (проверен на соответствие),
        // поскольку поведение этого метода зависит от варианта `self`
        // `self` имеет тип `&List`, а `*self` имеет тип `List`, сопоставление на
        // конкретном типе `T` предпочтительнее, чем сопоставление по ссылке `&T`
        match *self {
            // Мы не можем завладеть `tail`, т.к. `self` заимствован;
            // вместо этого возьмём ссылку на `tail`
            Cons(_, ref tail) => 1 + tail.len(),
            // Базовый случай: Пустой список имеет нулевую длину
            Nil => 0
        }
    }

    // Возвращаем представление списка в виде (размещённой в куче) строки
    fn stringify(&self) -> String {
        match *self {
            Cons(head, ref tail) => {
                // `format!` похож на `print!`, но возвращает строку
                // размещённую в куче, вместо вывода на консоль
                format!("{}, {}", head, tail.stringify())
            },
            Nil => {
                format!("Nil")
            },
        }
    }
}

fn main() {
    // Создаём пустой связанный список
    let mut list = List::new();

    // Присоединяем несколько элементов
    list = list.prepend(1);
    list = list.prepend(2);
    list = list.prepend(3);

    // Отображаем окончательное состояние списка
    println!("размер связанного списка: {}", list.len());
    println!("{}", list.stringify());
}

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

Box и методы

Константы

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

  • const: Неизменяемая переменная (в общем случае).
  • static: Возможно, изменяемая переменная с временем жизни 'static. Статическое время жизни подразумевается и может не быть указано явно. Доступ или модификация изменяемой статической переменной — небезопасны (см. unsafe).
// Константы объявлены в глобальной области видимости.
static LANGUAGE: &str = "Rust";
const THRESHOLD: i32 = 10;

fn is_big(n: i32) -> bool {
    // Получаем доступ к константе внутри функции
    n > THRESHOLD
}

fn main() {
    let n = 16;

    // Получаем доступ к константе внутри функции main
    println!("Это язык {}", LANGUAGE);
    println!("Установим предел, равный {}", THRESHOLD);
    println!("Число {} {} предела", n, if is_big(n) { "больше" } else { "меньше" });

    // Ошибка! `const` нельзя изменить.
    THRESHOLD = 5;
    // ИСПРАВЬТЕ ^ Закомментируйте эту строчку
}

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

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

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

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

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

fn main() {
    let an_integer = 1u32;
    let a_boolean = true;
    let unit = ();

    // Копируем `an_integer` в `copied_integer`
    let copied_integer = an_integer;

    println!("An integer: {:?}", copied_integer);
    println!("A boolean: {:?}", a_boolean);
    println!("Meet the unit value: {:?}", unit);

    // Компилятор предупреждает о неиспользуемых переменных; эти предупреждения
    // можно скрыть, поставив знак подчёркивания в начало имени переменной
    let _unused_variable = 3u32;

    let noisy_unused_variable = 2u32;
    // FIXME ^ Поставьте знак подчёркивания в начало имени, чтобы убрать предупреждение
    // Обратите внимание, что предупреждения могут не отображаться в браузере
}

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

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

fn main() {
    let _immutable_binding = 1;
    let mut mutable_binding = 1;

    println!("Перед изменением: {}", mutable_binding);

    // Ok
    mutable_binding += 1;

    println!("После изменения: {}", mutable_binding);

    // Ошибка!
    _immutable_binding += 1;
    // ИСПРАВЬТЕ ^ Закомментируйте эту строку
}

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

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

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

fn main() {
    // Эта переменная живёт в функции main
    let long_lived_binding = 1;

    // Это блок, он имеет меньшую область видимости, чем функция main
    {
        // Эта переменная существует только в этом блоке
        let short_lived_binding = 2;

        println!("inner short: {}", short_lived_binding);
    }
    // Конец блока

    // Ошибка! `short_lived_binding` нет в этой области видимости
    println!("outer short: {}", short_lived_binding);
    // ИСПРАВЬТЕ ^ Закомментируйте строку

    println!("outer long: {}", long_lived_binding);
}

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

fn main() {
    let shadowed_binding = 1;

    {
        println!("До затенения: {}", shadowed_binding);

        // Эта переменная *затеняет* внешнюю
        let shadowed_binding = "abc";

        println!("затенённая во внутреннем блоке: {}", shadowed_binding);
    }
    println!("во внешнем блоке: {}", shadowed_binding);

    // Эта привязка *затеняет* предыдущую
    let shadowed_binding = 2;
    println!("затенённая во внешнем блоке: {}", shadowed_binding);
}

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

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

fn main() {
    // Объявляем связь с переменной
    let a_binding;

    {
        let x = 2;

        // Инициализируем связь
        a_binding = x * x;
    }

    println!("связь а: {}", a_binding);

    let another_binding;

    // Ошибка! Использование неинициализированной связи с переменной
    println!("другая связь: {}", another_binding);
    // ИСПРАВЬТЕ ^ Закомментируйте строку

    another_binding = 1;

    println!("другая связь: {}", another_binding);
}

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

Заморозка

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

fn main() {
    let mut _mutable_integer = 7i32;

    {
        // Неизменяемое затенение `_mutable_integer`
        let _mutable_integer = _mutable_integer;

        // Ошибка! `_mutable_integer` заморожена в этой области
        _mutable_integer = 50;
        // ИСПРАВЬТЕ ^ Закомментируйте эту строку

        // `_mutable_integer` выходит из области видимости
    }

    // Ok! `_mutable_integer` не заморожена в этой области
    _mutable_integer = 3;
}

Типы

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

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

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

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

// Строчка ниже убирает все предупреждения,
// которые вызываются переполнением при преобразовании типов.
#![allow(overflowing_literals)]

fn main() {
    let decimal = 65.4321_f32;

    // Ошибка! Нет неявного преобразования
    let integer: u8 = decimal;
    // ИСПРАВЬТЕ ^ Закомментируйте данную строку

    // Явное преобразование
    let integer = decimal as u8;
    let character = integer as char;

    // Ошибка! Здесь ограничение в правилах конвертации.
    // Число с плавающей точкой не может быть напрямую конвертировано в символ.
    let character = decimal as char;
    // ИСПРАВЬТЕ ^ Закомментируйте данную строку

    println!("Преобразование: {} -> {} -> {}", decimal, integer, character);

    // Когда преобразовывается любое значение в беззнаковый тип T
    // std::T::MAX + 1 добавляется или вычитается до тех пор, пока значение
    // не будет помещаться в новый тип.

    // 1000 поместится в u16
    println!("1000 as u16: {}", 1000 as u16);

    // 1000 - 256 - 256 - 256 = 232
    // Подробнее. Первые 8 младших битов (LSB) сохраняются,
    // а старшие биты (MSB) будут усечены.
    println!("1000 as u8: {}", 1000 as u8);
    // -1 + 256 = 255
    println!("  -1 as u8: {}", (-1i8) as u8);

    // Для положительных чисел результатом будет остаток от деления
    println!("1000 mod 256: {}", 1000 % 256);

    // Когда значение преобразовывается в знаковый тип,
    // побитовый результат будет таким же, как и
    // первое преобразование к соответствующему типу без знака. Если старший бит этого значения
    // равен 1, то это значение — отрицательное.

    // За исключением случая, когда значение умещается в тип.
    println!(" 128 as i16: {}", 128 as i16);
    // 128 as u8 -> 128, дополнительный код которого в 8 битах:
    println!(" 128 as i8: {}", 128 as i8);

    // повторяем примеры
    // 1000 as u8 -> 232
    println!("1000 as u8: {}", 1000 as u8);
    // и дополнительный код 232 — это -24
    println!(" 232 as i8: {}", 232 as i8);
    
    // Начиная с Rust 1.45, ключевое слово `as` выполняет *насыщающее приведение* (saturating cast)
    // при преобразовании в целое число с плавающей точкой.
    // Если значение числа с плавающей точкой превышает верхнюю границу
    // или меньше нижней границы, то возвращаемое значение
    // будет равняться пересечённой границе.
    
    // 300.0 == 255
    println!("300.0 == {}", 300.0_f32 as u8);
    // -100.0 as u8 == 0
    println!("-100.0 as u8 == {}", -100.0_f32 as u8);
    // nan as u8 is 0
    println!("nan as u8 == {}", f32::NAN as u8);
    
    // Это поведение требует небольших затрат во время работы программы,
    // и его можно избежать при помощи unsafe-методов, однако результат
    // может пересечь границы, и мы получим ненадёжное значение.
    // Используйте эти методы с умом:
    unsafe {
        // 300.0 == 44
        println!("300.0 == {}", 300.0_f32.to_int_unchecked::<u8>());
        // -100.0 as u8 == 156
        println!("-100.0 as u8 == {}", (-100.0_f32).to_int_unchecked::<u8>());
        // nan as u8 == 0
        println!("nan as u8 == {}", f32::NAN.to_int_unchecked::<u8>());
    }
}

Литералы

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

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

fn main() {
    // Литералы с суффиксами. Их тип известен при инициализации.
    let x = 1u8;
    let y = 2u32;
    let z = 3f32;

    // Литералы без суффиксов. Их тип будет зависеть от того, как их используют.
    let i = 1;
    let f = 1.0;

    // `size_of_val` возвращает размер переменной в байтах
    println!("size of `x` in bytes: {}", std::mem::size_of_val(&x));
    println!("size of `y` in bytes: {}", std::mem::size_of_val(&y));
    println!("size of `z` in bytes: {}", std::mem::size_of_val(&z));
    println!("size of `i` in bytes: {}", std::mem::size_of_val(&i));
    println!("size of `f` in bytes: {}", std::mem::size_of_val(&f));
}

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

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

Вывод типов

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

fn main() {
    // Благодаря выведению типов компилятор знает, что `elem` имеет тип `u8`.
    let elem = 5u8;

    // Создадим пустой вектор (расширяемый массив).
    let mut vec = Vec::new();
    // В данном месте компилятор не знает точный тип `vec`, он лишь знает,
    // что это вектор каких-то значений (`Vec<_>`).

    // Добавляем `elem` в вектор.
    vec.push(elem);
    // Ага! Теперь компилятор знает, что `vec` — это вектор, который хранит в себе тип `u8`
    // (`Vec<u8>`)
    // ЗАДАНИЕ ^ Попробуйте закомментировать строку `vec.push(elem)`

    println!("{:?}", vec);
}

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

Псевдонимы

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

// `NanoSecond` это новое имя для `u64`.
type NanoSecond = u64;
type Inch = u64;

// Используйте этот атрибут, чтобы не выводить предупреждение
// об именах не в стиле CamelCase
#[allow(non_camel_case_types)]
type u64_t = u64;
// ЗАДАНИЕ ^ Попробуйте удалить атрибут

fn main() {
    // `NanoSecond` = `Inch` = `u64_t` = `u64`.
    let nanoseconds: NanoSecond = 5 as u64_t;
    let inches: Inch = 2 as u64_t;

    // Обратите внимание, что псевдонимы *не предоставляют* никакой
    // дополнительной безопасности типов, так как *не являются* новыми типами
    println!("{} nanoseconds + {} inches = {} unit?",
             nanoseconds,
             inches,
             nanoseconds + inches);
}

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

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

Атрибуты

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

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

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

From и Into

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

From

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

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


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

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

use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("Мой номер {:?}", num);
}

Into

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

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

use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let int = 5;
    // Попробуйте убрать аннотацию типа
    let num: Number = int.into();
    println!("Мой номер {:?}", num);
}

TryFrom и TryInto

Как и From и Into, TryFrom и TryInto - обобщённые типажи для конвертации между типами. Но в отличии от From/Into, типажи TryFrom/TryInto используются для преобразований с ошибками и возвращают Result.

use std::convert::TryFrom;
use std::convert::TryInto;

#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

fn main() {
    // TryFrom

    assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
    assert_eq!(EvenNumber::try_from(5), Err(()));

    // TryInto

    let result: Result<EvenNumber, ()> = 8i32.try_into();
    assert_eq!(result, Ok(EvenNumber(8)));
    let result: Result<EvenNumber, ()> = 5i32.try_into();
    assert_eq!(result, Err(()));
}

FromStr и ToString

Конвертация в строку

Преобразовать любой тип в String так же просто, как и реализовать для него типаж ToString. Вместо того, чтобы делать это напрямую, вы должны реализовать типаж fmt::Display, который автоматически предоставляет реализацию ToString, а также позволяет распечатать тип, как обсуждалось в секции print!.

use std::fmt;

struct Circle {
    radius: i32
}

impl fmt::Display for Circle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Круг радиусом {}", self.radius)
    }
}

fn main() {
    let circle = Circle { radius: 6 };
    println!("{}", circle.to_string());
}

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

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

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

fn main() {
    let parsed: i32 = "5".parse().unwrap();
    let turbo_parsed = "10".parse::<i32>().unwrap();

    let sum = parsed + turbo_parsed;
    println!("Сумма: {:?}", sum);
}

Выражения

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

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

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

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

    // оператор выражения
    x;
    x + 1;
    15;
}

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

fn main() {
    let x = 5u32;

    let y = {
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // Результат этого выражение будет присвоен переменной `y`
        x_cube + x_squared + x
    };

    let z = {
        // Т.к это выражение оканчивается на `;`, переменной `z` будет присвоен `()`
        2 * x;
    };

    println!("x равен {:?}", x);
    println!("y равен {:?}", y);
    println!("z равен {:?}", z);
}

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

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

if/else

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

fn main() {
    let n = 5;

    if n < 0 {
        print!("{} — отрицательное", n);
    } else if n > 0 {
        print!("{} — положительное", n);
    } else {
        print!("{} — нуль", n);
    }

    let big_n =
        if n < 10 && n > -10 {
            println!(", малое по модулю число, умножим его в десять раз");

            // Это выражение вернёт `i32`.
            10 * n
        } else {
            println!(", большое по модулю число, уменьшим его вдвое");

            // И это выражение вернёт `i32`.
            n / 2
            // ЗАДАНИЕ ^ Попробуйте отбросить значение, добавив точку с запятой.
        };
    //   ^ Не забудьте добавить тут точку с запятой! Все операторы `let` требуют её..

    println!("{} -> {}", n, big_n);
}

loop

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

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

fn main() {
    let mut count = 0u32;

    println!("Давайте считать до бесконечности!");

    // Бесконечный цикл
    loop {
        count += 1;

        if count == 3 {
            println!("три");

            // Пропустить оставшуюся часть цикла
            continue;
        }

        println!("{}", count);

        if count == 5 {
            println!("Всё, достаточно");

            // Выйти из цикла
            break;
        }
    }
}

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

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

#![allow(unreachable_code)]

fn main() {
    'outer: loop {
        println!("Вошли во внешний цикл");

        'inner: loop {
            println!("Вошли во внутренний цикл");

            // Это прервёт лишь внутренний цикл
            //break;

            // Это прервёт внешний цикл
            break 'outer;
        }

        println!("Эта точка не будет достигнута");
    }

    println!("Вышли из внешнего цикла");
}

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

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

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    assert_eq!(result, 20);
}

while

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

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

fn main() {
    // Переменная счётчик
    let mut n = 1;

    // Цикл while будет работать, пока `n` меньше 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Увеличиваем значение счётчика
        n += 1;
    }
}

Цикл for

for и диапазоны

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

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

fn main() {
    // `n` будет принимать значения: 1, 2, ..., 100 с каждой итерации
    for n in 1..101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

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

fn main() {
    // `n` будет принимать значения: 1, 2, ..., 100 с каждой итерации
    for n in 1..=100 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

for и итераторы

Конструкция for in может взаимодействовать с итератором разными способами. Как обсуждается далее про типаж Iterator, цикл for применяет к предоставленной коллекции метод into_iter, чтобы преобразовать её в итератор. Однако, это не единственный способ преобразования коллекции в итератор.

Каждый из методов into_iter, iter и iter_mut преобразует коллекцию в итератор по своему, предоставляя разные отображения содержащихся данных.

  • iter - эта функция заимствует каждый элемент коллекции на каждой итерации. Благодаря этому, он оставляет коллекцию нетронутой и доступной для использования после цикла.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter() {
        match name {
            &"Ferris" => println!("Программисты Rust вокруг нас!"),
            _ => println!("Привет {}", name),
        }
    }
}
  • into_iter - эта функция потребляет коллекцию так что на каждой итерации предоставляются данные. Коллекция больше не доступна для использования так как владение ею перешло в эту функцию.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        match name {
            "Ferris" => println!("Программисты Rust вокруг нас!"),
            _ => println!("Привет {}", name),
        }
    }
}
  • iter_mut - эта функция делает изменяемое заимствование каждого элемента коллекции, позволяя изменять коллекцию на месте.
fn main() {
    let mut names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter_mut() {
        *name = match name {
            &mut "Ferris" => "Программисты Rust вокруг нас!",
            _ => "Привет",
        }
    }

    println!("имена: {:?}", names);
}

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

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

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

match

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

fn main() {
    let number = 13;
    // TODO ^ Попробуйте разные значения переменной `number`

    println!("Расскажите мне о {}", number);
    match number {
        // Match a single value
        1 => println!("Один!"),
        // Match several values
        2 | 3 | 5 | 7 | 11 => println!("Это простое число"),
        // TODO ^ Попробуйте добавить 13 к списку простых чисел
        // Выбирает диапазон, включающий границы
        13..=19 => println!("от 13 до 19"),
        // Обрабатывает оставшиеся случаи
        _ => println!("Ничего особенногоl"),
        // TODO ^ Попробуйте закомментировать эту всепоглощающую ветку
    }

    let boolean = true;
    // Match ещё и выражение
    let binary = match boolean {
        // Ветви оператора match должны перечислять все возможные значения
        false => 0,
        true => 1,
        // TODO ^ Попробуйте закомментировать одну из этих ветвей
    };

    println!("{} -> {}", boolean, binary);
}

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

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

Кортежи

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

fn main() {
    let triple = (0, -2, 3);
    // TODO ^ Поэкспериментируйте со значениями `triple`

    println!("Расскажите мне о {:?}", triple);
    // Для деструктурирования можно использовать match
    match triple {
        // Деструктурируем второй и третий элементы
        (0, y, z) => println!("Первый равен `0`, `y` равен {:?}, а `z` равен {:?}", y, z),
        (1, ..)  => println!("Первый равен `1`, а остальное не важно"),
        (.., 2)  => println!("Последний равен `2`, а остальное не важно"),
        (3, .., 4)  => println!("Первый равен `3`, последний равен `4`, а остальное не важно"),
        // `..` можно использовать, чтобы игнорировать оставшуюся часть кортежа
        _      => println!("Не важно чему они равны"),
        // `_` означает, значение не будет присвоено переменной
    }
}

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

Tuples

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

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

// `allow` необходим, чтобы компилятор не выводил предупреждения,
// т.к используется только один вариант
#[allow(dead_code)]
enum Color {
    // Эти 3 перечисления определяют цвет по названию.
    Red,
    Blue,
    Green,
    // Остальные используют `u32` кортежи для идентификации цветовых моделей.
    RGB(u32, u32, u32),
    HSV(u32, u32, u32),
    HSL(u32, u32, u32),
    CMY(u32, u32, u32),
    CMYK(u32, u32, u32, u32),
}

fn main() {
    let color = Color::RGB(122, 17, 40);
    // ЗАДАНИЕ ^ Попробуйте другие значения для `color`

    println!("Какой это цвет?");
    // `enum` может быть деструктурирован с помощью `match`.
    match color {
        Color::Red   => println!("Красный цвет!"),
        Color::Blue  => println!("Синий цвет!"),
        Color::Green => println!("Зелёный цвет!"),
        Color::RGB(r, g, b) =>
            println!("Красный: {}, зелёный: {}, и синий: {}!", r, g, b),
        Color::HSV(h, s, v) =>
            println!("Тон: {}, насыщенность: {}, значение: {}!", h, s, v),
        Color::HSL(h, s, l) =>
            println!("Тон: {}, насыщенность: {}, светлота: {}!", h, s, l),
        Color::CMY(c, m, y) =>
            println!("Голубой: {}, пурпурный: {}, жёлтый: {}!", c, m, y),
        Color::CMYK(c, m, y, k) =>
            println!("Голубой: {}, пурпурный: {}, жёлтый: {}, key (чёрный): {}!",
                c, m, y, k),
        // Нет необходимости в других ветвях, т.к были рассмотрены все варианты
    }
}

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

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

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

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

  • Разыменование использует *
  • Деструктуризация использует &, ref и ref mut
fn main() {
    // Присваиваем ссылку на тип `i32`.
    // Символ `&` означает, что присваивается ссылка.
    let reference = &4;

    match reference {
        // Если `reference` - это шаблон, который сопоставляется с `&val`,
        // то это приведёт к сравнению:
        // `&i32`
        // `&val`
        // ^ Мы видим, что если отбросить сопоставляемые `&`,
        // то переменной `val` должно быть присвоено `i32`.
        &val => println!("Получаем значение через деструктуризацию: {:?}", val),
    }

    // Чтобы избежать символа `&`, нужно разыменовывать ссылку до сопоставления.
    match *reference {
        val => println!("Получаем значение через разыменование: {:?}", val),
    }

    // Что если у нас нет ссылки? `reference` была с `&`,
    // потому что правая часть была ссылкой. Но это не ссылка,
    // потому что правая часть ею не является.
    let _not_a_reference = 3;

    // Rust предоставляет ключевое слово `ref` именно для этой цели.
    // Оно изменяет присваивание так, что создаётся ссылка для элемента.
    // Теперь ссылка присвоена.
    let ref _is_a_reference = 3;

    // Соответственно, для определения двух значений без ссылок,
    // ссылки можно назначить с помощью `ref` и `ref mut`.
    let value = 5;
    let mut mut_value = 6;

    // Используйте ключевое слово `ref` для создания ссылки.
    match value {
        ref r => println!("Получили ссылку на значение: {:?}", r),
    }

    // Используйте `ref mut` аналогичным образом.
    match mut_value {
        ref mut m => {
            // Получаем ссылку. Её нужно разыменовать,
            // прежде чем мы сможем что-то добавить.
            *m += 10;
            println!("Мы добавили 10. `mut_value`: {:?}", m);
        },
    }
}

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

Паттерн ref

Структуры

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

fn main() {
    struct Foo { x: (u32, u32), y: u32 }

    // деструктуризация члена структуры
    let foo = Foo { x: (1, 2), y: 3 };
    let Foo { x: (a, b), y } = foo;

    println!("a = {}, b = {},  y = {} ", a, b, y);

    // Вы можете деструктурировать структуру и переименовывать переменные,
    // порядок при этом не важен

    let Foo { y: i, x: j } = foo;
    println!("i = {:?}, j = {:?}", i, j);

    // а так же можно проигнорировать часть переменных:
    let Foo { y, .. } = foo;
    println!("y = {}", y);

    // следующий код выдаст ошибку: в шаблоне нет упоминания поля `x`
    // let Foo { y } = foo;
}

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

Структуры

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

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

fn main() {
    let pair = (2, -2);
    // ЗАДАНИЕ ^ Попробуйте разные значения `pair`

    println!("Расскажи мне о {:?}", pair);
    match pair {
        (x, y) if x == y => println!("Близнецы"),
        // Данное ^ `условие if` является ограничителем шаблонов
        (x, y) if x + y == 0 => println!("Антиматерия, бабах!"),
        (x, _) if x % 2 == 1 => println!("Первое число нечётно"),
        _ => println!("Нет корреляции..."),
    }
}

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

Tuples

Связывание

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

// Функция `age`, возвращающая `u32`.
fn age() -> u32 {
    15
}

fn main() {
    println!("Скажи мне свой возраст");

    match age() {
        0             => println!("Я ещё не отпраздновал свой первый день рождения"),
        // Можно было бы использовать только 1 ... 12 в `match`,
        // но какого возраста тогда был бы ребёнок? Вместо этого мы
        // привязываем `n` к последовательности 1 .. 12. 
        // Теперь мы можем сообщить возраст.
        n @ 1  ..= 12 => println!("Я ребёнок. Мне {:?}", n),
        n @ 13 ..= 19 => println!("Я подросток. Мне {:?}", n),
        // Ничего не привязываем.
        n             => println!("Я взрослый. Мне {:?}", n),
    }
}

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

fn some_number() -> Option<u32> {
    Some(42)
}

fn main() {
    match some_number() {
        // Вариант `Some`, выбираем, если его значение, привязанное к `n`,
        // равно 42.
        Some(n @ 42) => println!("Ответ: {}!", n),
        // При других числах.
        Some(n)      => println!("Не интересно... {}", n),
        // Для всего остального (вариант `None`).
        _            => (),
    }
}

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

Функции, enum и Option

if let

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


#![allow(unused)]
fn main() {
// Создаём переменную `optional` типа `Option<i32>`
let optional = Some(7);

match optional {
    Some(i) => {
        println!("Это очень большая строка и `{:?}`", i);
        // ^ Нужно 2 отступа только для того, чтобы извлечь `i`
    },
    _ => {},
    // ^ Обязателен, так как `match` исчерпывающий. Не выглядит ли это
    // как зря потраченное пространство?
};

}

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

fn main() {
    // Все переменные типа `Option<i32>`
    let number = Some(7);
    let letter: Option<i32> = None;
    let emoticon: Option<i32> = None;

    // Конструкция `if let` читает, как: "Если `let` деструктуризирует `number` в
    // `Some(i)`, выполнить блок (`{}`).
    if let Some(i) = number {
        println!("Соответствует {:?}!", i);
    }

    // Если нужно указать, что делать, в случае ошибки, можно добавить else:
    if let Some(i) = letter {
        println!("Соответствует {:?}!", i);
    } else {
        // Ошибка деструктуризации. Переходим к обработке ошибки.
        println!("Не соответствует числу. Давайте попробуем строку!");
    }

    // Добавляем ещё одну ситуацию несоответствия образцу.
    let i_like_letters = false;

    if let Some(i) = emoticon {
        println!("Соответствует {:?}!", i);
    // Оцените условие `else if`, чтобы увидеть,
    // должна ли быть альтернативная ветка отказа:
    } else if i_like_letters {
        println!("Не соответствует числу. Давайте попробуем строку!");
    } else {
        // Рассматриваем ложное условие. Эта ветвь по умолчанию:
        println!("Мне не нравится сравнивать строки. Давайте возьмём смайлик :)!");
    }
}

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

// Наш пример перечисления
enum Foo {
    Bar,
    Baz,
    Qux(u32)
}

fn main() {
    // Создание переменных примера
    let a = Foo::Bar;
    let b = Foo::Baz;
    let c = Foo::Qux(100);
    
    // Переменная `a` соответствует `Foo::Bar`
    if let Foo::Bar = a {
        println!("a = Foo::Bar");
    }
    
    // Переменная `b` не соответствует `Foo::Bar`.
    // Поэтому ничего не выведется на экран
    if let Foo::Bar = b {
        println!("b = Foo::Bar");
    }
    
    // Переменная `c` соответствует `Foo::Qux`, которая имеет значение
    // аналогичное `Some()` как в предыдущем примере:
    if let Foo::Qux(value) = c {
        println!("c ={}", value);
    }

    // С `if let` также работает и привязка
    if let Foo::Qux(value @ 100) = c {
        println!("c = 100");
    }
}

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

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

// Для это перечисление намеренно не добавлен #[derive(PartialEq)],
// и мы не реализовывали для него PartialEq. Вот почему сравнение Foo::Bar == a терпит неудачу.
enum Foo {Bar}

fn main() {
    let a = Foo::Bar;

    // Переменная соответствует Foo::Bar
    if Foo::Bar == a {
    // ^-- это вызовет ошибку компиляции. Используйте `if let` вместо этого.
        println!("a is foobar");
    }
}

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

enum, Option, и RFC

while let

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


#![allow(unused)]
fn main() {
// Создадим переменную `optional` с типом `Option<i32>`
let mut optional = Some(0);

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

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

fn main() {
    // Создадим переменную `optional` с типом `Option<i32>`
    let mut optional = Some(0);

    // Это можно прочитать так: "Пока `let` деструктурирует `optional` в
    // `Some(i)`, выполняем блок (`{}`). В противном случае `break`.
    while let Some(i) = optional {
        if i > 9 {
            println!("Больше 9, уходим отсюда!");
            optional = None;
        } else {
            println!("`i` равен `{:?}`. Попробуем ещё раз.", i);
            optional = Some(i + 1);
        }
        // ^ Меньше смещаемся вправо, к тому же
        // нет необходимости обрабатывать ошибки.
    }
    // ^ К `if let` можно добавить дополнительный блок `else`/`else if`
    // Для `while let` подобного нет.
}

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

enum, Option, and the RFC

Функции

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

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

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

// В отличие от C/C++ здесь нет ограничений на порядок определения функций
fn main() {
    // Мы можем использовать функцию здесь, а определить её где-нибудь позже
    fizzbuzz_to(100);
}

// Функция, которая возвращает булево значение
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
    // Особый случай, ранний возврат
    if rhs == 0 {
        return false;
    }

    // Это выражение, ключевое слово `return` здесь не требуется
    lhs % rhs == 0
}

// Функции которые "не" возвращают значение, на самом деле возвращают единичный тип `()`
fn fizzbuzz(n: u32) -> () {
    if is_divisible_by(n, 15) {
        println!("fizzbuzz");
    } else if is_divisible_by(n, 3) {
        println!("fizz");
    } else if is_divisible_by(n, 5) {
        println!("buzz");
    } else {
        println!("{}", n);
    }
}

// Если функция возвращает `()`, тип возвращаемого значения можно не указывать
// в сигнатуре
fn fizzbuzz_to(n: u32) {
    for n in 1..=n {
        fizzbuzz(n);
    }
}

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

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

struct Point {
    x: f64,
    y: f64,
}

// Блок реализации, все функции и методы, связанные с типом `Point` размещаются здесь
impl Point {
    // Это "связанная функция", так как эта функция связана
    // с конкретным типом, в данном случае, Point.
    //
    // Связанные функции не обязательно вызывать с каким-то экземпляром класса.
    // Чаще всего такие функции используются как конструкторы.
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }

    // Ещё одны связанная функция, принимающая два аргумента:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }
}

struct Rectangle {
    p1: Point,
    p2: Point,
}

impl Rectangle {
    // Это метод
    // `&self` - это синтаксический сахар для замены `self: &Self`, где `Self` - это тип вызывающего объекта
    // В данном случае `Self` = `Rectangle`
    fn area(&self) -> f64 {
        // `self` предоставляет доступ к полям структуры с помощью оператора "точка"
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        // `abs` - это метод, возвращающий переменную типа `f64`, равную абсолютному значению
        // вызывающего объекта
        ((x1 - x2) * (y1 - y2)).abs()
    }

    fn perimeter(&self) -> f64 {
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
    }

    // Этот метод требует, чтобы вызывающий объект был изменяемым
    // `&mut self` преобразуется в  `self: &mut Self`
    fn translate(&mut self, x: f64, y: f64) {
        self.p1.x += x;
        self.p2.x += x;

        self.p1.y += y;
        self.p2.y += y;
    }
}

// `Pair` владеет ресурсами: двумя целыми числами, память для которых выделена в куче
struct Pair(Box<i32>, Box<i32>);

impl Pair {
    // Этот метод "потребляет" ресурсы вызывающего объекта
    // `self` преобразуется в  `self: Self`
    fn destroy(self) {
        // Деструктуризируем `self`
        let Pair(first, second) = self;

        println!("Удаляем Pair({}, {})", first, second);

        // `first` и `second` выходят за пределы области видимости и удаляются
    }
}

fn main() {
    let rectangle = Rectangle {
        // Связанные функции вызываются с помощью двойных двоеточий
        p1: Point::origin(),
        p2: Point::new(3.0, 4.0),
    };

    // Методы вызываются с помощью оператора "точка"
    // Обратите внимание, что первый аргумент `&self` передаётся неявно, т.е.
    // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
    println!("Периметр прямоугольника: {}", rectangle.perimeter());
    println!("Площадь прямоугольника: {}", rectangle.area());

    let mut square = Rectangle {
        p1: Point::origin(),
        p2: Point::new(1.0, 1.0),
    };

    // Ошибка! `rectangle` неизменяемый, но в методе требуется изменяемый
    // объект
    //rectangle.translate(1.0, 0.0);
    // TODO ^ Попробуйте раскомментировать эту строку

    // Порядок! Изменяемые объекты могут вызывать изменяемые методы
    square.translate(1.0, 1.0);

    let pair = Pair(Box::new(1), Box::new(2));

    pair.destroy();

    // Ошибка! Предыдущий вызов `destroy` "употребил" переменную `pair`
    //pair.destroy();
    // TODO ^ Попробуйте раскомментировать эту строку
}

Замыкания

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

|val| val + x

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

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

  • использование || вместо () для аргументов.
  • опциональное ограничения тела функции ({}) для одного выражения (в противном случае обязательно).
  • возможность захвата переменных за пределами окружения
fn main() {
    // Инкремент с помощью замыкания и функции.
    fn  function            (i: i32) -> i32 { i + 1 }

    // Замыкания анонимны. Тут мы связываем их с ссылками
    // Аннотация идентичны аннотации типов функции, но является опциональной
    // как и оборачивания тела в `{}`. Эти безымянные функции
    // назначены соответствующе названным переменным.
    let closure_annotated = |i: i32| -> i32 { i + 1 };
    let closure_inferred  = |i     |          i + 1  ;

    let i = 1;
    // Вызов функции и замыкания.
    println!("функция: {}", function(i));
    println!("замыкание с указанием типа: {}", closure_annotated(i));
    println!("замыкание с выводом типа: {}", closure_inferred(i));

    // Замыкание не принимает аргументов, но возвращает `i32`.
    // Тип возвращаемого значения выведен автоматически.
    let one = || 1;
    println!("замыкание, возвращающее один: {}", one());

}

Захват

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

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

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

fn main() {
    use std::mem;
    
    let color = "green";

    // Замыкание для вывода `color`, которое немедленно заимствует (`&`)
    // `color` и сохраняет замыкание в переменной `print`. `color` будет оставаться
    // заимствованным до тех пор, пока `print` не будет использован в последний раз.
    //
    // `println!` принимает аргументы только по неизменяемым ссылкам, поэтому он не накладывает
    // дополнительных ограничений.
    let print = || println!("`color`: {}", color);

    // Вызываем замыкание, использующее заимствование.
    print();

    // `color` может быть неизменяемо заимствован, так как замыкание
    // держит только неизменяемую ссылку на `color`.
    let _reborrow = &color;
    print();

    // Перемещение или перезанятие возможно после последнего использования `print`
    let _color_moved = color;


    let mut count = 0;
    // Замыкание для увеличения `count` может принимать как `&mut count`, так и `count`,
    // но использование `&mut count` накладывает меньше ограничений, так что
    // замыкание выбирает первый способ, т.е. немедленно заимствует `count`.
    //
    // `inc` должен быть `mut`, поскольку внутри него хранится `&mut`.
    // Таким образом, вызов замыкания изменяет его, что недопустимо без `mut`.
    let mut inc = || {
        count += 1;
        println!("`count`: {}", count);
    };

    // Вызываем замыкание, использующее изменяемое заимствование.
    inc();

    // Замыкание продолжает изменяемо заимствовать `count`, так как оно используется дальше.
    // Попытка перезанять приведёт к ошибке.
    // let _reborrow = &count;
    // ^ TODO: попробуйте раскомментировать эту строку.
    inc();

    // Замыкание больше не заимствует `&mut count`. Так что теперь
    // при перезаимствовании ошибок не будет.
    let _count_reborrowed = &mut count;

    
    // Некопируемый тип.
    let movable = Box::new(3);

    // `mem::drop` требует `T`, так что захват производится по значению.
    // Копируемый тип будет скопирован в замыкание, оставив оригинальное
    // значение без изменения. Некопируемый тип должен быть перемещён, так что
    // `movable` немедленно перемещается в замыкание.
    let consume = || {
        println!("`movable`: {:?}", movable);
        mem::drop(movable);
    };

    // `consume` поглощает переменную, так что оно может быть вызвано только один раз.
    consume();
    // consume();
    // ^ TODO: Попробуйте раскомментировать эту строку.
}

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

fn main() {
    // Vec` не поддерживает копирование.
    let haystack = vec![1, 2, 3];

    let contains = move |needle| haystack.contains(needle);

    println!("{}", contains(&1));
    println!("{}", contains(&4));

    // println!("Количество элементов {} в векторе", haystack.len());
    // ^ Уберите комментарий с этой строки и в результате получите ошибку компиляции,
    // потому что анализатор заимствований не позволяет использовать
    // переменную после передачи владения.
    
    // Удалите `move` у замыкания и _haystack_ будет заимствован по неизменяемой
    // ссылке, и удалённый комментарий теперь не вызывает ошибки.
}

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

Box и std::mem::drop

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

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

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

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

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

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

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

// Функция, которая принимает замыкание в качестве аргумента и вызывает его.
// <F> обозначает, что F - "параметр общего типа"
fn apply<F>(f: F) where
    // Замыкание ничего не принимает и не возвращает.
    F: FnOnce() {
    // ^ TODO: Попробуйте изменить это на `Fn` или `FnMut`.

    f();
}

// Функция, которая принимает замыкание и возвращает `i32`.
fn apply_to_3<F>(f: F) -> i32 where
    // Замыкание принимает `i32` и возвращает `i32`.
    F: Fn(i32) -> i32 {

    f(3)
}

fn main() {
    use std::mem;

    let greeting = "привет";
    // Не копируемый тип.
    // `to_owned` преобразует заимствованные данные в собственные.
    let mut farewell = "пока".to_owned();

    // Захват двух переменных: `greeting` по ссылке и
    // `farewell` по значению.
    let diary = || {
        // `greeting` захватывается по ссылке: требует `Fn`.
        println!("Я сказал {}.", greeting);

        // Изменяемость требует от `farewell` быть захваченным
        // по изменяемой ссылке. Сейчас требуется `FnMut`.
        farewell.push_str("!!!");
        println!("Потом я закричал {}.", farewell);
        println!("Теперь я могу поспать. zzzzz");

        // Ручной вызов удаления требуется от `farewell`
        // быть захваченным по значению. Теперь требуется `FnOnce`.
        mem::drop(farewell);
    };

    // Вызов функции, которая выполняет замыкание.
    apply(diary);

    // `double` удовлетворяет ограничениям типажа `apply_to_3`
    let double = |x| 2 * x;

    println!("Удвоенное 3: {}", apply_to_3(double));
}

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

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

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

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


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

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

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

// `F` должен реализовать `Fn` для замыкания, которое
// ничего не принимает и не возвращает - именно то,
// что нужно для `print`.
fn apply<F>(f: F) where
    F: Fn() {
    f();
}

fn main() {
    let x = 7;

    // Захватываем `x` в анонимный тип и реализуем
    // `Fn` для него. Сохраняем его как `print`.
    let print = || println!("{}", x);

    apply(print);
}

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

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

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

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

// Объявляем функцию, которая принимает обобщённый тип `F`,
// ограниченный типажом `Fn`, и вызывает его.
fn call_me<F: Fn()>(f: F) {
    f();
}

// Объявляем функцию-обёртку, удовлетворяющую ограничению `Fn`
fn function() {
    println!("Я функция!");
}

fn main() {
    // Определяем замыкание, удовлетворяющее ограничению `Fn`
    let closure = || println!("Я замыкание!");

    call_me(closure);
    call_me(function);
}

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

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

Fn, FnMut, и FnOnce

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

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

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

  • Fn
  • FnMut
  • FnOnce

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

fn create_fn() -> impl Fn() {
    let text = "Fn".to_owned();

    move || println!("a: {}", text)
}

fn create_fnmut() -> impl FnMut() {
    let text = "FnMut".to_owned();

    move || println!("a: {}", text)
}

fn create_fnonce() -> impl FnOnce() {
    let text = "FnOnce".to_owned();

    move || println!("a: {}", text)
}

fn main() {
    let fn_plain = create_fn();
    let mut fn_mut = create_fnmut();
    let fn_once = create_fnonce();

    fn_plain();
    fn_mut();
    fn_once();
}

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

Fn, FnMut, обобщения и impl Trait.

Примеры из библиотеки std

Этот раздел содержит несколько примеров использования замыканий из библиотеки std.

Iterator::any

Iterator::any - это функция, которая принимает итератор и возвращает true, если любой элемент удовлетворяет предикату. Иначе возвращает false. Её объявление:

pub trait Iterator {
    // Тип, по которому выполняется итерирование
    type Item;

    // `any` принимает `&mut self`, что означает заимствование
    // и изменение, но не поглощение `self`.
    fn any<F>(&mut self, f: F) -> bool where
        // `FnMut` означает, что любая захваченная переменная
        // может быть изменена, но не поглощена. `Self::Item`
        // указывает на захват аргументов замыкания по значению.
        F: FnMut(Self::Item) -> bool {}
}
fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    // `iter()` для векторов даёт `&i32`. Приводим к `i32`.
    println!("2 в vec1: {}", vec1.iter()     .any(|&x| x == 2));
    // `into_iter()` для векторов даёт `i32`. Приведения не требуется.
    println!("2 в vec2: {}", vec2.into_iter().any(| x| x == 2));

    let array1 = [1, 2, 3];
    let array2 = [4, 5, 6];

    // `iter()` для массивов даёт `&i32`.
    println!("2 в array1: {}", array1.iter()     .any(|&x| x == 2));
    // `into_iter()` для массивов неожиданно даёт `&i32`.
    println!("2 в array2: {}", array2.into_iter().any(|&x| x == 2));
}

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

std::iter::Iterator::any

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

Iterator::find - функция, которая перебирает значения итератора и ищет первое значение, удовлетворяющее условию. Если ни одно из значений не удовлетворяет условию, то возвращается None. Её сигнатура:

pub trait Iterator {
    // Тип, по которому выполняется итерирование.
    type Item;

    // `find` принимает `&mut self`, что означает, что вызывающий объект может быть заимствован
    // и изменён, но не поглощён.
    fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
        // `FnMut` означает, что любая захваченная переменная может максимум быть
        // изменена, но не поглощена. `&Self::Item`
        //казывает на то, что аргументы замыкания берутся по ссылке.
        P: FnMut(&Self::Item) -> bool;
}
fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    // `iter()` для векторов выдаёт `&i32`.
    let mut iter = vec1.iter();
    // `into_iter()` для векторов выдаёт `i32`.
    let mut into_iter = vec2.into_iter();

    // `iter()` для векторов выдаёт `&i32`, а мы хотим ссылаться на один из его
    // элементов, поэтому нам нужно деструктурировать `&&i32` в `i32`
    println!("Найдём 2 в vec1: {:?}", iter     .find(|&&x| x == 2));
    // `into_iter()` для векторов выдаёт `i32`, а мы хотим ссылаться на один
    // из его элементов, поэтому нам нужно деструктурировать `&i32` в `i32`
    println!("Найдём 2 в vec2: {:?}", into_iter.find(| &x| x == 2));

    let array1 = [1, 2, 3];
    let array2 = [4, 5, 6];

    // `iter()` для массивов выдаёт `&i32`
    println!("Find 2 in array1: {:?}", array1.iter()     .find(|&&x| x == 2));
    // `into_iter()` для массивов выдаёт `i32`
    println!("Find 2 in array2: {:?}", array2.into_iter().find(|&x| x == 2));
}

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

fn main() {
    let vec = vec![1, 9, 3, 3, 13, 2];

    // `iter()` для векторов выдаёт `&i32`, а `position()` не принимает ссылку, поэтому
    // мы должны деструктурировать `&i32` в `i32`
    let index_of_first_even_number = vec.iter().position(|&x| x % 2 == 0);
    assert_eq!(index_of_first_even_number, Some(5));
    
    // `into_iter()` для векторов выдаёт `i32`, а `position()` не принимает ссылку, поэтому
    // деструктуризация не требуется
    let index_of_first_negative_number = vec.into_iter().position(|x| x < 0);
    assert_eq!(index_of_first_negative_number, None);
}

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

std::iter::Iterator::find

std::iter::Iterator::find_map

std::iter::Iterator::position

std::iter::Iterator::rposition

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

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

fn is_odd(n: u32) -> bool {
    n % 2 == 1
}

fn main() {
    println!("Найти сумму всех квадратов нечётных чисел не больше 1000");
    let upper = 1000;

    // Императивный подход
    // Объявляем переменную-накопитель
    let mut acc = 0;
    // Итерируем: 0, 1, 2, ... до бесконечности
    for n in 0.. {
        // Возводим число в квадрат
        let n_squared = n * n;

        if n_squared >= upper {
            // Останавливаем цикл, если превысили верхний лимит
            break;
        } else if is_odd(n_squared) {
            // Прибавляем число, если оно нечётное
            acc += n_squared;
        }
    }
    println!("императивный стиль: {}", acc);

    // Функциональный подход
    let sum_of_squared_odd_numbers: u32 =
        (0..).map(|n| n * n)             // Все натуральные числа возводим в квадрат
             .take_while(||&n_squared| n_squared < upper) // Берём те, что ниже верхнего предела
             .filter(&n_squared| is_odd(n_squared))     // Выбираем нечётные
             .sum(); // Складываем
    println!("функциональный стиль: {}", sum_of_squared_odd_numbers);
}

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

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

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


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

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

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

fn some_fn() {
    ()
}

fn main() {
    let a: () = some_fn();
    println!("Эта функция возвращает управление и вы можете увидеть эту строку.")
}

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

#![feature(never_type)]

fn main() {
    let x: ! = panic!("Этот вызов никогда не вернёт управление.");
    println!("вы никогда не увидете эту строку!");
}

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

fn main() {
    fn sum_odd_numbers(up_to: u32) -> u32 {
        let mut acc = 0;
        for i in 0..up_to {
            // Обратите внимание, что возвращаемый тип этого выражения match должен быть u32
            // потому что такой тип в переменной "addition" .
            let addition: u32 = match i%2 == 1 {
                // Переменная "i" типа u32, что совершенно нормально.
                true => i,
                // С другой стороны выражение "continue" не возвращает
                // u32, но это тоже нормально, потому что это тип не возвращающий управление,
                // не нарушает требования к типу выражения match.
                false => continue,
            };
            acc += addition;
        }
        acc
    }
    println!("Сумма нечётных чисел до 9 (исключая): {}", sum_odd_numbers(9));
}

Это также возвращаемый тип функций, которые содержат вечный цикл (например, loop {}), как сетевые серверы или функции, завершающие процесс (например, exit()).

Модули

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

Модуль - это набор элементов, таких как: функции, структуры, типажи, блоки реализации (impl) и даже другие модули.

Видимость

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

// Модуль по имени `my_mod`
mod my_mod {
    // Все элементы модуля по умолчанию являются приватными.
    fn private_function() {
        println!("вызвана `my_mod::private_function()`");
    }

    // Используем модификатор `pub`, чтобы сделать элемент публичным.
    pub fn function() {
        println!("вызвана `my_mod::function()`");
    }

    // Приватные элементы модуля доступны другим элементам
    // данного модуля.
    pub fn indirect_access() {
        print!("вызвана `my_mod::indirect_access()`, которая\n> ");
        private_function();
    }

    // Модули так же могут быть вложенными
    pub mod nested {
        pub fn function() {
            println!("вызвана `my_mod::nested::function()`");
        }

        #[allow(dead_code)]
        fn private_function() {
            println!("вызвана `my_mod::nested::private_function()`");
        }

        // Функции объявленные с использованием синтаксиса `pub(in path)` будет видна
        // только в пределах заданного пути.
        // `path` должен быть родительским или наследуемым модулем
        pub(in my_mod) fn public_function_in_my_mod() {
            print!("вызвана `my_mod::nested::public_function_in_my_mod()`, которая\n > ");
            public_function_in_nested()
        }

        // Функции объявленные с использованием синтаксиса `pub(self)` будет видна
        // только в текущем модуле
        pub(self) fn public_function_in_nested() {
            println!("вызвана `my_mod::nested::public_function_in_nested");
        }

        // Функции объявленные с использованием синтаксиса `pub(super)` будет видна
        // только в родительском модуле
        pub(super) fn public_function_in_super_mod() {
            println!("вызвана my_mod::nested::public_function_in_super_mod");
        }
    }

    pub fn call_public_function_in_my_mod() {
        print!("вызвана `my_mod::call_public_funcion_in_my_mod()`, которая\n> ");
        nested::public_function_in_my_mod();
        print!("> ");
        nested::public_function_in_super_mod();
    }

    // pub(crate) сделает функцию видимой для всего текущего контейнера
    pub(crate) fn public_function_in_crate() {
        println!("called `my_mod::public_function_in_crate()");
    }

    // Вложенные модули подчиняются тем же правилам видимости
    mod private_nested {
        #[allow(dead_code)]
        pub fn function() {
            println!("вызвана `my_mod::private_nested::function()`");
        }
    }
}

fn function() {
    println!("вызвана `function()`");
}

fn main() {
    // Модули позволяют устранить противоречия между элементами,
    // которые имеют одинаковые названия.
    function();
    my_mod::function();

    // Публичные элементы, включая те, что находятся во вложенном модуле,
    // доступны извне родительского модуля
    my_mod::indirect_access();
    my_mod::nested::function();
    my_mod::call_public_function_in_my_mod();

    // pub(crate) элементы можно вызвать от везде в этом же пакете
    my_mod::public_function_in_crate();
    
    // pub(in path) элементы могут вызываться только для указанного модуля
    // Ошибка! функция `public_function_in_my_mod` приватная
    //my_mod::nested::public_function_in_my_mod();
    // TODO ^ Попробуйте раскомментировать эту строку

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

    // Ошибка! функция `private_function` приватная
    //my_mod::private_function();
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Ошибка! функция `private_function` приватная
    //my_modmy::nested::private_function();
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Ошибка! Модуль `private_nested` является приватным
    //my_mod::private_nested::function();
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку
}

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

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

mod my {
   // Публичная структура с публичным полем обобщённого типа `T`
    pub struct OpenBox<T> {
        pub contents: T,
    }

    // Публичная структура с приватным полем обобщённого типа `T`
    #[allow(dead_code)]
    pub struct ClosedBox<T> {
        contents: T,
    }

    impl<T> ClosedBox<T> {
        // Публичный конструктор
        pub fn new(contents: T) -> ClosedBox<T> {
            ClosedBox {
                contents: contents,
            }
        }
    }
}

fn main() {
    // Публичные структуры с публичными полеми могут быть созданы как обычно,
    let open_box = my::OpenBox { contents: "публичную информацию" };

    // а их поля доступны всем.
    println!("Открытая упаковка хранит: {}", open_box.contents);

    // Публичные структуры с приватными полями не могут быть созданы с использованием имён полей
    // Ошибка! `ClosedBox` имеет приватные поля
    //let closed_box = my::ClosedBox { contents: "классифицированную информацию" };
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Однако, структуры с приватными полями могут быть созданы с помощью
    // публичного конструктора
    let _closed_box = my::ClosedBox::new("классифицированную информацию");

    // нельзя получить доступ к приватным полям публичных структур.
    // Ошибка! Поле `contents` приватное
    //println!("Закрытая упаковка хранит: {}", _closed_box.contents);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку
}

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

generics и методы

Декларация use

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

use crate::deeply::nested::{
    my_first_function,
    my_second_function,
    AndATraitType
};

fn main() {
    my_first_function();
}

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

// Привязать путь `deeply::nested::function` к `other_function`.
use deeply::nested::function as other_function;

fn function() {
    println!("вызвана `function()`");
}

mod deeply {
    pub mod nested {
        pub fn function() {
            println!("вызвана `deeply::nested::function()`")
        }
    }
}

fn main() {
    // Упрощённый доступ к `deeply::nested::function`
    other_function();

    println!("Entering block");
    {
        // Эквивалентно `use deeply::nested::function as function`.
        // `function()` затенение собой внешнюю функцию.
        use deeply::nested::function;
        function();

        // у привязок `use` локальная область видимости. В данном случае
        // внешняя `function()` затенена только в этом блоке.
        println!("Leaving block");
    }

    function();
}

super и self

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

fn function() {
    println!("вызвана `function()`");
}

mod cool {
    pub fn function() {
        println!("called `cool::function()`");
    }
}

mod my {
    fn function() {
        println!("вызвана `my::function()`");
    }
    
    mod cool {
        pub fn function() {
            println!("вызвана `my::cool::function()`");
        }
    }
    
    pub fn indirect_call() {
        // Давайте вызовем  все функции под названием `function` в этой области видимости!
        print!("вызвана `my::indirect_call()`, с помощью которой\n> ");
        
        // Ключевое слово `self` ссылается на область видимости текущего модуля. 
        // В нашем случае - модуль `my`.
        // Вызов `self::function()`, так же как и вызов `function()` дают одинаковый результат,
        // т.к они ссылаются на одну и ту же функцию.
        self::function();
        function();
        
        // Мы так же можем использовать ключевое слово `self`,
        // чтобы получить доступ к другим модулям внутри модуля `my`:
        self::cool::function();
        
        // Ключевое слово `super` ссылается на родительскую область видимости (вне модуля `my`).
        super::function();
        
        // Этим действием мы свяжем `cool::function` в области видимости *контейнера*.
        // В данном случае область видимости контейнера является самой дальней областью видимости.
        {
            use cool::function as root_function;
            root_function();
        }
    }
}

fn main() {
    my::indirect_call();
}

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

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

$ tree .
.
├── my
│   ├── inaccessible.rs
│   └── nested.rs
├── my.rs
└── split.rs

В split.rs:

// Это объявление запустит поиск файла с именем `my.rs` и
// вставит его содержимое в модуль с именем `my` под текущей областью видимости
mod my;

fn function() {
    println!("вызвали `function()`");
}

fn main() {
    my::function();

    function();

    my::indirect_access();

    my::nested::function();
}

В my.rs :

// Точно так же, `mod inaccessible` и `mod nested` обнаружат файлы `nested.rs`
// и `inaccessible.rs`, и затем вставят их здесь в соответствующие модули

mod inaccessible;
pub mod nested;

pub fn function() {
    println!("вызвана `my::function()`");
}

fn private_function() {
    println!("вызывает `my::private_function()`");
}

pub fn indirect_access() {
    print!("вызвана `my::indirect_access()`, которая\n> ");

    private_function();
}

В my/nested.rs:

pub fn function() {
    println!("вызвана `my::nested::function()`");
}

#[allow(dead_code)]
fn private_function() {
    println!("вызвана `my::nested::private_function()`");
}

В my/inaccessible.rs:

#[allow(dead_code)]
pub fn public_function() {
    println!("вызвана `my::inaccessible::public_function()`");
}

Давайте проверим, что все ещё работает, как раньше:

$ rustc split.rs && ./split
вызвана `my::function()`
вызвана `function()`
вызвана `my::indirect_access()`, которая
> вызвана `my::private_function()`
вызвана `my::nested::function()`

Контейнеры

Контейнер (crate) — единица компиляции в языке Rust. Когда вызывается rustc some_file.rs, some_file.rs обрабатывается как файл контейнера. Если в some_file.rs есть декларация mod, то содержимое модуля будет объединено с файлом контейнера перед его компиляцией. Другими словами, модули не собираются отдельно, собираются лишь контейнеры.

Контейнер может быть скомпилирован в исполняемый файл или в библиотеку. По умолчанию rustc создаёт из контейнера исполняемый файл. Это поведение может быть изменено добавлением флага --crate-type со значением lib к rustc.

Создание проекта

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

pub fn public_function() {
    println!("called rary's `public_function()`");
}

fn private_function() {
    println!("called rary's `private_function()`");
}

pub fn indirect_access() {
    print!("called rary's `indirect_access()`, that\n> ");

    private_function();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

Библиотеки получают префикс «lib», и по умолчанию они получают имена в честь своего крейта, но это имя по умолчанию можно переопределить, передав параметр --crate-name в rustc или используя атрибут crate_name.

extern crate

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

// Ссылка на `library`. Импортируем элементы, как модуль `rary`
extern crate rary;

fn main() {
    rary::public_function();

    // Ошибка! Функция `private_function` приватная
    //rary::private_function();

    rary::indirect_access();
}
# Где library.rlib путь к скомпилированной библиотеке. Предположим, что
# она находится в той же директории:
$ rustc executable.rs --extern rary=library.rlib && ./executable
вызвана `public_function()` библиотеки rary
вызвана `indirect_access()` библиотеки rary, и в ней
> вызвана `private_function()` библиотеки rary

Cargo

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

  • Управление зависимостями и интеграция с crates.io (официальный реестр пакетов Rust)
  • Осведомлённость о модульных тестах
  • Осведомлённость о тестах производительности

Эта глава рассказывает об основах, но вы можете найти полное описание по адресу The Cargo Book.

Зависимости

Большинство программ зависят от нескольких библиотек. Если вам приходилось когда-либо управлять зависимостями вручную, вы знаете, сколько боли это может доставить. К счастью экосистема языка Rust содержит такой инструмент как cargo! cargo может управлять зависимостями проекта.

Создание нового проекта на языке Rust:

# Исполняемый проект (проект с программой)
cargo new foo

# ИЛИ библиотека
cargo new --lib foo

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

После выполнения следующих команд вы увидите примерно следующую иерархию файлов:

foo
├── Cargo.toml
└── src
    └── main.rs

main.rs - это корневой файл вашего нового проекта. Cargo.toml - это конфигурационный файл этого проекта (foo) для cargo. Если посмотрите внутрь файла, вы должны увидеть что-то вроде этого:

[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]

[dependencies]

Поле name под [package] определяет имя проекта. Оно используется если Вы будете его публиковать на crates.io (более подробно позже). Также это имя выходного файла при компиляции.

Поле version - это версия пакета, записанное с использованием системы семантического версионирования.

Поле authors содержит список авторов пакета и используется при публикации.

В секции [dependencies] вы можете указывать зависимости вашего проекта.

Предположим, что вы хотите, чтобы ваша программа имела отличный CLI. Вы можете найти много хороших пакетов на crates.io (официальный реестр пакетов языка Rust). Один из популярных вариантов - clap. На момент написания этой статьи самой последней опубликованной версией clap является 2.27.1. Для добавления зависимости в ваш проект, вы можете просто добавить соответствующую запись в Ваш Cargo.toml под [dependencies]: clap = "2.27.1". И конечно, extern crate clap в main.rs. И это все! Вы можете начать использовать clap в вашей программе.

cargo также поддерживает другие типы зависимостей. Здесь только небольшие примеры:

[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]

[dependencies]
clap = "2.27.1" # из crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # из онлайн репозитория
bar = { path = "../bar" } # из локальной файловой системы

cargo больше чем менеджер зависимостей. Все поддерживаемые возможности доступны в спецификации формата Cargo.toml.

Для сборки проекта Вы можете выполнить команду cargo build в любой директории проекта (включая поддиректории!). Также Вы можете выполнить cargo run для сборки и запуска. Обратите внимание, что эти команды разрешат все зависимости, скачают пакеты если нужно, и соберут все, включая ваш пакет. (Обратите внимание, что он собирает только то, что ещё не собрал, подобно make).

Вот и все!

Соглашения

В предыдущей главе мы видели следующую иерархию каталогов:

foo
├── Cargo.toml
└── src
    └── main.rs

Предположим, что мы хотим иметь два двоичных файла в одном проекте. Что тогда?

Оказывается, cargo это поддерживает. Двоичный файл по умолчанию называется main.rs, это мы видели раньше, но вы можете добавить дополнительные файлы, поместив их в каталог bin/:

foo
├── Cargo.toml
└── src
    ├── main.rs
    └── bin
        └── my_other_bin.rs

Чтобы сказать cargo скомпилировать или запустить этот двоичный файл, мы просто передаём cargo флаг --bin my_other_bin, где my_other_bin это имя двоичного файла, с которым мы хотим работать.

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

В следующей главе мы более подробно рассмотрим тесты.

Тестирование

Как мы знаем, тестирование является неотъемлемой частью любого программного обеспечения! Rust имеет первоклассную поддержку модульного и интеграционного тестирования (см. главу о тестировании в TRPL).

Из разделов тестирования, приведённых выше, мы знаем, как писать модульные и интеграционные тесты. Организационно, мы можем расположить модульные тесты в модулях, которые они тестируют, а интеграционные - в собственном каталоге tests/:

foo
├── Cargo.toml
├── src
│   └── main.rs
└── tests
    ├── my_test.rs
    └── my_other_test.rs

Каждый файл в каталоге tests - это отдельный интеграционный тест.

cargo естественно, обеспечивает простой способ запуска всех ваших тестов!

$ cargo test

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

$ cargo test
   Compiling blah v0.1.0 (file:///nobackup/blah)
    Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs
     Running target/debug/deps/blah-d3b32b97275ec472

running 3 tests
test test_bar ... ok
test test_baz ... ok
test test_foo_bar ... ok
test test_foo ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Вы также можете запустить тесты, чьё имя соответствует шаблону:

$ cargo test test_foo
$ cargo test test_foo
   Compiling blah v0.1.0 (file:///nobackup/blah)
    Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs
     Running target/debug/deps/blah-d3b32b97275ec472

running 2 tests
test test_foo ... ok
test test_foo_bar ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out

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

Скрипты сборки

Иногда обычной сборки, предоставляемой cargo, недостаточно. Возможно вашему крейту нужны некоторые предварительные условия, прежде чем он успешно скомпилируется, например кодогенерация или предварительно должен скомпилироваться какой-то нативный код. Для решения этой проблемы, мы имеем скрипты сборки, которые cargo может запустить.

Для добавления скрипта сборки в ваш пакет, вы можете указать его в Cargo.toml следующим образом:

[package]
...
build = "build.rs"

Иначе по умолчанию cargo будет искать файл build.rs в директории проекта.

Как использовать скрипт сборки

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

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

Скрипт возвращает значения через stdout. Все напечатанные строки записываются в target/debug/build/<pkg>/output. Кроме того, строки с префиксом cargo: напрямую интерпретируются cargo и следовательно могут быть использованы для объявления параметров для компиляции пакета.

Больше информации и примеров можно найти в спецификации cargo.

Атрибуты

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

Когда атрибуты применяются ко всему контейнеру, их синтаксис будет #![crate_attribute], а когда они применяются к модулю или элементу модуля, их синтаксис станет #[item_attribute] (обратите внимание на отсутствие !).

Атрибуты могут принимать аргументы с различным синтаксисом:

  • #[attribute = "value"]
  • #[attribute(key = "value")]
  • #[attribute(value)]

Атрибуты могут иметь несколько значений и могут быть разделены несколькими строками:

#[attribute(value, value2)]


#[attribute(value, value2, value3,
            value4, value5)]

dead_code

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

fn used_function() {}

// `#[allow(dead_code)]` — атрибут, который убирает проверку на неиспользуемый код
#[allow(dead_code)]
fn unused_function() {}

fn noisy_unused_function() {}
// FIXME ^ Добавьте атрибут `dead_code`, чтобы убрать предупреждение

fn main() {
    used_function();
}

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

Контейнеры

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

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

// Этот контейнер - библиотека
#![crate_type = "lib"]
// Эта библиотека называется "rary"
#![crate_name = "rary"]

pub fn public_function() {
    println!("вызвана `public_function()` библиотеки `rary`");
}

fn private_function() {
    println!("вызвана `private_function()` библиотеки `rary`");
}

pub fn indirect_access() {
    print!("вызвана `indirect_access()` библиотеки `rary`, и в ней\n> ");

    private_function();
}

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

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

cfg

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

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

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

// Эта функция будет скомпилирована только в том случае, если целевая ОС будет linux
#[cfg(target_os = "linux")]
fn are_you_on_linux() {
    println!("Вы работаете в linux!");
}

// А эта функция будет скомпилирована, если целевая ОС *не* linux
#[cfg(not(target_os = "linux"))]
fn are_you_on_linux() {
    println!("Вы работаете *не* в linux!");
}

fn main() {
    are_you_on_linux();
    
    println!("Вы уверены?");
    if cfg!(target_os = "linux") {
        println!("Да. Это точно linux!");
    } else {
        println!("Да. Это точно *не* linux!");
    }
}

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

Reference, cfg!, и macros.

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

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

#[cfg(some_condition)]
fn conditional_function() {
    println!("condition met!");
}

fn main() {
    conditional_function();
}

Попробуйте запустить без указания флага cfg.

С указанием флага cfg:

$ rustc --cfg some_condition custom.rs && ./custom
condition met!

Обобщения

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

Обобщить параметр типа можно используя угловые скобки и верхний верблюжий регистр: <Aaa, Bbb, ...>. "Обобщённые параметры типа" обычно представлены как <T>. В Rust, "обобщённым" также принято называть все, что может принимать один или более обобщённых параметров типа <T>. Любой тип, указанный в качестве параметра обобщённого типа, является обобщённым, а всё остальное является конкретным (не обобщённым).

Например, объявление обобщённой функции foo принимающей аргумент T любого типа:

fn foo<T>(arg: T) { ... }

Поскольку T был объявлен как обобщённый тип, посредством <T>, он считается обобщённым когда используется как (arg: T). Это работает даже если T был определён как структура.

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

// Конкретный тип `A`.
struct A;

// В определении типа `Single` первому использованию `A` не предшествует `<A>`.
// Поэтому `Single` имеет конкретный тип, и `A` определена выше.
struct Single(A);
//            ^ Здесь `A` в первый раз используется в `Single`.

// В данном примере, `<T>` предшествует первому использованию `T`,
// поэтому `SingleGen` является обобщённым типом.
// Поскольку тип параметра `T` является обобщённым, он может быть чем угодно, включая
// конкретный тип `A`, определённый выше.
struct SingleGen<T>(T);

fn main() {
    // `Single` имеет конкретный тип и явно принимает параметр `A`.
    let _s = Single(A);

    // Создаём переменную `_char` типа `SingleGen<char>`
    // и присваиваем ей значение `SingleGen('a')`.
    // В примере ниже, тип параметра `SingleGen` явно определён.
    let _char: SingleGen<char> = SingleGen('a');

    // Здесь, `SingleGen` также может иметь неявно определённый параметр типа:
    let _t    = SingleGen(A); // Используется структура `A`, объявленная выше.
    let _i32  = SingleGen(6); // Используется `i32`.
    let _char = SingleGen('a'); // Используется `char`.
}

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

Структуры

Функции

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

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

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

struct A; // Конкретный тип `A`.
struct S(A); // Конкретный тип `S`.
struct SGen<T>(T); // Обобщённый тип `SGen`.

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

// Объявляем функцию `reg_fn`, которая принимает аргумент `_s` типа `S`.
// Здесь отсутствует `<T>`, поэтому это не обобщённая функция.
fn reg_fn(_s: S) {}

// Объявляем функцию `gen_spec_t`, которая принимает аргумент `_s` типа `SGen<T>`.
// В ней явно задан параметр типа `A`, но поскольку `A` не был указан
// как параметр обобщённого типа для `gen_spec_t`, то он не является обобщённым.
fn gen_spec_t(_s: SGen<A>) {}

// Объявляем функцию `gen_spec_i32`, которая принимает аргумент `_s` типа `SGen<i32>`.
// В ней явно задан тип `i32`, который является определённым типом.
// Поскольку `i32` не является обобщённым типом, эта функция
// также не является обобщённой.
fn gen_spec_i32(_s: SGen<i32>) {}

// Объявляем функцию `generic`, которая принимает аргумент `_s` типа `SGen<T>`.
// Поскольку `SGen<T>` предшествует `<T>`, эта функция
// является обобщённой над `T`.
fn generic<T>(_s: SGen<T>) {}

fn main() {
    // Используем не обобщённые функции.
    reg_fn(S(A)); // Конкретный тип.
    gen_spec_t(SGen(A)); // Неявно определён тип параметра `A`.
    gen_spec_i32(SGen(6)); // Неявно определён тип параметра `i32`.

    // Явно определён тип параметра `char` в `generic()`.
    generic::<char>(SGen('a'));

    // Неявно определён параметр типа `char` в `generic()`.
    generic(SGen('c'));
}

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

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

Реализация

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


#![allow(unused)]
fn main() {
struct S; // Конкретный тип `S`
struct GenericVal<T>(T); // Обобщенный тип `GenericVal`

// Реализация GenericVal, где мы явно указываем типы данных параметров:
impl GenericVal<f32> {} // Указываем тип `f32`
impl GenericVal<S> {} // Указываем тип `S`, который мы определили выше

// `<T>` должен указываться перед типом, чтобы оставаться обобщённым
impl<T> GenericVal<T> {}
}
struct Val {
    val: f64,
}

struct GenVal<T> {
    gen_val: T,
}

// Реализация Val
impl Val {
    fn value(&self) -> &f64 {
        &self.val
    }
}

// Реализация GenVal для обобщённого типа `T`
impl<T> GenVal<T> {
    fn value(&self) -> &T {
        &self.gen_val
    }
}

fn main() {
    let x = Val { val: 3.0 };
    let y = GenVal { gen_val: 3i32 };

    println!("{}, {}", x.value(), y.value());
}

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

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

Типажи

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

// Некопируемые типы.
struct Empty;
struct Null;

// Обобщённый типаж от `T`.
trait DoubleDrop<T> {
    // Определим метод для типа вызывающего объекта,
    // который принимает один дополнительный параметр `T` и ничего с ним не делает.
    fn double_drop(self, _: T);
}

// Реализация `DoubleDrop<T>` для любого общего параметра `T` и
// вызывающего объекта `U`.
impl<T, U> DoubleDrop<T> for U {
    // Этот метод получает право владения на оба переданных аргумента и
    // освобождает их.
    fn double_drop(self, _: T) {}
}

fn main() {
    let empty = Empty;
    let null  = Null;

    // Освободить `empty` и `null`.
    empty.double_drop(null);

    //empty;
    //null;
    // ^ TODO: Попробуйте раскомментировать эти строки.
}

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

Drop, struct и trait

Ограничения

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

// Определим функцию `printer`, которая принимает обобщённый тип `T`,
// который должен реализовать типаж `Display`
fn printer<T: Display>(t: T) {
    println!("{}", t);
}

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

struct S<T: Display>(T);

// Ошибка! `Vec<T>` не реализует `Display`. Эта
// специализация не удастся
let s = S(vec![1]);

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

// Типаж, который реализует маркер печати: `{:?}`.
use std::fmt::Debug;

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for Rectangle {
    fn area(&self) -> f64 { self.length * self.height }
}

#[derive(Debug)]
struct Rectangle { length: f64, height: f64 }
#[allow(dead_code)]
struct Triangle  { length: f64, height: f64 }

// Обобщённый тип `T` должен реализовать `Debug`. Независимо
// от типа, это будет работать правильно.
fn print_debug<T: Debug>(t: &T) {
    println!("{:?}", t);
}

// `T` должен реализовать `HasArea`. Любая функция, которая удовлетворяет
// ограничению может получить доступ к функции `area` из `HasArea`.
fn area<T: HasArea>(t: &T) -> f64 { t.area() }

fn main() {
    let rectangle = Rectangle { length: 3.0, height: 4.0 };
    let _triangle = Triangle  { length: 3.0, height: 4.0 };

    print_debug(&rectangle);
    println!("Area: {}", area(&rectangle));

    //print_debug(&_triangle);
    //println!("Area: {}", area(&_triangle));
    // ^ TODO: Попробуйте раскомментировать эти строки.
    // | Ошибка: Не реализован `Debug` или `HasArea`.
}

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

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

std::fmt, struct и trait

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

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

struct Cardinal;
struct BlueJay;
struct Turkey;

trait Red {}
trait Blue {}

impl Red for Cardinal {}
impl Blue for BlueJay {}

// Эти функции действительны только для типов реализующих эти типажи.
// То, что типажи пусты, не имеет значения.
fn red<T: Red>(_: &T)   -> &'static str { "красная" }
fn blue<T: Blue>(_: &T) -> &'static str { "синяя" }

fn main() {
    let cardinal = Cardinal;
    let blue_jay = BlueJay;
    let _turkey   = Turkey;

    // `red()` не будет работать для blue_jay, ни наоборот,
    // из-за ограничений по трейту.
    println!("Кардинал {} птица", red(&cardinal));
    println!("Голубая сойка {} птица", blue(&blue_jay));
    //println!("Индюк {} птица", red(&_turkey));
    // ^ TODO: Попробуйте раскомментировать эту строку.
}

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

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

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

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

use std::fmt::{Debug, Display};

fn compare_prints<T: Debug + Display>(t: &T) {
    println!("Debug: `{:?}`", t);
    println!("Display: `{}`", t);
}

fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
    println!("t: `{:?}", t);
    println!("u: `{:?}", u);
}

fn main() {
    let string = "words";
    let array = [1, 2, 3];
    let vec = vec![1, 2, 3];

    compare_prints(&string);
    //compare_prints(&array);
    // ЗАДАНИЕ ^ Попробуйте удалить комментарий.

    compare_types(&array, &vec);
}

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

std::fmt и trait

Утверждения where

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

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

  • При указании обобщённых типов и ограничений типажей отдельно, код становится более ясным:
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

// Выражение ограничений типажей через утверждение `where`
impl <A, D> MyTrait<A, D> for YourType where
    A: TraitB + TraitC,
    D: TraitE + TraitF {}
  • Использование утверждения where более выразительно, чем использование обычного синтаксиса. В этом примере impl не может быть непосредственно выражен без утверждения where:
use std::fmt::Debug;

trait PrintInOption {
    fn print_in_option(self);
}

// Потому что в противном случае мы должны были бы выразить это как
// `T: Debug` или использовать другой метод косвенного подхода,
// для этого требуется утверждение `where`:
impl<T> PrintInOption for T where
    Option<T>: Debug {
    // Мы хотим использовать `Option<T>: Debug` как наше ограничение
    // типажа, потому то это то, что будет напечатано. В противном случае
    // использовалось бы неправильное ограничение типажа.
    fn print_in_option(self) {
        println!("{:?}", Some(self));
    }
}

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

    vec.print_in_option();
}

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

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

New Type идиома

Идиома newtype гарантирует во время компиляции, что программе передаётся значение правильного типа.

Например, функция верификации возраста, которая проверяет возраст в годах должна получать значение типа Years.

struct Years(i64);

struct Days(i64);

impl Years {
    pub fn to_days(&self) -> Days {
        Days(self.0 * 365)
    }
}


impl Days {
    /// truncates partial years
    pub fn to_years(&self) -> Years {
        Years(self.0 / 365)
    }
}

fn old_enough(age: &Years) -> bool {
    age.0 >= 18
}

fn main() {
    let age = Years(5);
    let age_days = age.to_days();
    println!("Old enough {}", old_enough(&age));
    println!("Old enough {}", old_enough(&age_days.to_years()));
    // println!("Old enough {}", old_enough(&age_days));
}

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

Чтобы получить из newtype-переменной значение базового типа, вы можете использовать кортежный синтаксис, как в примере:

struct Years(i64);

fn main() {
    let years = Years(42);
    let years_as_primitive: i64 = years.0;
}

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

struct

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

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

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

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

RFC

Проблема

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

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

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

struct Container(i32, i32);

// Типаж, который проверяет, сохранены ли 2 элемента в контейнере.
// Также он может вернуть первое или последнее значение.
trait Contains<A, B> {
    fn contains(&self, _: &A, _: &B) -> bool; // Явно требует `A` и `B`.
    fn first(&self) -> i32; // Не требует явного `A` или `B`.
    fn last(&self) -> i32;  // Не требует явного `A` или `B`.
}

impl Contains<i32, i32> for Container {
    // Истина, если сохранённые цифры равны.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }

    // Берём первую цифру.
    fn first(&self) -> i32 { self.0 }

    // Берём последнюю цифру.
    fn last(&self) -> i32 { self.1 }
}

// `C` содержит `A` и `B`. В свете этого, необходимость снова явно указывать `A` и
// `B` огорчает.
fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container(number_1, number_2);

    println!("Содержатся ли в контейнере {} и {}? {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("Первое число: {}", container.first());
    println!("Последнее число: {}", container.last());

    println!("Разница: {}", difference(&container));
}

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

struct и trait

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

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


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

    // Обновлённый синтаксис для обращения к этим двум ассоциированным типам.
    fn contains(&self, &Self::A, &Self::B) -> bool;
}
}

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

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

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

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

struct Container(i32, i32);

// Типаж, который проверяет, сохранены ли 2 элемента в контейнере.
// Также он может вернуть первое или последнее значение.
trait Contains {
    // Объявляем общие типы, которые будут использовать методы.
    type A;
    type B;

    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
    fn first(&self) -> i32;
    fn last(&self) -> i32;
}

impl Contains for Container {
    // Определяем, какими будут типы `A` и `B`. Если `входящий` тип
    // `Container(i32, i32)`, тогда `выходящие` типы определяются, как
    // `i32` и `i32`.
    type A = i32;
    type B = i32;

    // `&Self::A` и `&Self::B` также будут здесь уместны.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }
    // Берём первую цифру.
    fn first(&self) -> i32 { self.0 }

    // Берём последнюю цифру.
    fn last(&self) -> i32 { self.1 }
}

fn difference<C: Contains>(container: &C) -> i32 {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container(number_1, number_2);

    println!("Содержатся ли в контейнере {} и {}: {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("Первое число: {}", container.first());
    println!("Последнее число: {}", container.last());

    println!("Разница: {}", difference(&container));
}

PhantomData-параметры

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

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

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

use std::marker::PhantomData;

// Фантомная кортежная структура, которая имеет обобщение `A` со скрытым параметром `B`.
#[derive(PartialEq)] // Разрешаем для данного типа сравнения.
struct PhantomTuple<A, B>(A,PhantomData<B>);

// Фантомная структура, которая имеет обобщение `A` со скрытым параметром `B`.
#[derive(PartialEq)] // Разрешаем для данного типа сравнения.
struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }

// Заметьте: память выделена для обобщённого типа `A`, но не для `B`.
//           Следовательно, `B` не может быть использована в вычислениях.

fn main() {
    // Здесь `f32` и `f64` - скрытые параметры.
    // Тип PhantomTuple объявлен с `<char, f32>`.
    let _tuple1: PhantomTuple<char, f32> = PhantomTuple('Q', PhantomData);
    // Тип PhantomTuple объявлен с `<char, f64>`.
    let _tuple2: PhantomTuple<char, f64> = PhantomTuple('Q', PhantomData);

    // Тип определён как `<char, f32>`.
    let _struct1: PhantomStruct<char, f32> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };
    // Тип определён как `<char, f64>`.
    let _struct2: PhantomStruct<char, f64> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };
    
    // Ошибка времени компиляции! Типы не совпадают, так что сравнение не может быть произведено:
    //println!("_tuple1 == _tuple2 даёт в результате: {}",
    //          _tuple1 == _tuple2);
    
    // Ошибка времени компиляции! Типы не совпадают, так что сравнение не может быть произведено:
    //println!("_struct1 == _struct2 даёт в результате: {}",
    //          _struct1 == _struct2);
}

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

derive, struct и кортежные структуры

Пример: unit clarification

Полезный метод преобразования единиц измерения может быть получен путём реализации типажа Add с параметром фантомного типа. trait``Add рассмотрен ниже:

// Эта конструкция будет навязывать: `Self + RHS = Output`
// где RHS по умолчанию Self, если иное не указано в реализации.
pub trait Add<RHS = Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

// `Output` должен быть `T<U>` так что `T<U> + T<U> = T<U>`.
impl<U> Add for T<U> {
    type Output = T<U>;
    ...
}

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

use std::ops::Add;
use std::marker::PhantomData;

/// Создаём пустые перечисления для определения типов единиц измерения.
#[derive(Debug, Clone, Copy)]
enum Inch {}
#[derive(Debug, Clone, Copy)]
enum Mm {}

/// `Length` - тип с параметром фантомного типа `Unit`,
/// и не обобщён для типа длины (который `f64`).
///
/// Для `f64` уже реализованы типажи `Clone` и `Copy`.
#[derive(Debug, Clone, Copy)]
struct Length<Unit>(f64, PhantomData<Unit>);

/// Типаж `Add` объявляет поведение оператора `+`.
impl<Unit> Add for Length<Unit> {
     type Output = Length<Unit>;

    // add() возвращает новую структуру `Length`, содержащую сумму.
    fn add(self, rhs: Length<Unit>) -> Length<Unit> {
        // `+` вызывает реализацию `Add` для `f64`.
        Length(self.0 + rhs.0, PhantomData)
    }
}

fn main() {
    // Объявим, что `one_foot` имеет парамет фантомного типа `Inch`.
    let one_foot:  Length<Inch> = Length(12.0, PhantomData);
    // `one_meter` имеет параметр фантомного типа `Mm`.
    let one_meter: Length<Mm>   = Length(1000.0, PhantomData);

    // `+` вызывает метод `add()`, который мы реализовали для `Length<Unit>`.
    //
    // Так как `Length` реализует `Copy`, `add()` не поглощает
    // `one_foot` и `one_meter`, а копирует их в `self` и `rhs`.
    let two_feet = one_foot + one_foot;
    let two_meters = one_meter + one_meter;

    // Сложение работает.
    println!("один фут + один фут = {:?} фута", two_feet.0);
    println!("один метр + один метр = {:?} метра", two_meters.0);

    // Бессмысленные операции потерпят неудачу, как и должно быть:
    // Ошибка времени компиляции: несоответствие типов.
    //let one_feter = one_foot + one_meter;
}

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

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

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

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

RAII

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

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

// raii.rs
fn create_box() {
    // Выделить память для целого число в куче
    let _box1 = Box::new(3i32);

    // `_box1` здесь уничтожается, а память освобождается
}

fn main() {
    // Выделить память для целого числа в куче
    let _box2 = Box::new(5i32);

    // Вложенная область видимости:
    {
        // Выделить память для ещё одного целого числа в куче
        let _box3 = Box::new(4i32);

        // `_box3` здесь уничтожается, а память освобождается
    }

    // Создаём большое количество упаковок. Просто потому что можем.
    // Здесь нет необходимости освобождать память вручную!
    for _ in 0u32..1_000 {
        create_box();
    }

    // `_box2` здесь уничтожается, а память освобождается
}

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

$ rustc raii.rs && valgrind ./raii
==26873== Memcheck, a memory error detector
==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==26873== Command: ./raii
==26873==
==26873==
==26873== HEAP SUMMARY:
==26873==     in use at exit: 0 bytes in 0 blocks
==26873==   total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated
==26873==
==26873== All heap blocks were freed -- no leaks are possible
==26873==
==26873== For counts of detected and suppressed errors, rerun with: -v
==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

Утечки отсутствуют!

Деструктор

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

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

struct ToDrop;

impl Drop for ToDrop {
    fn drop(&mut self) {
        println!("ToDrop is being dropped");
    }
}

fn main() {
    let x = ToDrop;
    println!("Made a ToDrop!");
}

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

Упаковка

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

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

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

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

// Эта функция берёт во владение память, выделенную в куче
fn destroy_box(c: Box<i32>) {
    println!("Уничтожаем упаковку, в которой хранится {}", c);

    // `c` уничтожится, а память будет освобождена
}

fn main() {
    // Целое число выделенное в стеке
    let x = 5u32;

    // *Копируем* `x` в `y`. В данном случае нет ресурсов для перемещения
    let y = x;

    // Оба значения можно использовать независимо
    println!("x равен {}, а y равен {}", x, y);

    // `a` - указатель на целое число, выделенное в куче
    let a = Box::new(5i32);

    println!("a содержит: {}", a);

    // *Перемещаем* `a` в `b`
    let b = a;
    // Адрес указателя `a` копируется (но не данные) в `b`.
    // Оба указателя указывают на одни и те же данные в куче, но
    // `b` теперь владеет ими.

    // Ошибка! `a` больше не может получить доступ к данным, потому что
    // больше не владеет данными в куче.
    //println!("a содержит: {}", a);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Эта функция берет во владение память, выделенную в куче, которой ранее владела `b`
    destroy_box(b);

    // Поскольку в данный момент память в куче уже освобождена, это действие
    // приведёт к разыменованию освобождённой памяти, но это запрещено компилятором
    // Ошибка! Причина та же, что и в прошлый раз
    //println!("b содержит: {}", b);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку
}

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

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

fn main() {
    let immutable_box = Box::new(5u32);

    println!("immutable_box содержит в себе {}", immutable_box);

    // Ошибка изменяемости
    //*immutable_box = 4;

    // *Переместить* упаковку, изменив её владение (и изменяемость)
    let mut mutable_box = immutable_box;

    println!("mutable_box содержит в себе {}", mutable_box);

    // Изменяем данные внутри упаковки
    *mutable_box = 4;

    println!("mutable_box now содержит в себе {}", mutable_box);
}

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

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

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

// Эта функция берёт во владение упаковку и уничтожает её
fn eat_box_i32(boxed_i32: Box<i32>) {
    println!("Уничтожаем упаковку в которой хранится {}", boxed_i32);
}

// Эта функция заимствует i32
fn borrow_i32(borrowed_i32: &i32) {
    println!("Это число равно: {}", borrowed_i32);
}

fn main() {
    // Создаём упакованное i32, и i32 на стеке
    let boxed_i32 = Box::new(5_i32);
    let stacked_i32 = 6_i32;

    // Заимствуем содержимое упаковки. При этом мы не владеем ресурсом.
    // Содержимое может быть заимствовано снова.
    borrow_i32(&boxed_i32);
    borrow_i32(&stacked_i32);

    {
        // Получаем ссылку на данные, которые хранятся внутри упаковки
        let _ref_to_i32: &i32 = &boxed_i32;

        // Ошибка!
        // Нельзя уничтожать упаковку `boxed_i32` пока данные внутри заимствованы.
        eat_box_i32(boxed_i32);
        // ИСПРАВЬТЕ ^ Закомментируйте эту строку

        // `_ref_to_i32` покидает область видимости и больше не является заимствованным ресурсом.
    }

    // `boxed_i32` теперь может получить владение над `eat_box` и быть уничтожено
    eat_box_i32(boxed_i32);
}

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

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

#[allow(dead_code)]
#[derive(Clone, Copy)]
struct Book {
    // `&'static str` - это ссылка на строку, расположенную в неизменяемой памяти
    author: &'static str,
    title: &'static str,
    year: u32,
}

// Эта функция получает ссылку на книгу
fn borrow_book(book: &Book) {
    println!("Я неизменяемо заимствовала {} - {} издания", book.title, book.year);
}

// Эта функция получает изменяемую ссылку на книгу и устанавливает поле `year` в 2014
fn new_edition(book: &mut Book) {
    book.year = 2014;
    println!("Я изменяемо заимствовала {} - {} издания", book.title, book.year);
}

fn main() {
    // Создаём неизменяемую книгу в переменной `immutabook`
    let immutabook = Book {
        // строковый литерал имеет тип `&'static str`
        author: "Douglas Hofstadter",
        title: "Gödel, Escher, Bach",
        year: 1979,
    };

    // Создаём изменяемую копию `immutabook` и называем её `mutabook`
    let mut mutabook = immutabook;
    
    // Неизменяемое заимствование неизменяемого объекта
    borrow_book(&immutabook);

    // Неизменяемое заимствование изменяемого объекта
    borrow_book(&mutabook);
    
    // Заимствование изменяемого объекта как изменяемого
    new_edition(&mut mutabook);
    
    // Ошибка! Нельзя заимствовать неизменяемый объект как изменяемый
    new_edition(&mut immutabook);
    // ИСПРАВЬТЕ ^ Добавьте комментарий для этой строки
}

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

static

Алиасинг

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

struct Point { x: i32, y: i32, z: i32 }

fn main() {
    let mut point = Point { x: 0, y: 0, z: 0 };

    let borrowed_point = &point;
    let another_borrow = &point;

    // Данные могут быть доступны через ссылки и владельца этих данных
    println!("Точка имеет координаты: ({}, {}, {})",
              borrowed_point.x, another_borrow.y, point.z);

    // Ошибка! Нельзя заимствовать для изменения `point`, так как она уже
    // существует неизменяемая ссылка.
    //let mutable_borrow = &mut point;
    // TODO ^ Попробуйте раскомментировать эту строку

    // Заимствованное значение снова используется
    println!("Точка имеет координаты: ({}, {}, {})",
                borrowed_point.x, another_borrow.y, point.z);

    // Неизменяемая ссылка больше не используется, так что можно перезаимствовать её
    // с помощью изменяемой ссылки.
    let mutable_borrow = &mut point;

    // Меняем при помощи изменяемой ссылки
    mutable_borrow.x = 5;
    mutable_borrow.y = 2;
    mutable_borrow.z = 1;

    // Ошибка! Нельзя неизменяемо заимствовать `point` так как она уже
    // заимствована изменяемо.
    //let y = &point.y;
    // TODO ^ Попробуйте раскомментировать эту строку

    // Ошибка! Нельзя вывести на экран, потому что `println!` берёт неизменяемую ссылку.
    //println!("Координата Z {}", point.z);
    // TODO ^ Попробуйте раскомментировать эту строку

    // Ok! Изменяемая ссылка может быть передана `println!` как неизменяемая
    println!("Точка имеет координаты: ({}, {}, {})",
              mutable_borrow.x, mutable_borrow.y, mutable_borrow.z);

    // Изменяемая ссылка больше не используется, так что можно перезаимствовать
    let new_borrowed_point = &point;
    println!("Точка имеет координаты: ({}, {}, {})",
              new_borrowed_point.x, new_borrowed_point.y, new_borrowed_point.z);
}

ref паттерн

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

#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }

fn main() {
    let c = 'Q';

    // Заимствование с `ref` по левую сторону от присваивания, эквивалетно
    // заимствованию с `&` по правую сторону.
    let ref ref_c1 = c;
    let ref_c2 = &c;

    println!("ref_c1 равно ref_c2: {}", *ref_c1 == *ref_c2);

    let point = Point { x: 0, y: 0 };

    // `ref` также может использоваться при деструктуризации структур.
    let _copy_of_x = {
        // `ref_to_x` - ссылка на поле `x` в `point`.
        let Point { x: ref ref_to_x, y: _ } = point;

        // Возвращаем копию поля `x` из `point`.
        *ref_to_x
    };

    // Изменяемая копия `point`
    let mut mutable_point = point;

    {
        // `ref` может использоваться вместе с `mut` для получения изменяемой ссылки.
        let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;

        // Изменяем поле `y` переменной `mutable_point` через изменяемую ссылку.
        *mut_ref_to_y = 1;
    }

    println!("point ({}, {})", point.x, point.y);
    println!("mutable_point ({}, {})", mutable_point.x, mutable_point.y);

    // Изменяемый кортеж с указателем
    let mut mutable_tuple = (Box::new(5u32), 3u32);
    
    {
        // Деструктурируем `mutable_tuple` чтобы изменить значение `last`.
        let (_, ref mut last) = mutable_tuple;
        *last = 2u32;
    }
    
    println!("tuple {:?}", mutable_tuple);
}

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

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

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

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

// Времена жизни аннотированы линиями, обозначающими
// создание и уничтожение каждой переменной.
// `i` имеет самое длинное время жизни, так как его область охватывает
// полностью оба заимствования `borrow1` и `borrow2`.
// Продолжительность заимствования `borrow1` по сравнению с
// заимствованием `borrow2` не имеет значения, так как они не пересекаются.
fn main() {
    let i = 3; // Lifetime for `i` starts. ────────────────┐
    //                                                     │
    { //                                                   │
        let borrow1 = &i; // `borrow1` lifetime starts. ──┐│
        //                                                ││
        println!("borrow1: {}", borrow1); //              ││
    } // `borrow1 ends. ──────────────────────────────────┘│
    //                                                     │
    //                                                     │
    { //                                                   │
        let borrow2 = &i; // `borrow2` lifetime starts. ──┐│
        //                                                ││
        println!("borrow2: {}", borrow2); //              ││
    } // `borrow2` ends. ─────────────────────────────────┘│
    //                                                     │
}   // Lifetime ends. ─────────────────────────────────────┘

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

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

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

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

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

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

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

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

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

// `print_refs` получает две ссылки на `i32`, имеющие различные
// времена жизни `'a` и `'b`. Оба этих времени жизни должны существовать
// не меньше, чем функция `print_refs`.
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x равно {} и y равно {}", x, y);
}

// Функция, не имеющая аргументов, но имеющая параметр времени жизни `'a`.
fn failed_borrow<'a>() {
    let _x = 12;

    // ОШИБКА: `_x` не живёт достаточно долго (`_x` does not live long enough)
    //let y: &'a i32 = &_x;
    // Попытка использования времени жизни `'a` для явного аннотирования
    // внутри функции приведёт к ошибке, так как время жизни у `&_x` короче, чем
    // у `y`. Короткое время жизни не может быть приведено к длинному.
}

fn main() {
    // Создадим переменные, которые далее будут заимствованы.
    let (four, nine) = (4, 9);
    
    // Заимствуем (`&`) обе переменные и передадим их в функцию.
    print_refs(&four, &nine);
    // Любой ввод, который заимствуется, должен жить дольше, чем заимствующий.
    // Другими словами, время жизни `four` и `nine` должно
    // быть больше, чем время жизни `print_refs`.
    
    failed_borrow();
    // `failed_borrow` не содержит ссылок, заставляющих `'a` быть
    // больше, чем время жизни функции, но `'a` больше.
    // Поскольку время жизни никогда не ограничено, оно, по умолчанию, равно `'static`.
}
1

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

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

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

Функции

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

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

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

// Одна входная ссылка со временем жизни `'a`, которая
// будет жить как минимум до конца функции.
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

// Использование времени жизни также возможно с изменяемыми ссылками.
fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

// Несколько элементов с различными временами жизни. В этом случае
// было бы хорошо, чтобы у обоих ссылок было одно время жизни `'a`,
// в более сложных случаях может потребоваться различное время жизни.
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// Возврат переданных на вход ссылок допустим.
// Однако должен быть указано правильное время жизни.
fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

//fn invalid_output<'a>() -> &'a String { &String::from("foo") }
// Код написанный выше является недопустимым: время жизни `'a`
// должно жить после выхода из функции.
// Здесь, `&String::from("foo")` создает ссылку на `String`
// Данные будут удалены после выхода из области видимости, и
// будет возвращена ссылка на недопустимые данные.

fn main() {
    let x = 7;
    let y = 9;

    print_one(&x);
    print_multi(&x, &y);

    let z = pass_x(&x, &y);
    print_one(z);

    let mut t = 3;
    add_one(&mut t);
    print_one(&t);
}

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

функции

Методы

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

struct Owner(i32);

impl Owner {
    // Время жизни аннотируется как в отдельной функции.
    fn add_one<'a>(&'a mut self) { self.0 += 1; }
    fn print<'a>(&'a self) {
        println!("`print`: {}", self.0);
    }
}

fn main() {
    let mut owner = Owner(18);

    owner.add_one();
    owner.print();
}

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

Методы

Структуры

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

// Тип `Borrowed`, в котором находится ссылка на `i32`.
// Ссылка на `i32` должна пережить `Borrowed`.
#[derive(Debug)]
struct Borrowed<'a>(&'a i32);

// Аналогично, обе ссылки расположенные здесь, должны пережить эту структуру.
#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

// Перечисление, которое указывает на `i32` или на ссылку.
#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let x = 18;
    let y = 15;

    let single = Borrowed(&x);
    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number = Either::Num(y);

    println!("x заимствован в {:?}", single);
    println!("x и y заимствованы в {:?}", double);
    println!("x заимствован в {:?}", reference);
    println!("y *не* заимствован в {:?}", number);
}

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

Структуры

Типажи

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

// Структура с аннотированным временем жизни.
#[derive(Debug)]
 struct Borrowed<'a> {
     x: &'a i32,
 }

// Аннотированное время жизни для реализации.
impl<'a> Default for Borrowed<'a> {
    fn default() -> Self {
        Self {
            x: &10,
        }
    }
}

fn main() {
    let b: Borrowed = Default::default();
    println!("b равно {:?}", b);
}

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

trait

Ограничения

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

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

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

use std::fmt::Debug; // Типаж с ограничениями.

#[derive(Debug)]
struct Ref<'a, T: 'a>(&'a T);
// `Ref` содержит ссылки на обобщённый тип `T` который имеет
// неизвестное время жизни `'a`. `T` ограничен так, что любые
// *ссылки* в `T` должны пережить `'a`.
// Кроме того, время жизни `Ref` не может превышать `'a`.

// Обобщённая функция, которая показывает использование типажа `Debug`.
fn print<T>(t: T) where
    T: Debug {
    println!("`print`: t это {:?}", t);
}

// Здесь приводится ссылка на `T`, где `T` реализует
// `Debug` и все *ссылки* в `T` переживают `'a`.
// К тому же, `'a` должен пережить функцию.
fn print_ref<'a, T>(t: &'a T) where
    T: Debug + 'a {
    println!("`print_ref`: t это {:?}", t);
}

fn main() {
    let x = 7;
    let ref_x = Ref(&x);

    print_ref(&ref_x);
    print(ref_x);
}

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

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

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

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

// Здесь Rust выводит наиболее короткое время жизни.
// Затем обе ссылки приводятся к этому времени жизни.
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `<'a: 'b, 'b>` читается как "время жизни `'a` не меньше, чем время жизни `'b`".
// Здесь мы получаем  `&'a i32` и в результате приведения возвращаем `&'b i32`.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Более длинное время жизни
    
    {
        let second = 3; // Более короткое время жизни
        
        println!("Произведение равно {}", multiply(&first, &second));
        println!("{} первое", choose_first(&first, &second));
    };
}

Static

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


#![allow(unused)]
fn main() {
// Ссылка с временем жизни 'static:
let s: &'static str = "hello world";

// 'static как часть ограничения типажа:
fn generic<T>(x: T) where T: 'static {}
}

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

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

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

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

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

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

// Создадим константу со временем жизни `'static`.
static NUM: i32 = 18;

// Вернём ссылку на `NUM`, у которой собственное время жизни `'static`
// приводится ко времени жизни аргумента.
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

fn main() {
    {
        // Создадим *строковый* литерал и выведем его:
        let static_string = "Я в неизменяемой памяти";
        println!("static_string: {}", static_string);

        // Когда `static_string` выходит из области видимости, ссылка
        // на неё больше не может быть использована, но данные остаются в бинарном файле.
    }
    
    {
        // Создадим число для использования в `coerce_static`:
        let lifetime_num = 9;

        // Приведём `NUM` ко времени жизни `lifetime_num`:
        let coerced_static = coerce_static(&lifetime_num);

        println!("coerced_static: {}", coerced_static);
    }
    
    println!("NUM: {} остаётся доступным!", NUM);
}

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

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

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

use std::fmt::Debug;

fn print_it( input: impl Debug + 'static ) {
    println!( "Переданное значение 'static равно: {:?}", input );
}

fn main() {
    // I владеемое и не имеет ссылок, следовательно является 'static:
    let i = 5;
    print_it(i);

    // Упс, &I имеет время жизни, ограниченное областью видимости
    // main(), поэтому оно не 'static:
    print_it(&i);
}

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

error[E0597]: `i` does not live long enough
  --> src/lib.rs:15:15
   |
15 |     print_it(&i);
   |     ---------^^--
   |     |         |
   |     |         borrowed value does not live long enough
   |     argument requires that `i` is borrowed for `'static`
16 | }
   | - `i` dropped here while still borrowed

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

'static константы

Сокрытие

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

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

// По существу, `elided_input` и `annotated_input` имеют одинаковую сигнатуру
// потому что время жизни `elided_input` выводится компилятором:
fn elided_input(x: &i32) {
    println!("`elided_input`: {}", x);
}

fn annotated_input<'a>(x: &'a i32) {
    println!("`annotated_input`: {}", x);
}

// Аналогично, `elided_pass` и `annotated_pass` имеют идентичные сигнатуры
// потому что время жизни неявно добавлено к `elided_pass`:
fn elided_pass(x: &i32) -> &i32 { x }

fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }

fn main() {
    let x = 3;

    elided_input(&x);
    annotated_input(&x);

    println!("`elided_pass`: {}", elided_pass(&x));
    println!("`annotated_pass`: {}", annotated_pass(&x));
}

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

сокрытие

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

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

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

struct Sheep { naked: bool, name: &'static str }

trait Animal {
    // Сигнатура присоединённой функции; `Self` ссылается на тип, реализующий типаж.
    fn new(name: &'static str) -> Self;

    // Сигнатуры методов; эти вернут строку.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Типажи могут предоставлять для методов реализацию по умолчанию.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

impl Sheep {
    fn is_naked(&self) -> bool {
        self.naked
    }

    fn shear(&mut self) {
        if self.is_naked() {
            // Тип, реализующий типаж, может использовать другие методы этого же типа.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);

            self.naked = true;
        }
    }
}

// Реализуем типаж `Animal` для структуры `Sheep`.
impl Animal for Sheep {
    // `Self` - это сам тип, реализующий типаж: `Sheep`.
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }
    
    // Можно переопределить методы по умолчанию, заданные в типаже.
    fn talk(&self) {
        // Например, мы можем немного тихо помедитировать.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

fn main() {
    // В этом случае необходима аннотация типа.
    let mut dolly: Sheep = Animal::new("Dolly");
    // TODO ^ Попробуйте убрать аннотацию типа.

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Атрибут Derive

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

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

  • Типажи сравнения:Eq, PartialEq, Ord, PartialOrd
  • Clone, для создания T из &T с помощью копии.
  • Copy, чтобы создать тип семантикой копирования, вместо семантики перемещения.
  • Hash, чтобы вычислить хеш из &T.
  • Default, чтобы создать пустой экземпляр типа данных.
  • Debug, чтобы отформатировать значение с помощью {:?}.
// `Centimeters`, кортежная структура, которую можно сравнить
#[derive(PartialEq, PartialOrd)]
struct Centimeters(f64);

// `Inches`, кортежная структура, которую можно напечатать
#[derive(Debug)]
struct Inches(i32);

impl Inches {
    fn to_centimeters(&self) -> Centimeters {
        let &Inches(inches) = self;

        Centimeters(inches as f64 * 2.54)
    }
}

// `Seconds`, кортежная структура без дополнительных атрибутов
struct Seconds(i32);

fn main() {
    let _one_second = Seconds(1);

    // Ошибка: `Seconds` не может быть напечатана; не реализован типаж `Debug`
    //println!("Одна секунда выглядит как: {:?}", _one_second);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Ошибка: `Seconds` нельзя сравнить; не реализован типаж `PartialEq`
    //let _this_is_true = (_one_second == _one_second);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    let foot = Inches(12);

    println!("Один фут равен {:?}", foot);

    let meter = Centimeters(100.0);

    let cmp =
        if foot.to_centimeters() < meter {
            "меньше"
        } else {
            "больше"
        };

    println!("Один фут {} одного метра.", cmp);
}

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

derive

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

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

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

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

struct Sheep {}
struct Cow {}

trait Animal {
    // Сигнатура метода объекта
    fn noise(&self) -> &'static str;
}

// Реализуем типаж `Animal` для `Sheep`.
impl Animal for Sheep {
    fn noise(&self) -> &'static str {
        "baaaaah!"
    }
}

// Реализуем типаж `Animal` для `Cow`.
impl Animal for Cow {
    fn noise(&self) -> &'static str {
        "moooooo!"
    }
}

// Вернём некоторую структуру, которая реализует `Animal`, но которая не известна в момент компиляции.
fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
        Box::new(Sheep {})
    } else {
        Box::new(Cow {})
    }
}

fn main() {
    let random_number = 0.234;
    let animal = random_animal(random_number);
    println!("Вы выбрали случайное животное и оно говорит {}", animal.noise());
}

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

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

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

use std::ops;

struct Foo;
struct Bar;

#[derive(Debug)]
struct FooBar;

#[derive(Debug)]
struct BarFoo;

// Типаж `std::ops::Add` используется для указания функциональности `+`.
// Здесь мы объявим `Add<Bar>` - типаж сложения, со вторым
// операндом типа `Bar`.
// Следующий блок реализует операцию: Foo + Bar = FooBar
impl ops::Add<Bar> for Foo {
    type Output = FooBar;

    fn add(self, _rhs: Bar) -> FooBar {
        println!("> Вызвали Foo.add(Bar)");

        FooBar
    }
}

// Если мы поменяем местами типы, то получим реализацию некоммутативного сложения.
// Здесь мы объявим `Add<Foo>` - типаж сложения, со вторым
// операндом типа `Foo`.
// Этот блок реализует операцию: Bar + Foo = BarFoo
impl ops::Add<Foo> for Bar {
    type Output = BarFoo;

    fn add(self, _rhs: Foo) -> BarFoo {
        println!("> Вызвали Bar.add(Foo)");

        BarFoo
    }
}

fn main() {
    println!("Foo + Bar = {:?}", Foo + Bar);
    println!("Bar + Foo = {:?}", Bar + Foo);
}

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

Add, Syntax Index

Типаж Drop

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

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

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

struct Droppable {
    name: &'static str,
}

// Это простая реализация `drop`, которая добавляет вывод в консоль.
impl Drop for Droppable {
    fn drop(&mut self) {
        println!("> Сбросили {}", self.name);
    }
}

fn main() {
    let _a = Droppable { name: "a" };

    // блок А
    {
        let _b = Droppable { name: "b" };

        // блок Б
        {
            let _c = Droppable { name: "c" };
            let _d = Droppable { name: "d" };

            println!("Выходим из блока Б");
        }
        println!("Вышли из блока Б");

        println!("Выходим из блока А");
    }
    println!("Вышли из блока А");

    // Переменную можно сбросить вручную с помощью функции `drop`.
    drop(_a);
    // ЗАДАНИЕ ^ Попробуйте закомментировать эту строку

    println!("Конец главной функции.");

    // *Нельзя* сбросить `_a` снова, потому что переменная уже
    // (вручную) сброшена.
}

Итераторы

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

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

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

struct Fibonacci {
    curr: u32,
    next: u32,
}

// Реализация `Iterator` для `Fibonacci`.
// Для реализации типажа `Iterator` требуется реализовать метод `next`.
impl Iterator for Fibonacci {
    type Item = u32;
    
    // Здесь мы определяем последовательность, используя `.curr` и `.next`.
    // Возвращаем тип `Option<T>`:
    //     * Когда в `Iterator` больше нет значений, будет возвращено `None`.
    //     * В противном случае следующее значение оборачивается в `Some` и возвращается.
    fn next(&mut self) -> Option<u32> {
        let new_next = self.curr + self.next;

        self.curr = self.next;
        self.next = new_next;

        // Поскольку последовательность Фибоначчи бесконечна,
        // то `Iterator` никогда не вернет `None`, и всегда будет
        // возвращаться `Some`.
        Some(self.curr)
    }
}

// Возвращается генератор последовательности Фибоначчи.
fn fibonacci() -> Fibonacci {
    Fibonacci { curr: 0, next: 1 }
}

fn main() {
    // `0..3` это `Iterator`, который генерирует : 0, 1, и 2.
    let mut sequence = 0..3;

    println!("Четыре подряд вызова `next`на 0..3");
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());

    // `for` работает через `Iterator` пока тот не вернет `None`.
    // каждое значение `Some` распаковывается  и привязывается к переменной (здесь это `i`).
    println!("Итерирование по 0..3 используя `for`");
    for i in 0..3 {
        println!("> {}", i);
    }

    // Метод `take(n)` уменьшает `Iterator` до его первых `n` членов.
    println!("Первые четыре члена последовательности Фибоначчи: ");
    for i in fibonacci().take(4) {
        println!("> {}", i);
    }

    // Метод `skip(n)` сокращает `Iterator`, отбрасывая его первые `n` членов.
    println!("Следующие четыре члена последовательности Фибоначчи: ");
    for i in fibonacci().skip(4).take(4) {
        println!("> {}", i);
    }

    let array = [1u32, 3, 3, 7];

    // Метод `iter` превращает `Iterator` в массив/срез.
    println!("Итерирование по массиву {:?}", &array);
    for i in array.iter() {
        println!("> {}", i);
    }
}

impl Trait

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

use std::iter;
use std::vec::IntoIter;

// Эта функция объединяет два `Vec<i32>` и возвращает итератор.
// Посмотрите какой получается сложный тип возвращаемого значения!
fn combine_vecs_explicit_return_type(
    v: Vec<i32>,
    u: Vec<i32>,
) -> iter::Cycle<iter::Chain<IntoIter<i32>, IntoIter<i32>>> {
    v.into_iter().chain(u.into_iter()).cycle()
}

// Это та же самая функция, но в возвращаемом типе использует нотацию `impl Trait`.
// Посмотрите как он упростился!
fn combine_vecs(
    v: Vec<i32>,
    u: Vec<i32>,
) -> impl Iterator<Item=i32> {
    v.into_iter().chain(u.into_iter()).cycle()
}

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = vec![4, 5];
    let mut v3 = combine_vecs(v1, v2);
    assert_eq!(Some(1), v3.next());
    assert_eq!(Some(2), v3.next());
    assert_eq!(Some(3), v3.next());
    assert_eq!(Some(4), v3.next());
    assert_eq!(Some(5), v3.next());
    println!("готово");
}

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

// Вернём функцию, которая добавляет `y` ко входному значению
fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 {
    let closure = move |x: i32| { x + y };
    closure
}

fn main() {
    let plus_one = make_adder_function(1);
    assert_eq!(plus_one(2), 3);
}

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

fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a {
    numbers
        .iter()
        .filter(|x| x > &&0)
        .map(|x| x * 2)
}

Типаж Clone

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

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

// Единичная структура без ресурсов
#[derive(Debug, Clone, Copy)]
struct Unit;

// Кортежная структура с ресурсами, которая реализует типаж `Clone`
#[derive(Clone, Debug)]
struct Pair(Box<i32>, Box<i32>);

fn main() {
    // Создадим экземпляр `Unit`
    let unit = Unit;
    // Скопируем `Unit`, который не имеет ресурсов для перемещения
    let copied_unit = unit;

    // Оба `Unit` могут быть использованы независимо
    println!("original: {:?}", unit);
    println!("copy: {:?}", copied_unit);

    // Создадим экземпляр `Pair`
    let pair = Pair(Box::new(1), Box::new(2));
    println!("original: {:?}", pair);

    // Переместим `pair` в `moved_pair`, перемещая и ресурсы
    let moved_pair = pair;
    println!("moved: {:?}", moved_pair);

    // Ошибка! Переменная `pair` потеряла свои ресурсы
    //println!("original: {:?}", pair);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Клонируем `moved_pair` в `cloned_pair` (включая ресурсы)
    let cloned_pair = moved_pair.clone();
    // Удалим исходную пару, используя std::mem::drop
    drop(moved_pair);

    // Ошибка! `moved_pair` была удалена
    //println!("copy: {:?}", moved_pair);
    // ЗАДАНИЕ ^ Попробуйте раскомментировать эту строку

    // Результат, полученный из .clone(), все ещё можно использовать!
    println!("clone: {:?}", cloned_pair);
}

Супертрейты

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

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

// `Student` - супертрейт для `Person`.
// Реализация `Student` требует, чтобы вы также реализовали и `Person`.
trait Student: Person {
    fn university(&self) -> String;
}

trait Programmer {
    fn fav_language(&self) -> String;
}

// `CompSciStudent` (студент факультета информацики) - супертрейт для `Programmer` 
// и `Student`. Реализация `CompSciStudent` требует реализации обоих подтрейтов.
trait CompSciStudent: Programmer + Student {
    fn git_username(&self) -> String;
}

fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    format!(
        "Меня зовут {} и я посещаю {}. Моё имя в Git {}",
        student.name(),
        student.university(),
        student.git_username()
    )
}

fn main() {}

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

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

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

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

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

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

trait UsernameWidget {
    // Получить из виджета имя пользователя
    fn get(&self) -> String;
}

trait AgeWidget {
    // Получить из виджета возраст
    fn get(&self) -> u8;
}

// Форма, реализующая оба трейта: и `UsernameWidget`, и `AgeWidget`
struct Form {
    username: String,
    age: u8,
}

impl UsernameWidget for Form {
    fn get(&self) -> String {
        self.username.clone()
    }
}

impl AgeWidget for Form {
    fn get(&self) -> u8 {
        self.age
    }
}

fn main() {
    let form = Form{
        username: "rustacean".to_owned(),
        age: 28,
    };

    // Если вы раскомментируете эту строку, вы получите ошибку, которая говорит
    // "multiple `get` found". Потому что это, в конце концов, несколько методов
    // с именем `get`.
    // println!("{}", form.get());

    let username = <Form as UsernameWidget>::get(&form);
    assert_eq!("rustacean".to_owned(), username);
    let age = <Form as AgeWidget>::get(&form);
    assert_eq!(28, age);
}

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

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

macro_rules!

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

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

// Этот простой макрос называется `say_hello`.
macro_rules! say_hello {
    // `()` указывает, что макрос не принимает аргументов.
    () => (
        // Макрос будет раскрываться с содержимым этого блока.
        println!("Hello!");
    )
}

fn main() {
    // Этот вызов будет раскрыт в код `println!("Hello");`
    say_hello!()
}

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

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

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

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

Синтаксис

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

Указатели

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

macro_rules! create_function {
    // Этот макрос принимает аргумент идентификатора `ident` и
    // создаёт функцию с именем `$func_name`.
    // Идентификатор `ident` используют для обозначения имени переменной/функции.
    ($func_name:ident) => (
        fn $func_name() {
            // Макрос `stringify!` преобразует `ident` в строку.
            println!("Вызвана функция {:?}()",
                     stringify!($func_name))
        }
    )
}

// Создадим функции с именами `foo` и `bar` используя макрос, указанный выше.
create_function!(foo);
create_function!(bar);

macro_rules! print_result {
    // Этот макрос принимает выражение типа `expr` и напечатает
    // его как строку вместе с результатом.
    // Указатель `expr` используют для обозначения выражений.
    ($expression:expr) => (
        // `stringify!` преобразует выражение в строку *без изменений*.
        println!("{:?} = {:?}",
                 stringify!($expression),
                 $expression);
    )
}

fn main() {
    foo();
    bar();

    print_result!(1u32 + 1);

    // Напомним, что блоки тоже являются выражениями!
    print_result!({
        let x = 1u32;

        x * x + 2 * x - 1
    });
}

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

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

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

Перегрузка

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

// `test!` будет сравнивать `$left` и `$right`
// по разному, в зависимости от того, как вы объявите их:
macro_rules! test {
    // Не нужно разделять аргументы запятой.
    // Можно использовать любой шаблон!
    ($left:expr; and $right:expr) => (
        println!("{:?} и {:?} это {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left && $right)
    );
    // ^ каждый блок должен заканчиваться точкой с запятой.
    ($left:expr; or $right:expr) => (
        println!("{:?} или {:?} это {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left || $right)
    );
}

fn main() {
    test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
    test!(true; or false);
}

Повторение

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

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

// `min!` посчитает минимальное число аргументов.
macro_rules! find_min {
    // Простой вариант:
    ($x:expr) => ($x);
    // `$x` следует хотя бы одному `$y,`
    ($x:expr, $($y:expr),+) => (
        // Вызовем `find_min!` на конце `$y`
        std::cmp::min($x, find_min!($($y),+))
    )
}

fn main() {
    println!("{}", find_min!(1u32));
    println!("{}", find_min!(1u32 + 2 , 2u32));
    println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
}

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

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

use std::ops::{Add, Mul, Sub};

macro_rules! assert_equal_len {
    // Указатель `tt` (единственное дерево лексем) используют для
    // операторов и лексем.
    ($a:expr, $b:expr, $func:ident, $op:tt) => (
        assert!($a.len() == $b.len(),
                "{:?}: несоответствие размеров: {:?} {:?} {:?}",
                stringify!($func),
                ($a.len(),),
                stringify!($op),
                ($b.len(),));
    )
}

macro_rules! op {
    ($func:ident, $bound:ident, $op:tt, $method:ident) => (
        fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
            assert_equal_len!(xs, ys, $func, $op);

            for (x, y) in xs.iter_mut().zip(ys.iter()) {
                *x = $bound::$method(*x, *y);
                // *x = x.$method(*y);
            }
        }
    )
}

// Реализуем функции `add_assign`, `mul_assign`, и `sub_assign`.
op!(add_assign, Add, +=, add);
op!(mul_assign, Mul, *=, mul);
op!(sub_assign, Sub, -=, sub);

mod test {
    use std::iter;
    macro_rules! test {
        ($func: ident, $x:expr, $y:expr, $z:expr) => {
            #[test]
            fn $func() {
                for size in 0usize..10 {
                    let mut x: Vec<_> = iter::repeat($x).take(size).collect();
                    let y: Vec<_> = iter::repeat($y).take(size).collect();
                    let z: Vec<_> = iter::repeat($z).take(size).collect();

                    super::$func(&mut x, &y);

                    assert_eq!(x, z);
                }
            }
        }
    }

    // Протестируем `add_assign`, `mul_assign` и `sub_assign`
    test!(add_assign, 1u32, 2u32, 3u32);
    test!(mul_assign, 2u32, 3u32, 6u32);
    test!(sub_assign, 3u32, 2u32, 1u32);
}
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

Domain Specific Languages (DSLs)

DSL - это мини язык, встроенный в макросы Rust. Это полностью допустимый код на Rust, так как система макросов разворачивается в нормальные конструкции, но выглядит как маленький язык. Это позволяет вам определять краткий или интуитивный синтаксис для некоторой функциональности (в пределах границ).

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

macro_rules! calculate {
    (eval $e:expr) => {{
        {
            let val: usize = $e; // Заставим быть переменную целым числом.
            println!("{} = {}", stringify!{$e}, val);
        }
    }};
}

fn main() {
    calculate! {
        eval 1 + 2 // хе-хе, `eval` _не_ ключевое слово Rust!
    }

    calculate! {
        eval (1 + 2) * (3 / 4)
    }
}

Вывод:

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

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

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

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

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

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

macro_rules! calculate {
    // Шаблон для единичного `eval`
    (eval $e:expr) => {{
        {
            let val: usize = $e; // Заставим быть переменную целым числом.
            println!("{} = {}", stringify!{$e}, val);
        }
    }};

    // Рекурсивно декомпозируем несколько `eval`
    (eval $e:expr, $(eval $es:expr),+) => {{
        calculate! { eval $e }
        calculate! { $(eval $es),+ }
    }};
}

fn main() {
    calculate! { // Смотри, мама! Вариативный `calculate!`!
        eval 1 + 2,
        eval 3 + 4,
        eval (2 * 3) + 1
    }
}

Вывод:

1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7

Обработка ошибок

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

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

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

Тип Option предназначен для случаев, когда значение не обязательно или когда отсутствие значения не является ошибкой. Например, корневые директории / и C: не имеют родителя. При работе с Option, для прототипирования и случаев, когда мы точно знаем, что значение должно быть, отлично подходит unwrap. Однако более полезен expect, так как он позволяет указать сообщение об ошибке на случай, если что-то пойдёт не так.

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

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

panic

Самый простой механизм обработки ошибок, с которым мы познакомимся – это panic. Он печатает сообщение с ошибкой, начинает процедуру раскрутки стека и, чаще всего, завершает программу. В данном примере мы явно вызываем panic в случае ошибки:

fn drink(beverage: &str) {
    // Вы не должны пить слишком много сладких напитков.
    if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("Some refreshing {} is all I need.", beverage);
}

fn main() {
    drink("water");
    drink("lemonade");
}

Option и unwrap

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

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

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

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

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

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

// Взрослый человек всё это видео, и может хорошо справиться с любым напитком.
// Все напитки обрабатываются явно, с использованием конструкции `match`.
fn give_adult(drink: Option<&str>) { // выдать взрослому напиток
    // Укажем что нужно делать, в каждом случае.
    match drink {
        Some("лимонад") => println!("Фи! Слишком сладко."),
        Some(inner)   => println!("{}? Хорошо.", inner),
        None          => println!("Нет напитка? Ну что ж."),
    }
}

// Другие будут паниковать перед тем как выпить напитки, содержащие сахар.
// Все напитки обрабатываются неявно, с использованием `unwrap`.
fn drink(drink: Option<&str>) {
    // `unwrap` возвращает `panic` когда получает на вход `None`.
    let inside = drink.unwrap();
    if inside == "лимонад" { panic!("AAAaaaaa!!!!"); }

    println!("Я люблю {}ы!!!!!", inside);
}

fn main() {
    let water  = Some("вода");
    let lemonade = Some("лимонад");
    let void  = None;

    give_adult(water);
    give_adult(lemonade);
    give_adult(void);

    let coffee = Some("кофе");
    let nothing = None;

    drink(coffee);
    drink(nothing);
}

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

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

fn next_birthday(current_age: Option<u8>) -> Option<String> {
	// Если `current_age` == `None`, то возвращаем `None`.
	// Если `current_age` == `Some`, то содержащееся в ней `u8` будет присвоено переменной `next_age`
    let next_age: u8 = current_age?;
    Some(format!("В следующем году мне будет {}", next_age))
}

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

struct Person {
    job: Option<Job>,
}

#[derive(Clone, Copy)]
struct Job {
    phone_number: Option<PhoneNumber>,
}

#[derive(Clone, Copy)]
struct PhoneNumber {
    area_code: Option<u8>,
    number: u32,
}

impl Person {
    
    // Получим из рабочего номера телефона код региона, если он существует.
    fn work_phone_area_code(&self) -> Option<u8> {
        // Мы можем не использовать оператор `?` и тогда здесь будет много вложенных операторов `match`.
        // С ним кода будет больше. Попробуйте использовать в этом коде `match` и посмотрите,
        // какой вариант проще.
        self.job?.phone_number?.area_code
    }
}

fn main() {
    let p = Person {
        job: Some(Job {
            phone_number: Some(PhoneNumber {
                area_code: Some(61),
                number: 439222222,
            }),
        }),
    };

    assert_eq!(p.work_phone_area_code(), Some(61));
}

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

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

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

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

#![allow(dead_code)]

#[derive(Debug)] enum Food { Apple, Carrot, Potato }

#[derive(Debug)] struct Peeled(Food);
#[derive(Debug)] struct Chopped(Food);
#[derive(Debug)] struct Cooked(Food);

// Очистка продуктов. Если продуктов нет, то возвращаем `None`.
// Иначе вернём очищенные продукты.
fn peel(food: Option<Food>) -> Option<Peeled> {
    match food {
        Some(food) => Some(Peeled(food)),
        None       => None,
    }
}

// Нарезка продуктов. Если продуктов нет, то возвращаем `None`.
// Иначе вернём нарезанные продукты.
fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
    match peeled {
        Some(Peeled(food)) => Some(Chopped(food)),
        None               => None,
    }
}

// Приготовление еды. Здесь, для обработки вариантов, мы используем 
// `map()` вместо `match`.
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    chopped.map(|Chopped(food)| Cooked(food))
}

// Функция для последовательной очистки, нарезке и приготовлении продуктов.
// Мы объединили в цепочку несколько вызовов `map()` для упрощения кода.
fn process(food: Option<Food>) -> Option<Cooked> {
    food.map(|f| Peeled(f))
        .map(|Peeled(f)| Chopped(f))
        .map(|Chopped(f)| Cooked(f))
}

// Проверим, есть ли еда, прежде чем её съесть
fn eat(food: Option<Cooked>) {
    match food {
        Some(food) => println!("Ммм. Я люблю {:?}", food),
        None       => println!("О, нет! Это не съедобно."),
    }
}

fn main() {
    let apple = Some(Food::Apple);
    let carrot = Some(Food::Carrot);
    let potato = None;

    let cooked_apple = cook(chop(peel(apple)));
    let cooked_carrot = cook(chop(peel(carrot)));
    // Давайте сейчас попробуем проще выглядящую `process()`.
    let cooked_potato = process(potato);

    eat(cooked_apple);
    eat(cooked_carrot);
    eat(cooked_potato);
}

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

Замыкания, Option, Option::map()

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

map() описывался как использование цепочек функций для упрощения выражения match. Однако использование map() с функцией, которая в качестве результата возвращает Option<T> приводит к вложенности Option<Option<T>>. Такая цепочка из множества вызовов в итоге может запутать. Вот тут и появляется другой комбинатор, зовущийся and_then(), известный в некоторых языках как flatmap.

and_then() запускает функцию, которая на вход получает обёрнутое значение, а возвращает результирующее значение. Если Option равен None, то он вернёт None.

В следующем примере, cookable_v2() возвращает
Option<Food>. Используя map() вместо and_then() мы получим Option<Option<Food>>, который является не правильным типом для eat().

#![allow(dead_code)]

#[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }

// У нас нет ингридиентов для приготовления Sushi.
fn have_ingredients(food: Food) -> Option<Food> {
    match food {
        Food::Sushi => None,
        _           => Some(food),
    }
}

// У нас есть рецепты для всего, за исключением Cordon Bleu.
fn have_recipe(food: Food) -> Option<Food> {
    match food {
        Food::CordonBleu => None,
        _                => Some(food),
    }
}

// Для приготовления блюда нам необходимы и рецепт, и ингредиент.
// Мы можем представить логику, как цепочку из`match`:
fn cookable_v1(food: Food) -> Option<Food> {
    match have_recipe(food) {
        None       => None,
        Some(food) => match have_ingredients(food) {
            None       => None,
            Some(food) => Some(food),
        },
    }
}

// Для удобства это может быть переписано с использованием более компактного `and_then()`:
fn cookable_v2(food: Food) -> Option<Food> {
    have_recipe(food).and_then(have_ingredients)
}

fn eat(food: Food, day: Day) {
    match cookable_v2(food) {
        Some(food) => println!("Yay! В {:?} мы будем есть {:?}.", day, food),
        None       => println!("О, нет. Мы не будем есть в {:?}?", day),
    }
}

fn main() {
    let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);

    eat(cordon_bleu, Day::Monday);
    eat(steak, Day::Tuesday);
    eat(sushi, Day::Wednesday);
}

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

Замыкания, Option, и Option::and_then()

Result

Result является более богатой версией типа Option, тип который описывает возможную ошибку вместо возможного её отсутствия.

Result<T, E> имеет два возможных значения:

  • Ok(T): Значение типа T
  • Err(E): Ошибка обработки элемента, типа E

По соглашению, ожидаемый результат Ok, тогда как не ожидаемый - Err.

Подобно Option, Result имеет множество ассоциированных с ним методов. Например, unwrap() или возвращает T, или вызывает panic. Для обработки результата у Result существует множество комбинаторов, которые совпадают с комбинаторами Option.

При работе с Rust вы, скорее всего, столкнётесь с методами, которые возвращают тип Result, например метод parse(). Не всегда можно разобрать строку в другой тип, поэтому parse() возвращает Result, указывающий на возможный сбой.

Давайте посмотрим, что происходит, когда мы успешно и безуспешно попытаемся преобразовать строку с помощью parse():

fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
    // Давайте попробуем использовать `unwrap()` чтобы получить число. Он нас укусит?
    let first_number = first_number_str.parse::<i32>().unwrap();
    let second_number = second_number_str.parse::<i32>().unwrap();
    first_number * second_number
}

fn main() {
    let twenty = multiply("10", "2");
    println!("удовоенное {}", twenty);

    let tt = multiply("t", "2");
    println!("удвоенное {}", tt);
}

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

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

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

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

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

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

use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let number_str = "10";
    let number = match number_str.parse::<i32>() {
        Ok(number)  => number,
        Err(e) => return Err(e),
    };
    println!("{}", number);
    Ok(())
}

map для Result

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

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

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

use std::num::ParseIntError;

// Мы используем сопоставление с образцом без `unwrap()` и меняем тип результата.
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    match first_number_str.parse::<i32>() {
        Ok(first_number)  => {
            match second_number_str.parse::<i32>() {
                Ok(second_number)  => {
                    Ok(first_number * second_number)
                },
                Err(e) => Err(e),
            }
        },
        Err(e) => Err(e),
    }
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n равно {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    // Это даёт разумный ответ.
    let twenty = multiply("10", "2");
    print(twenty);

    // Следующее теперь предоставляет более понятное сообщение об ошибке.
    let tt = multiply("t", "2");
    print(tt);
}

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

use std::num::ParseIntError;

// Как и с `Option`, мы можем использовать комбинаторы, как `map()`.
// Эта функция в основном идентична предыдущей и читается как:
// изменяем n при валидном значении, иначе передаём ошибку.
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    first_number_str.parse::<i32>().and_then(|first_number| {
        second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    })
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n равно {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    // Это даёт разумный ответ.
    let twenty = multiply("10", "2");
    print(twenty);

    // Следующее теперь предоставляет более понятное сообщение об ошибке.
    let tt = multiply("t", "2");
    print(tt);
}

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

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

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

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

use std::num::ParseIntError;

// Объявим обобщённый псевдоним для `Result` с типом ошибки `ParseIntError`.
type AliasedResult<T> = Result<T, ParseIntError>;

// Используем вышеуказанный псевдоним для обозначения
// нашего конкретного типа `Result`.
fn multiply(first_number_str: &str, second_number_str: &str) -> AliasedResult<i32> {
    first_number_str.parse::<i32>().and_then(|first_number| {
        second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    })
}

// Здесь псевдоним снова позволяет нам сэкономить место.
fn print(result: AliasedResult<i32>) {
    match result {
        Ok(n)  => println!("n это {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

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

io::Result

Ранний выход

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

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

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = match first_number_str.parse::<i32>() {
        Ok(first_number)  => first_number,
        Err(e) => return Err(e),
    };

    let second_number = match second_number_str.parse::<i32>() {
        Ok(second_number)  => second_number,
        Err(e) => return Err(e),
    };

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n равно {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

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

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

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

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

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

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

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

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = first_number_str.parse::<i32>()?;
    let second_number = second_number_str.parse::<i32>()?;

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n равно {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

Макрос try!

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

// Для компиляции и запуска с помощью Cargo этого примера без ошибок
// поменяйте в `Cargo.toml` значение поля `edition` секции 
// `[package]` на "2015".

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = try!(first_number_str.parse::<i32>());
    let second_number = try!(second_number_str.parse::<i32>());

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n равно {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}
1

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

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

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

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

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

fn double_first(vec: Vec<&str>) -> i32 {
    let first = vec.first().unwrap(); // Генерирует ошибку 1
    2 * first.parse::<i32>().unwrap() // Генерирует ошибку 2
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("Первое удвоенное {}", double_first(numbers));

    println!("Первое удвоенное {}", double_first(empty));
    // Ошибка 1: входной вектор пустой

    println!("Первое удвоенное {}", double_first(strings));
    // Ошибка 2: элемент не может быть преобразован в число
}

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

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

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

use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
    vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    })
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("Первое удвоенное: {:?}", double_first(numbers));

    println!("Первое удвоенное: {:?}", double_first(empty));
    // Ошибка первая: исходный вектор пустой

    println!("Первое удвоенное {:?}", double_first(strings));
    // Ошибка вторая: элемент не переводится в число
}

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

use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> {
    let opt = vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    });

    opt.map_or(Ok(None), |r| r.map(Some))
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {:?}", double_first(numbers));
    println!("The first doubled is {:?}", double_first(empty));
    println!("The first doubled is {:?}", double_first(strings));
}

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

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

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

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

type Result<T> = std::result::Result<T, DoubleError>;

// Определите типы ошибок. Они могут быть настроены для наших случаев обработки ошибок.
// Теперь мы сможем написать наши собственные ошибки, реализовать приведение до основной ошибки
// или сделать что-то ещё между приведениями.
#[derive(Debug, Clone)]
struct DoubleError;

// Генерация ошибки полностью отделена от того, как она отображается.
// Нет необходимости в загромождении сложной логикой построения отображения ошибки.
//
// Мы не храним дополнительной информации об ошибках. Это означает, что мы не можем вывести строку, которую не удалось обработать, без изменения наших типов.
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "неверный первый элемент")
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
        // Изменим ошибку на наш новый тип.
        .ok_or(DoubleError)
        .and_then(|s| {
            s.parse::<i32>()
                // Обновим тип ошибки также здесь.
                .map_err(|_| DoubleError)
                .map(|i| 2 * i)
        })
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("Первое удвоение {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

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

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

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

use std::error;
use std::fmt;

// Создадим псевдоним с типом ошибки `Box<error::Error>`.
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;

#[derive(Debug, Clone)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "неверный первый элемент")
    }
}

impl error::Error for EmptyVec {}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
        .ok_or_else(|| EmptyVec.into()) // Упаковка (преобразование в Box)
        .and_then(|s| {
            s.parse::<i32>()
                .map_err(|e| e.into()) // Упаковка (преобразование в Box)
                .map(|i| 2 * i)
        })
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("Удвоенный первый элемент: {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

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

Динамическая диспетчеризация и типаж Error

Другие способы использования ?

Вы обратили внимание, что сразу же после вызова parse, мы в map_err упаковали ошибку из библиотеки?

.and_then(|s| s.parse::<i32>()
    .map_err(|e| e.into())

Это простая и распространённая операция и было бы не плохо, если бы мы могли её опустить. Но из-за того, что and_then недостаточно гибок, мы не можем этого сделать. Однако, тут нам может помочь ?.

Ранее ? был рассмотрен как unwrap или return Err(err). По большей части это правда: на самом деле ? означает unwrap или return Err(From::from(err)). Поскольку From::from используется для преобразования между разными типами, применение ? к ошибке автоматически преобразует её в возвращаемый тип (при условии, что исходная ошибка может быть в него преобразована).

Теперь мы перепишем наш предыдущий пример с использованием ?. В результате у нас пропал map_err, так как для нашего типа реализован From::from:

use std::error;
use std::fmt;

// Создадим псевдоним с типом ошибки `Box<dyn error::Error>`.
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;

#[derive(Debug)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "неверный первый элемент")
    }
}

impl error::Error for EmptyVec {}

// Такая же последовательность, как и раньше, но вместо объединения
// всех `Result` и `Option`, мы используем `?` чтобы незамедлительно
// получить внутреннее значение.
fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(EmptyVec)?;
    let parsed = first.parse::<i32>()?;
    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("Удвоенный первый элемент: {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

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

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

From::from и ?

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

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

use std::error;
use std::num::ParseIntError;
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
    EmptyVec,
    // Мы не будем обрабатывать ошибку разбора сами, а передадим её в программу.
    // Предоставление дополнительной информации требует добавления дополнительных данных к типу
    Parse(ParseIntError),
}

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec =>
                write!(f, "пожалуйста используйте вектор хотя бы с одним элементом"),
            // Это адаптер, так что обратимся к нижележащей реализации `fmt`.
            DoubleError::Parse(ref e) => e.fmt(f),
        }
    }
}

impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            DoubleError::EmptyVec => None,
            // Причиной ошибки является адаптированный тип. Здесь происходит
            // неявное преобразование к типажу `&error::Error`. Это работает
            // так как основной тип реализует типаж `Error`.
            DoubleError::Parse(ref e) => Some(e),
        }
    }
}

// Реализуем преобразование из `ParseIntError` в `DoubleError`.
// Это преобразование будет автоматически вызвано оператором `?`, 
// если будет необходимо преобразовать `ParseIntError` в `DoubleError`.
impl From<ParseIntError> for DoubleError {
    fn from(err: ParseIntError) -> DoubleError {
        DoubleError::Parse(err)
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(DoubleError::EmptyVec)?;
    let parsed = first.parse::<i32>()?;

    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("Первое удвоение {}", n),
        Err(e) => println!("Ошибка: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

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

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

From::from и Enums

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

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

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Результаты: {:?}", numbers);
}

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

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

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

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .filter_map(Result::ok)
        .collect();
    println!("Результаты: {:?}", numbers);
}

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

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

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Result<Vec<_>, _> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Результаты: {:?}", numbers);
}

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

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

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let (numbers, errors): (Vec<_>, Vec<_>) = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);
    println!("Числа: {:?}", numbers);
    println!("Ошибки: {:?}", errors);
}

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

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let (numbers, errors): (Vec<_>, Vec<_>) = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);
    let numbers: Vec<_> = numbers.into_iter().map(Result::unwrap).collect();
    let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
    println!("Числа: {:?}", numbers);
    println!("Ошибки: {:?}", errors);
}

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

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

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

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

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

Box, стек и куча

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

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

use std::mem;

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

// `Rectangle` может быть определён по расположению в пространстве 
// его верхнего левого и нижнего правого углов
#[allow(dead_code)]
struct Rectangle {
    top_left: Point,
    bottom_right: Point,
}

fn origin() -> Point {
    Point { x: 0.0, y: 0.0 }
}

fn boxed_origin() -> Box<Point> {
    // Аллоцируем точку в куче и вернём указатель на неё
    Box::new(Point { x: 0.0, y: 0.0 })
}

fn main() {
    // (все аннотации типов избыточны)
    // Переменные, аллоцированные на стеке
    let point: Point = origin();
    let rectangle: Rectangle = Rectangle {
        top_left: origin(),
        bottom_right: Point { x: 3.0, y: -4.0 }
    };

    // Прямоугольник, аллоцированный в куче
    let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
        top_left: origin(),
        bottom_right: Point { x: 3.0, y: -4.0 },
    });

    // Результат функции может быть упакован
    let boxed_point: Box<Point> = Box::new(origin());

    // Двойная косвенность
    let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

    println!("Точка занимает {} байт на стеке",
             mem::size_of_val(&point));
    println!("Прямоугольник занимает {} байт на стеке",
             mem::size_of_val(&rectangle));

    // box size == pointer size
    println!("Упакованная точка занимает {} байт на стеке",
             mem::size_of_val(&boxed_point));
    println!("Упакованный прямоугольник занимает {} байт на стеке",
             mem::size_of_val(&boxed_rectangle));
    println!("Упакованная 'упаковка' занимает {} байт на стеке",
             mem::size_of_val(&box_in_a_box));

    // Копируем данные из `boxed_point` в `unboxed_point`
    let unboxed_point: Point = *boxed_point;
    println!("Распакованная точка занимает {} байт на стеке",
             mem::size_of_val(&unboxed_point));
}

Вектора

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

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

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

fn main() {
    // Итераторы могут быть собраны в вектора
    let collected_iterator: Vec<i32> = (0..10).collect();
    println!("(0..10) собраны в: {:?}", collected_iterator);

    // Макрос `vec!` может быть использован для инициализации вектора
    let mut xs = vec![1i32, 2, 3];
    println!("Исходный вектор: {:?}", xs);

    // Вставка нового элемента в конец вектора
    println!("Добавим 4 в конец вектора");
    xs.push(4);
    println!("Вектор: {:?}", xs);

    // Ошибка! Неизменяемые вектора не могут увеличиваться
    collected_iterator.push(0);
    // ИСПРАВЬТЕ ^ Закомментируйте эту строку

    // Метод `len` отдаёт количество элементом, сохранённых в векторе
    println!("Длина вектора: {}", xs.len());

    // Индексация выполняется при помощи квадратных скобок (индексация начинается с 0)
    println!("Второй элемент: {}", xs[1]);

    // `pop` удаляет последний элемент из вектора и возвращает его
    println!("Последний элемент: {:?}", xs.pop());

    // Выход за пределы индексации вызывает панику
    println!("Четвёртый элемент: {}", xs[3]);
    // ИСПРАВЬТЕ ^ Закомментируйте эту строку

    // По векторами легко итерироваться
    println!("Содержимое `xs`:");
    for x in xs.iter() {
        println!("> {}", x);
    }

    // Также можно итерироваться по вектору с получением индекса элемента
    // (который будет содержаться в отдельной переменной `i`)
    for (i, x) in xs.iter().enumerate() {
        println!("{}-ый элемент имеет значение {}", i, x);
    }

    // Благодаря `iter_mut`, у изменяемых векторов можно менять значения
    // во время итерирования
    for x in xs.iter_mut() {
        *x *= 3;
    }
    println!("Обновлённый вектор: {:?}", xs);
}

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

Строки

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

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

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

fn main() {
    // (все аннотации типов избыточны)
    // Ссылка на строку, размещённую в read-only памяти
    let pangram: &'static str = "the quick brown fox jumps over the lazy dog";
    println!("Pangram: {}", pangram);

    // Итерируемся по словам в обратном прядке, новая строка не аллоцируется
    println!("Words in reverse");
    for word in pangram.split_whitespace().rev() {
        println!("> {}", word);
    }

    // Копируем символы в вектор, сортируем и удаляем дубликаты
    let mut chars: Vec<char> = pangram.chars().collect();
    chars.sort();
    chars.dedup();

    // Создаём пустую расширяемую `String`
    let mut string = String::new();
    for c in chars {
        // Добавляем символ в конец строки
        string.push(c);
        // Добавляем в конец строки другую строку
        string.push_str(", ");
    }

    // Усечённая строка - это срез оригинальной строки, а значит новых 
    // аллокаций не производится
    let chars_to_trim: &[char] = &[' ', ','];
    let trimmed_str: &str = string.trim_matches(chars_to_trim);
    println!("Used characters: {}", trimmed_str);

    // Строка, аллоцированная в куче
    let alice = String::from("I like dogs");
    // Выделяется новая память, в которую сохраняется модифицированная строка
    let bob: String = alice.replace("dog", "cat");

    println!("Alice says: {}", alice);
    println!("Bob says: {}", bob);
}

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

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

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

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

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

fn main() {
    // Вы можете использовать экранирование для записи байтов 
    // при помощи их шестнадцатиричных значений...
    let byte_escape = "Я пишу на \x52\x75\x73\x74!";
    println!("Что ты делашь\x3F (\\x3F означает ?) {}", byte_escape);

    // ... или кодов Unicode.
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!("Unicode символ {} (U+211D) называется {}",
                unicode_codepoint, character_name );


    let long_string = "Строковый литерал
                       может занимать несколько строк.
                       Разрыв строки и отступ ->\
                       <- также можно экранировать!";
    println!("{}", long_string);
}

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

fn main() {
    let raw_str = r"Экранирование здесь не работает: \x3F \u{211D}";
    println!("{}", raw_str);

    // Если вам необходимы кавычки с сырой строке, добавьте пару `#`
    let quotes = r#"И затем я сказал: "Здесь нет экранирования!""#;
    println!("{}", quotes);

    // Если вам необходимо добавить в вашу строку `"#`, то просто добавьте больше `#` в разделитель.
    // Здесь нет ограничений на количество `#` которое вы можете использовать.
    let longer_delimiter = r###"Строка с "# внутри неё. И даже с "##!"###;
    println!("{}", longer_delimiter);
}

Хотите строку, которая не UTF-8? (Помните, str и String должны содержать действительные UTF-8 последовательности). Или возможно вы хотите массив байтов, которые в основном текст? Байтовые строки вас спасут!

use std::str;

fn main() {
    // Обратите внимание, что в действительности это не `&str`
    let bytestring: &[u8; 21] = b"это строка байтов";

    // Для массива байтов не реализован типаж `Display`, поэтому способы его печати ограничены
    println!("Строка байтов: {:?}", bytestring);

    // Байтовые строки могут содержать экранированные байты...
    let escaped = b"\x52\x75\x73\x74 как байты";
    // ... но не Unicode
    // let escaped = b"\u{211D} здесь не разрешён";
    println!("Экранированные байты: {:?}", escaped);


    // Сырые байтовые строки работают также, как и сырые строки
    let raw_bytestring = br"\u{211D} здесь не экранировано";
    println!("{:?}", raw_bytestring);

    // Преобразование массива байт в `str` может завершиться ошибкой
    if let Ok(my_str) = str::from_utf8(raw_bytestring) {
        println!("И то же самое в виде текста: '{}'", my_str);
    }

    let _quotes = br#"Вы также можете использовать удобное для вас форматирование, \
                      как и с обычными сырыми строками"#;

    // Байтовые строки не обязаны быть UTF-8
    let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82"; // "ようこそ" в SHIFT-JIS

    // Но из-за этого они не всегда могут быть преобразованы в `str`
    match str::from_utf8(shift_jis) {
        Ok(my_str) => println!("Удачное преобразование: '{}'", my_str),
        Err(e) => println!("Неудачное преобразование: {:?}", e),
    };
}

Для преобразования между кодировками символов, посмотрите крейт encoding.

Более детальный список способов записи строковых литералов и экранирования символов можно найти в главе 'Tokens' Rust Reference.

Option

Иногда желательно перехватить ошибку в какой-либо части программы вместо вызова паники с помощью макроса panic!. Это можно сделать с помощью перечисления Option.

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

  • None, указывающий о наличии ошибки или отсутствия значений
  • Some(value), кортежная структура, обёртка для значения типа T.
// Целочисленное деление, которое не вызывает `panic!`
fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {
    if divisor == 0 {
        // В случае ошибки возвращаем `None`
        None
    } else {
        // Результат деления возвращаем в варианте `Some`
        Some(dividend / divisor)
    }
}

// Эта функция обрабатывает деление, которое может выполнится с ошибкой
fn try_division(dividend: i32, divisor: i32) {
    // Значение типа `Option` могут быть сопоставлены по шаблону
    match checked_division(dividend, divisor) {
        None => println!("{} / {} вызвало ошибку!", dividend, divisor),
        Some(quotient) => {
            println!("{} / {} = {}", dividend, divisor, quotient)
        },
    }
}

fn main() {
    try_division(4, 2);
    try_division(1, 0);

    // Привязка `None` к переменной должна быть аннотированной по типу
    let none: Option<i32> = None;
    let _equivalent_none = None::<i32>;

    let optional_float = Some(0f32);

    // Распаковка варианта `Some` будет извлекать данные, которые в нем находятся.
    println!("{:?} распаковывается в {:?}", optional_float, optional_float.unwrap());

    // Распаковка варианта `None` вызовет `panic!`
    println!("{:?} распаковывается в {:?}", none, none.unwrap());
}

Result

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

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

  • Ok(value), который обозначает, что операция успешно завершилась, и оборачивает значение (value), возвращаемое операцией (value имеет тип T).
  • Err(why), который показывает, что операция потерпела неудачу, оборачивает значение ошибки (причину, why), которое (надеемся) описывает причину неудачи. why имеет тип E.
mod checked {
    // Математические "ошибки", которые мы хотим отлавливать
    #[derive(Debug)]
    pub enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    pub type MathResult = Result<f64, MathError>;

    pub fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            // При таком значение операция потерпит неудачу.
            // Вместо этого давайте вернём ошибку, обёрнутую в `Err`
            Err(MathError::DivisionByZero)
        } else {
            // Эта операция возможна, так что вернём результат, обёрнутый в `Ok`
            Ok(x / y)
        }
    }

    pub fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    pub fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }
}

// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
    // Это трёхуровневая пирамида из `match`!
    match checked::div(x, y) {
        Err(why) => panic!("{:?}", why),
        Ok(ratio) => match checked::ln(ratio) {
            Err(why) => panic!("{:?}", why),
            Ok(ln) => match checked::sqrt(ln) {
                Err(why) => panic!("{:?}", why),
                Ok(sqrt) => sqrt,
            },
        },
    }
}

fn main() {
    // Потерпит ли это неудачу?
    println!("{}", op(1.0, 10.0));
}

?

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

mod checked {
    #[derive(Debug)]
    enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    type MathResult = Result<f64, MathError>;

    fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            Err(MathError::DivisionByZero)
        } else {
            Ok(x / y)
        }
    }

    fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }

    // Промежуточная функция
    fn op_(x: f64, y: f64) -> MathResult {
        // Если `div` "упадёт", тогда будет "возвращено" `DivisionByZero`
        let ratio = div(x, y)?;

        // если `ln` "упадёт", тогда будет "возвращено" `NonPositiveLogarithm`
        let ln = ln(ratio)?;

        sqrt(ln)
    }

    pub fn op(x: f64, y: f64) {
        match op_(x, y) {
            Err(why) => panic!(match why {
                MathError::NonPositiveLogarithm
                    => "логарифм не положительного числа",
                MathError::DivisionByZero
                    => "деление на ноль",
                MathError::NegativeSquareRoot
                    => "квадратный корень от отрицательного числа",
            }),
            Ok(value) => println!("{}", value),
        }
    }
}

fn main() {
    checked::op(1.0, 10.0);
}

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

panic!

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

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

// Реализуем свою версию целочисленного деления (/)
fn division(dividend: i32, divisor: i32) -> i32 {
    if divisor == 0 {
       // Деление на ноль вызывает панику
        panic!("Деление на ноль!");
    } else {
        dividend / divisor
    }
}

// Основной поток `main`
fn main() {
    // Целочисленное значение, выделенное в куче
    let _x = Box::new(0i32);

    // Это операция вызовет панику в основном потоке
    division(3, 0);

    println!("Эта часть кода не будет достигнута");

    // `_x` должен быть уничтожен в этой точке
}

Давайте убедимся, что panic! не приводит к утечке памяти.

$ rustc panic.rs && valgrind ./panic
==4401== Memcheck, a memory error detector
==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==4401== Command: ./panic
==4401==
thread '<main>' panicked at 'division by zero', panic.rs:5
==4401==
==4401== HEAP SUMMARY:
==4401==     in use at exit: 0 bytes in 0 blocks
==4401==   total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated
==4401==
==4401== All heap blocks were freed -- no leaks are possible
==4401==
==4401== For counts of detected and suppressed errors, rerun with: -v
==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

HashMap

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

Как у векторов, размер объектов HashMap может увеличиваться, но они также могут и уменьшиться, если часть занимаемой ими памяти не используется. Вы можете создать хэш-карту с определённой начальной вместимостью при помощи функции HashMap::with_capacity(uint) или использовать HashMap::new() для получения хэш-карты с начальной вместимостью по умолчанию (рекомендуется).

use std::collections::HashMap;

fn call(number: &str) -> &str {
    match number {
        "798-1364" => "Абонент выключен или находится вне зоны действия сети.
            Пожалуйста, позвоните позднее.",
        "645-7689" => "Здравствуйте, это Mr. Awesome's Pizza. Меня зовут Фред.
            Что я могу сделать для вас?",
        _ => "Привет! Кто это опять?"
    }
}

fn main() {
    let mut contacts = HashMap::new();

    contacts.insert("Даниель", "798-1364");
    contacts.insert("Эшли", "645-7689");
    contacts.insert("Кейти", "435-8291");
    contacts.insert("Роберт", "956-1745");

    // Возьмём ссылку и вернём `Option<&V>`
    match contacts.get(&"Даниель") {
        Some(&number) => println!("Звоним Даниелю: {}", call(number)),
        _ => println!("У нас нет номера Даниеля."),
    }

    // `HashMap::insert()` вернёт `None`, если мы добавляем
    // новое значение, иначе - `Some(value)`
    contacts.insert("Даниель", "164-6743");

    match contacts.get(&"Эшли") {
        Some(&number) => println!("Звоним Эшли: {}", call(number)),
        _ => println!("У нас нет номера Эшли."),
    }

    contacts.remove(&"Эшли");

    // `HashMap::iter()` возвращает итератор, который в произвольном
    // порядке отдаёт пары `(&'a key, &'a value)`.
    for (contact, &number) in contacts.iter() {
        println!("Звоним {}: {}", contact, call(number));
    }
}

Для большей информации о том, как работает хеширование и хэш-карты (который иногда называются хэш-таблицами), вы можете обратиться к Wikipedia.

Альтернативные (пользовательские) типы ключей

Любой тип, реализующий типажи Eq и Hash могут являться ключами в HashMap. Туда входят:

  • bool (хотя он будет не очень полезен, так как будет всего лишь два возможных ключа)
  • int, uint и все их варианты
  • String и &str (подсказка: вы можете сделать HashMap с ключами типа String, а вызывать .get() - с &str)

Заметьте, что f32 и f64 не реализуют Hash, из-за того, что ошибки точности при работе с плавающей запятой могут привести к ужасным ошибкам при использовании их в качестве ключей для хэш-карт.

Все классы коллекций реализуют Eq и Hash если содержащийся в них тип также реализует Eq и Hash. Например, Vec<T> реализует Hash, если T реализует Hash.

Вы можете легко реализовать Eq и Hash для пользовательских типов добавив всего лишь одну строчку: #[derive(PartialEq, Eq, Hash)]

Компилятор сделает всё остальное. Если вы хотите больше контроля над деталями, вы можете сами реализовать Eq и/или Hash. Данное руководство не охватывает специфику реализации Hash.

Чтобы поиграть с использованием struct в HashMap, давайте попробуем реализовать очень простую систему авторизации пользователей:

use std::collections::HashMap;

// `Eq` требует, чтобы для типа был также выведен `PartialEq`.
#[derive(PartialEq, Eq, Hash)]
struct Account<'a>{
    username: &'a str,
    password: &'a str,
}

struct AccountInfo<'a>{
    name: &'a str,
    email: &'a str,
}

type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>;

fn try_logon<'a>(accounts: &Accounts<'a>,
        username: &'a str, password: &'a str){
    println!("Имя пользователя: {}", username);
    println!("Пароль: {}", password);
    println!("Попытка входа...");

    let logon = Account {
        username,
        password,
    };

    match accounts.get(&logon) {
        Some(account_info) => {
            println!("Успешный вход!");
            println!("Имя: {}", account_info.name);
            println!("Email: {}", account_info.email);
        },
        _ => println!("Ошибка входа!"),
    }
}

fn main(){
    let mut accounts: Accounts = HashMap::new();

    let account = Account {
        username: "j.everyman",
        password: "password123",
    };

    let account_info = AccountInfo {
        name: "John Everyman",
        email: "j.everyman@email.com",
    };

    accounts.insert(account, account_info);

    try_logon(&accounts, "j.everyman", "psasword123");

    try_logon(&accounts, "j.everyman", "password123");
}

HashSet

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

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

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

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

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

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

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

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

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

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

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

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

use std::collections::HashSet;

fn main() {
    let mut a: HashSet<i32> = vec![1i32, 2, 3].into_iter().collect();
    let mut b: HashSet<i32> = vec![2i32, 3, 4].into_iter().collect();

    assert!(a.insert(4));
    assert!(a.contains(&4));

    // `HashSet::insert()` вернёт `false`
    // если элемент уже содержится в наборе.
    assert!(b.insert(4), "Значение 4 уже есть в наборе B!");
    // ИСПРАВЬТЕ ^ Закомментируйте эту строку

    b.insert(5);

    // Если элементы коллекции реализуют `Debug`,
    // то и сама коллекция реализует `Debug`.
    // Обычно, элементы выводятся в формате `[elem1, elem2, ...]`
    println!("A: {:?}", a);
    println!("B: {:?}", b);

    // Выведет [1, 2, 3, 4, 5] в произвольном порядке
    println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());

    // Выведет только [1]
    println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());

    // Выведет [2, 3, 4] в произвольном порядке.
    println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());

    // Выведет [1, 5]
    println!("Symmetric Difference: {:?}",
             a.symmetric_difference(&b).collect::<Vec<&i32>>());
}

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

Rc

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

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

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

use std::rc::Rc;

fn main() {
    let rc_examples = "Примеры с Rc".to_string();
    {
        println!("--- Создан rc_a ---");
        
        let rc_a: Rc<String> = Rc::new(rc_examples);
        println!("Счётчик ссылок в rc_a: {}", Rc::strong_count(&rc_a));
        
        {
            println!("--- rc_a клонировано в rc_b ---");
            
            let rc_b: Rc<String> = Rc::clone(&rc_a);
            println!("Счётчик ссылок в rc_b: {}", Rc::strong_count(&rc_b));
            println!("Счётчик ссылок в rc_a: {}", Rc::strong_count(&rc_a));
            
            // Два `Rc` равны, если равны их внутренние значения
            println!("rc_a и rc_b равны: {}", rc_a.eq(&rc_b));
            
            // Мы можем напрямую использовать методы внутреннего значения
            println!("Размер значения внутри rc_a: {}", rc_a.len());
            println!("Значение rc_b: {}", rc_b);
            
            println!("--- rc_b удаляется ---");
        }
        
        println!("Счётчик ссылок в rc_a: {}", Rc::strong_count(&rc_a));
        
        println!("--- rc_a удаляется ---");
    }
    
    // Ошибка! Строка `rc_examples` уже перемещена в `rc_a`
    // И когда `rc_a` был удалён, `rc_examples` удалилась вместе с ним
    // println!("rc_examples: {}", rc_examples);
    // TODO ^ Попробуйте раскомментировать эту строку
}

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

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

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

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

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

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

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

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

Потоки

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

use std::thread;

static NTHREADS: i32 = 10;

// Это главный поток `main`
fn main() {
    // Создаём вектор дочерних потоков.
    let mut children = vec![];

    for i in 0..NTHREADS {
        // Создаём очередной поток
        children.push(thread::spawn(move || {
            println!("этот поток номер {}", i);
        }));
    }

    for child in children {
        // Ждём пока поток завершится и вернёт результат.
        let _ = child.join();
    }
}

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

Пример: map-reduce

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

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

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

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

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

use std::thread;

// Это главный поток
fn main() {

    // Это данные, которые мы будем обрабатывать.
    // Мы посчитаем сумму всех чисел при помощи разделённого на потоки map-reduce алгоритма.
    // Каждый фрагмент, разделённый пробелами, будет обрабатываться в отдельном потоке.
    //
    // TODO: посмотрите, что случится, если вы добавите пробелов!
    let data = "86967897737416471853297327050364959
11861322575564723963297542624962850
70856234701860851907960690014725639
38397966707106094172783238747669219
52380795257888236525459303330302837
58495327135744041048897885734297812
69920216438980873548808413720956532
16278424637452589860345374828574668";

    // Создадим вектор, который будет содержать созданные нам дочерние потоки.
    let mut children = vec![];

    /*************************************************************************
     * "Map" фаза
     *
     * Разделим наши данные на сегменты и запустим начальную обработку
     ************************************************************************/

    // Разделим наши данные на сегменты для индивидуального вычисления.
    // Каждый фрагмент будет ссылкой (&str) на данные.
    let chunked_data = data.split_whitespace();

    // Обойдём сегменты данных.
    // .enumerate() добавит в текущий цикл индекс элемента
    // и далее полученный кортеж "(index, element)" будет немедленно
    // "деструктурирован" на две переменные, "i" и "data_segment", при помощи
    // "деструктурирующего присваивания"
    for (i, data_segment) in chunked_data.enumerate() {
        println!("{} сегмент данных \"{}\"", i, data_segment);

        // Обработаем каждый сегмент данных в отдельном потоке
        //
        // `spawn()` вернёт ручку на новый поток,
        // которую мы ДОЛЖНЫ сохранить, чтобы иметь доступ к возвращённому значению
        //
        // Синтаксис 'move || -> u32' обозначает замыкание, которое:
        // * не имеет аргументов ('||')
        // * забирает владение захваченных переменных ('move')
        // * возвращает беззнаковое 32-битное целое число ('-> u32')
        //
        // Rust может вывести '-> u32' из самого замыкация,
        // так что мы можем его опустить.
        //
        // TODO: попробуйте удалить 'move' и посмотреть что получится
        children.push(thread::spawn(move || -> u32 {
            // Вычислим промежуточную сумму этого сегмента:
            let result = data_segment
                        // итерируемся по символам этого сегмента..
                        .chars()
                        // .. преобразуем текстовые символы в их числовые значения..
                        .map(|c| c.to_digit(10).expect("должно быть числом"))
                        // .. и суммируем получившийся итератор из чисел
                        .sum();

            // `println!` блокирует стандартный вывод, так что чередования текста не происходит
            println!("обработан сегмент {}, result={}", i, result);

            // "return" не обязателен, так как Rust "язык выражений" и
            // последнее выполненное выращение в каждом блоке автоматически становится значением этого блока.
            result

        }));
    }


    /*************************************************************************
     * Фаза "Reduce"
     *
     * Собираем наши промежуточные значения и объединяем их в конечные результат
     ************************************************************************/

    // собираем промежуточный результат каждого потока в новый вектор
    let mut intermediate_sums = vec![];
    for child in children {
        // собираем возвращаемое значение каждого дочернего потока
        let intermediate_sum = child.join().unwrap();
        intermediate_sums.push(intermediate_sum);
    }

    // Объединяем все промежуточные суммы в одну конечную сумму.
    //
    // Мы используем "turbofish" `::<>` чтобы подсказать `sum()` тип.
    //
    // TODO: попробуйте без turbofish, явно указывая тип final_result
    let final_result = intermediate_sums.iter().sum::<u32>();

    println!("Финальная сумма: {}", final_result);
}

Назначения

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

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

Каналы

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

use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use std::thread;

static NTHREADS: i32 = 3;

fn main() {
    // Каналы имеют две конечные точки: Sender<T>` и `Receiver<T>`,
    // где `T` - тип передаваемового сообщения.
    // (аннотации типов избыточны)
    let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
    let mut children = Vec::new();

    for id in 0..NTHREADS {
        // Отправитель может быть скопирован
        let thread_tx = tx.clone();

        // Каждый поток отправит через канал его id
        let child = thread::spawn(move || {
            // Поток забирает владение `thread_tx`
            // Каждый поток добавляет своё сообщение в очередь канала
            thread_tx.send(id).unwrap();

            // Отправка - не блокирующая операция, поток незамедлительно
            // продолжит работу после отправки сообщения
            println!("поток {} завершён", id);
        });

        children.push(child);
    }

    // Здесь все сообщения собираются
    let mut ids = Vec::with_capacity(NTHREADS as usize);
    for _ in 0..NTHREADS {
        // Метод `recv` "достаёт" сообщения из канала
        // `recv` блокирует текущий поток, если доступных сообщений нет
        ids.push(rx.recv());
    }
    
    // Ожидаем, когда потоки завершат всю оставшуюся работу
    for child in children {
        child.join().expect("Упс! Дочерний поток паникует");
    }

    // Посмотрите порядок, с которым сообщения были отправлeны
    println!("{:?}", ids);
}

Path

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

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

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

use std::path::Path;

fn main() {
    // Создаём `Path` из `&'static str`
    let path = Path::new(".");

    // Метод `display` возвращает показываемую структуру
    let _display = path.display();

    // `join` соединяет `path` с байтовым контейнером, используя ОС-специфичный
    // разделитель, и возвращает новый путь
    let new_path = path.join("a").join("b");

    // Конвертируем путь в строковый срез
    match new_path.to_str() {
        None => panic!("новый путь не является действительной UTF-8 последовательностью"),
        Some(s) => println!("новый путь {}", s),
    }
}

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

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

OsStr и Metadata.

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

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

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

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

open

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

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

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // Создадим "путь" к нужному файлу
    let path = Path::new("hello.txt");
    let display = path.display();

    // Откроем "путь" в режиме "только чтение". Возвращается `io::Result<File>`
    let mut file = match File::open(&path) {
        Err(why) => panic!("невозможно открыть {}: {}", display, why),
        Ok(file) => file,
    };

    // Читаем содержимое файла в строку. Метод возвращает `io::Result<usize>`
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("невозможно прочесть {}: {}", display, why),
        Ok(_) => print!("{} содержит:\n{}", display, s),
    }

    // `file` выходит из области видимости и файл "hello.txt" закрывается
}

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

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

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

create

Статический метод create открывает файл в режиме только для записи. Если файл уже существует, то его содержимое уничтожится, в противном же случае, создастся новый файл.

static LOREM_IPSUM: &str =
    "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    let path = Path::new("out/lorem_ipsum.txt");
    let display = path.display();

    // Откроем файл в режиме для записи. Возвращается `io::Result<File>`
    let mut file = match File::create(&path) {
        Err(why) => panic!("невозможно создать {}: {}", display, why),
        Ok(file) => file,
    };

    // Запишем строку `LOREM_IPSUM` в `file`. Возвращается `io::Result<()>`
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => panic!("невозможно записать в {}: {}", display, why),
        Ok(_) => println!("успешно записано в {}", display),
    }
}

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

$ rustc create.rs && ./create
successfully wrote to lorem_ipsum.txt
$ cat lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

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

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

read_lines

Метод lines() возвращает итератор, проходящий через все строки файла.

File::open работает с чем-то, что реализует типаж AsRef<Path>. Поэтому read_lines() будет ожидать это же.

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

fn main() {
    // Файл `hosts` должен существовать в текущей директории
    if let Ok(lines) = read_lines("./hosts") {
        // Получает итератор, который возвращает Option
        for line in lines {
            if let Ok(ip) = line {
                println!("{}", ip);
            }      
        }   
    }
}

// Для обработки ошибок, возвращаемое значение оборачивается в Result
// Возвращаем `Iterator` для построчного чтения файла.
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

Запуск этой программы просто выводит эти строки на экран по отдельности.

$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts
$ rustc read_lines.rs && ./read_lines
127.0.0.1
192.168.0.1

Такой подход более эффективен, чем создание String в памяти, особенно при работе с большими файлами.

Дочерние процессы

Структура process::Output представляет результат завершённого дочернего процесса, и структура process::Command - это строитель процесса.

use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("--version")
        .output().unwrap_or_else(|e| {
            panic!("Ошибка выполнения процесса {}", e)
    });

    if output.status.success() {
        let s = String::from_utf8_lossy(&output.stdout);

        print!("rustc завершился успешно и вывел в stdout:\n{}", s);
    } else {
        let s = String::from_utf8_lossy(&output.stderr);

        print!("rustc завершился с ошибкой и вывел в stderr:\n{}", s);
    }
}

(Рекомендуется попробовать предыдущий пример с неправильным флагом обращения к rustc)

Pipes

Структура std::Child представляет собой запущенный дочерний процесс и предоставляет дескрипторы stdin, stdout и stderr для взаимодействия с этим процессом через каналы (pipes).

use std::io::prelude::*;
use std::process::{Command, Stdio};

static PANGRAM: &'static str =
"the quick brown fox jumped over the lazy dog\n";

fn main() {
    // Создадим команду `wc`
    let process = match Command::new("wc")
                                .stdin(Stdio::piped())
                                .stdout(Stdio::piped())
                                .spawn() {
        Err(why) => panic!("не удалось создать wc: {}", why.description()),
        Ok(process) => process,
    };

    // Запишем строку в `stdin` созданной команды.
    //
    // `stdin` имеет тип `Option<ChildStdin>`, но так как мы знаем, что экземпляр должен быть только один,
    // мы можем напрямую вызвать `unwrap`.
    match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
        Err(why) => panic!("не удалось записать в stdin команды wc: {}", why),
        Ok(_) => println!("пангамма отправлена"),
    }

    // Так как `stdin` не существует после вышележащих вызовов, он разрушается
    // и канал закрывается.
    //
    // Это очень важно, иначе `wc` не начал бы обработку только что
    // отправленных данных.

    // Поле `stdout` имеет тип `Option<ChildStdout>` и может быть извлечено.
    let mut s = String::new();
    match process.stdout.unwrap().read_to_string(&mut s) {
        Err(why) => panic!("невозможно прочесть stdout команды wc: {}", why),
        Ok(_) => print!("wc ответил:\n{}", s),
    }
}

Ожидание

Если вы хотите дождаться завершения process::Child, вы должны вызвать Child::wait, который вернёт process::ExitStatus.

use std::process::Command;

fn main() {
    let mut child = Command::new("sleep").arg("5").spawn().unwrap();
    let _result = child.wait().unwrap();

    println!("достигнут конец функции main");
}
$ rustc wait.rs && ./wait
# `wait` продолжает работать в течение 5 секунд, пока команда `sleep 5` не завершится
достигнут конец функции main

Работа с файловой системой

Модуль std::fs содержит различные функции для работы с файловой системой.

use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;

// Упрощённая реализация `% cat path`
fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

// Упрощённая реализация `% echo s > path`
fn echo(s: &str, path: &Path) -> io::Result<()> {
    let mut f = File::create(path)?;

    f.write_all(s.as_bytes())
}

// Упрощённая реализация `% touch path` (игнорирует существующие файлы)
fn touch(path: &Path) -> io::Result<()> {
    match OpenOptions::new().create(true).write(true).open(path) {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    println!("`mkdir a`");
    // Создаём директорию, получаем `io::Result<()>`
    match fs::create_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(_) => {},
    }

    println!("`echo hello > a/b.txt`");
    // Предыдущий `match` может быть написан проще, с помощью метода`unwrap_or_else`
    echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`mkdir -p a/c/d`");
    // Рекурсивно создаём директории, получаем `io::Result<()>`
    fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`touch a/c/e.txt`");
    touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`ln -s ../b.txt a/c/b.txt`");
    // Создаём символическую ссылку, получаем `io::Result<()>`
    if cfg!(target_family = "unix") {
        unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
        });
    }

    println!("`cat a/c/b.txt`");
    match cat(&Path::new("a/c/b.txt")) {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(s) => println!("> {}", s),
    }

    println!("`ls a`");
    // Читаем содержимое директории, получаем `io::Result<Vec<Path>>`
    match fs::read_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(paths) => for path in paths {
            println!("> {:?}", path.unwrap().path());
        },
    }

    println!("`rm a/c/e.txt`");
    // Удаляем файл, получаем `io::Result<()>`
    fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`rmdir a/c/d`");
    // Удаляем пустую директорию, получаем `io::Result<()>`
    fs::remove_dir("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });
}

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

$ rustc fs.rs && ./fs
`mkdir a`
`echo hello > a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
> hello
`ls a`
> "a/b.txt"
> "a/c"
`rm a/c/e.txt`
`rmdir a/c/d`

И конечное состояние директории a:

$ tree a
a
|-- b.txt
`-- c
    `-- b.txt -> ../b.txt

1 directory, 2 files

Альтернативный путь определения функции cat - с нотацией ?:

fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

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

cfg!

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

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

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

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    // Первый аргумент - путь, используемый для вызова программы.
    println!("Мой путь {}.", args[0]);

    // Оставшиеся аргументы - переданные в командной строке параметры.
    // Вызов программы выглядит так:
    //   $ ./args arg1 arg2
    println!("У меня {:?} аргумента: {:?}.", args.len() - 1, &args[1..]);
}
$ ./args 1 2 3
Мой путь ./args.
У меня 3 аргумента: ["1", "2", "3"].

Крейты

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

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

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

use std::env;

fn increase(number: i32) {
    println!("{}", number + 1);
}

fn decrease(number: i32) {
    println!("{}", number - 1);
}

fn help() {
    println!("usage:
match_args <string>
    Проверяет является ли данная строка ответом.
match_args {{increase|decrease}} <integer>
    Увеличивает или уменьшает число на 1.");
}

fn main() {
    let args: Vec<String> = env::args().collect();

    match args.len() {
        // аргументы не переданы
        1 => {
            println!("Я - 'match_args'. Попробуйте передать аргументы!");
        },
        // передан один аргумент
        2 => {
            match args[1].parse() {
                Ok(42) => println!("Это ответ!"),
                _ => println!("Это не ответ."),
            }
        },
        // переданы одна команда и один аргумент
        3 => {
            let cmd = &args[1];
            let num = &args[2];
            // parse the number
            let number: i32 = match num.parse() {
                Ok(n) => {
                    n
                },
                Err(_) => {
                    eprintln!("ошибка: второй аргумент не является числом");
                    help();
                    return;
                },
            };
            // парсим команду
            match &cmd[..] {
                "increase" => increase(number),
                "decrease" => decrease(number),
                _ => {
                    eprintln!("ошибка: неизвестная команда");
                    help();
                },
            }
        },
        // все остальные случаи
        _ => {
            // показываем сообщение с помощью
            help();
        }
    }
}
$ ./match_args Rust
Это не ответ.
$ ./match_args 42
Это ответ!
$ ./match_args do something
ошибка: второй аргумент не является числом
usage:
match_args <string>
    Проверяет является ли данная строка ответом.
match_args {increase|decrease} <integer>
    Увеличивает или уменьшает число на 1.
$ ./match_args do 42
ошибка: неизвестная команда
usage:
match_args <string>
    Проверяет является ли данная строка ответом.
match_args {increase|decrease} <integer>
    Увеличивает или уменьшает число на 1.
$ ./match_args increase 42
43

Foreign Function Interface

Rust предоставляет интерфейс внешних функций (Foreign Function Interface, FFI) к библиотекам, написанным на языке С. Внешние функции должны быть объявлены внутри блока extern и аннотированы при помощи атрибута #[link], который содержит имя внешней библиотеки.

use std::fmt;

// Этот extern-блок подключает библиотеку libm
#[link(name = "m")]
extern {
    // Это внешняя функция, которая считает квадратный корень
    // комплексного числа одинарной точности
    fn csqrtf(z: Complex) -> Complex;

    fn ccosf(z: Complex) -> Complex;
}

// Так как вызовы внешних функций считаются unsafe,
// принято писать над ними обёртки.
fn cos(z: Complex) -> Complex {
    unsafe { ccosf(z) }
}

fn main() {
    // z = -1 + 0i
    let z = Complex { re: -1., im: 0. };

    // вызов внешней функции - unsafe операция
    let z_sqrt = unsafe { csqrtf(z) };

    println!("квадратный корень от {:?} равен {:?}", z, z_sqrt);

    // вызов безопасного API в котором находится unsafe операция
    println!("cos({:?}) = {:?}", z, cos(z));
}

// Минимальная реализация комплексного числа одинарной точности
#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
    re: f32,
    im: f32,
}

impl fmt::Debug for Complex {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.im < 0. {
            write!(f, "{}-{}i", self.re, -self.im)
        } else {
            write!(f, "{}+{}i", self.re, self.im)
        }
    }
}

Тестирование

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

Предоставляется три стиля тестирования:

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

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

Unit-тестирование

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

Большинство модульных тестов располагается в модуле tests, помеченном атрибутом #[cfg(test)]. Тестовые функции помечаются атрибутом #[test].

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

  • assert!(expression) - паникует, если результат выражения равен false.
  • assert_eq!(left, right) и assert_ne!(left, right) - сравнивает левое и правое выражения на равенство и неравенство соответственно.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Это действительно плохая функция сложения, её назначение в данном // примере - потерпеть неудачу.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    // Обратите внимание на эту полезную идиому: импортирование имён из внешней (для mod - тестов) области видимости.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        // Это утверждение запустится и проверка не сработает.
        // Заметьте, что приватные функции также могут быть протестированы!
        assert_eq!(bad_add(1, 2), 3);
    }
}

Тесты могут быть запущены при помощи команды cargo test.

$ cargo test

running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok

failures:

---- tests::test_bad_add stdout ----
        thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
  left: `-1`,
 right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::test_bad_add

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Тесты и ?

Ни один из предшествующих unit-тестов не имеют возвращаемый тип. Но в Rust 2018 ваши unit-тесты могут вернуть Result<()>, что позволяет использовать в них ?! Это может сделать их более краткими.

fn sqrt(number: f64) -> Result<f64, String> {
    if number >= 0.0 {
        Ok(number.powf(0.5))
    } else {
        Err("у отрицательного вещественного числа нет квадратного корня".to_owned())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sqrt() -> Result<(), String> {
        let x = 4.0;
        assert_eq!(sqrt(x)?.powf(2.0), x);
        Ok(())
    }
}

Для дополнительной информации смотрите "The Edition Guide".

Тестирование паники

Для тестирования функций, которые должны паниковать при определённых обстоятельствах, используется атрибут #[should_panic]. Этот атрибут принимает необязательный параметр expected = с текстом сообщения о панике. Если ваша функция может паниковать в разных случаях, то этот параметр поможет вам быть уверенным, что вы тестируете именно ту панику, которую собирались.

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    } else if a < b {
        panic!("Divide result is zero");
    }
    a / b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_divide() {
        assert_eq!(divide_non_zero_result(10, 2), 5);
    }

    #[test]
    #[should_panic]
    fn test_any_panic() {
        divide_non_zero_result(1, 0);
    }

    #[test]
    #[should_panic(expected = "Divide result is zero")]
    fn test_specific_panic() {
        divide_non_zero_result(1, 10);
    }
}

Запуск этих тестов даст следующее:

$ cargo test

running 3 tests
test tests::test_any_panic ... ok
test tests::test_divide ... ok
test tests::test_specific_panic ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Запуск конкретных тестов

Для запуска конкретного теста надо добавить имя теста в команду cargo test.

$ cargo test test_any_panic
running 1 test
test tests::test_any_panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

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

$ cargo test panic
running 2 tests
test tests::test_any_panic ... ok
test tests::test_specific_panic ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

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

Тесты могут быть помечены атрибутом #[ignore], чтобы они были исключены из списка запускаемых командой cargo test. Такие тесты можно запустить с помощью команды cargo test -- --ignored.


#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_hundred() {
        assert_eq!(add(100, 2), 102);
        assert_eq!(add(2, 100), 102);
    }

    #[test]
    #[ignore]
    fn ignored_test() {
        assert_eq!(add(0, 0), 0);
    }
}
}
$ cargo test
running 3 tests
test tests::ignored_test ... ignored
test tests::test_add ... ok
test tests::test_add_hundred ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-ignore

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

$ cargo test -- --ignored
running 1 test
test tests::ignored_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-ignore

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Тестирование документации

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

/// Первая строка - это краткое описание функции.
///
/// Следующие строки представляют детальную документацию. Блоки кода /// начинаются трёх обратных кавычек и внутри содержат неявные
/// `fn main()` и `extern crate <cratename>`. Предположим, мы
/// тестируем крейт `doccomments`:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Ообычно документационные комментарии могут содержат секции "Examples", "Panics" and "Failures".
///
/// Следующая функция делит два числа.
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// Функция паникует, если второй аргумент равен нулю.
///
/// ```rust,should_panic
/// // паникует при делении на 0
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Ошибка деления на 0");
    }

    a / b
}

Тесты могут быть запущены при помощи cargo test:

$ cargo test
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests doccomments

running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Мотивация для документационных тестов

Главная цель документационных тестов - служить примерами предоставляемой функциональности, что является одной из самых важных рекомендаций. Это позволяет использовать примеры из документации в качестве полных фрагментов кода. Но использование ? приведёт к ошибке компиляции, так как функция main возвращает () (unit). На помощь приходит возможность скрыть из документации некоторые строки исходного кода: можно написать fn try_main() -> Result<(), ErrorType>, скрыть её и вызвать её в скрытом main с unwrap. Звучит сложно? Вот пример:

/// Использование скрытой `try_main` в документационных тестах.
///
/// ```
/// # // скрытые строки начинаются с символа `#`, но они всё ещё компилируемы!
/// # fn try_main() -> Result<(), String> { // эта линия оборачивает тело функции, которое отображается в документации
/// let res = try::try_div(10, 2)?;
/// # Ok(()) // возвращается из try_main
/// # }
/// # fn main() { // начало `main` которая выполняет `unwrap()`
/// #    try_main().unwrap(); // вызов `try_main` и извлечение результата
/// #                         // так что в случае ошибки этот тест запаникует
/// # }
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Деление на 0"))
    } else {
        Ok(a / b)
    }
}

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

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

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

Cargo ищет интеграционные тесты в каталоге tests после каталога src.

Файл src/lib.rs:

// Задаём это в кресте с именем `adder`.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Файл с тестом: tests/integration_test.rs:

#[test]
fn test_add() {
    assert_eq!(adder::add(3, 2), 5);
}

Запустим тесты можно командой cargo test:

$ cargo test
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/integration_test-bcd60824f5fbfe19

running 1 test
test test_add ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

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

Файл tests/common/mod.rs:

pub fn setup() {
    // некоторый код для настройки, создание необходимых файлов/каталогов, запуск серверов.
}

Файл с тестом: tests/integration_test.rs

// импортируем модуль с общим кодом.
mod common;

#[test]
fn test_add() {
    // используем общий код.
    common::setup();
    assert_eq!(adder::add(3, 2), 5);
}

Создание модуля как tests/common.rs также работает, но не рекомендуется, потому что средство запуска тестов будет рассматривать файл как тестовый крейт и пытаться запускать тесты внутри него.

Зависимости для разработки

Иногда возникает необходимость иметь зависимости только для тестов (или примеров, или бенчмарков). Такие зависимости добавляются в Cargo.toml в раздел [dev-dependencies] . Эти зависимости не распространяются как зависимости на другие пакеты, которые зависят от этого пакета.

Одним из таких примеров является пакет pretty_assertions, который расширяет стандартные макросы assert_eq! и assert_ne!, чтобы обеспечить цветной вывод отличий.
Файл Cargo.toml :

# Стандартное содержимое крейта здесь пропущено
[dev-dependencies]
pretty_assertions = "1"

Файл src/lib.rs:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq; // Крейт для использования только во время тестировании. Он не может быть использован вне кода тестов.

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

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

Документация Cargo по указанию зависимостей.

Небезопасные операции

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

  • разыменование сырых указателей
  • вызов функций или методов, которые являются unsafe (включая вызов функции через FFI см. предыдущую главу книги)
  • доступ или изменение статических изменяемых переменных
  • реализация небезопасных типажей

Сырые указатели

Сырые указатели * и ссылки &T имеют схожую функциональность, но ссылки всегда безопасны, потому что они гарантированно указывают на достоверные данные за счёт механизма проверки заимствований. Разыменование же сырого указателя можно выполнить только через небезопасный блок.

fn main() {
    let raw_p: *const u32 = &10;

    unsafe {
        assert!(*raw_p == 10);
    }
}

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

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

use std::slice;

fn main() {
    let some_vector = vec![1, 2, 3, 4];

    let pointer = some_vector.as_ptr();
    let length = some_vector.len();

    unsafe {
        let my_slice: &[u32] = slice::from_raw_parts(pointer, length);

        assert_eq!(some_vector.as_slice(), my_slice);
    }
}

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

Совместимость

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

Сырые идентификаторы

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

Например, рассмотрим крейт foo, скомпилированный с 2015 редакцией Rust, и который экспортирует функцию с именем try. Это ключевое слово зарезервировано для новой функциональности в 2018 редакции, из-за чего без сырых идентификаторов мы не можем назвать так функцию.

extern crate foo;

fn main() {
    foo::try();
}

Вы получите ошибку:

error: expected identifier, found keyword `try`
 --> src/main.rs:4:4
  |
4 | foo::try();
  |      ^^^ expected identifier, found keyword

Вы можете записать это при помощи сырого идентификатора:

extern crate foo;

fn main() {
    foo::r#try();
}

Meta

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

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

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

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

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

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

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

#![crate_name = "doc"]

/// Эта структура представляет человека
pub struct Person {
    /// Человек должен иметь имя вне зависимости от того, на сколько Джульетта его ненавидит
    name: String,
}

impl Person {
    /// Возвращает человека с данным ему именем
    ///
    /// # Аргументы
    ///
    /// * `name` - Срез строки, содержащий имя человека
    ///
    /// # Пример
    ///
    /// ```
    /// // Вы можете писать код на Rust внутри комментариев.
    /// // Если вы передадите `--test` в `rustdoc`, то он проверит его!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Дружественное приветствие!
    ///
    /// Говорит "Привет, [name]" для `Person` у которого он вызывается.
    pub fn hello(& self) {
        println!("Привет, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

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

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

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

Playpen