Rust на примерах

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

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

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

Кроме этого можно посмотреть исходный код этого сайта или оригинала.

И так, давайте начнём!

Привет мир

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

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

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

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

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

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

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

$ rustc hello.rs

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

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

Задание

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

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

Комментарии

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

  • Обычные комментарии, которые игнорируются компилятором:
  • // Однострочный комментарий. Который завершается в конце строки.
  • /* Блочный комментарий, который продолжается до завершающего символа. */
  • /// Генерация документации для функции.
  • //! Генерация документации для модуля.
fn main() {
    // Это пример однострочного комментария
    // Обратите внимание на то, что строка комментария начинается с двух косых черт
    // Компилятор проигнорирует все, что написано внутри комментария

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

    // Запустите данный код. Видите? Теперь попробуйте удалить комментарий и запустить ещё раз.

    /*
     * Это другой тип комментария - блочный. В целом, рекомендованным стилем
     * комментирования кода является однострочный комментарий, но блочный комментарий
     * является очень полезным в случае, когда необходимо проигнорировать какую-то
     * большую часть кода. /* Блочные комментарии могут быть /* вложенными, */ */
     * так что комментирование всего кода в функции main займёт всего пару нажатий
     * клавиш на клавиатуре. /*/*/* Попробуйте сами! */*/*/
     */

     /*
     Обратите внимание, что столбец из * в прошлом комментарии был добавлен
     для стиля. Он является необязательным.
     */

     // Обратите внимание, как блочный комментарий позволяет легко управлять выражениями
     // однострочный комментарии не подходит для этой цели. Удалите блочный комментарий
     // и посмотрите, как изменится результат:
     let x = 5 + /* 90 + */ 5;
     println!("`x` равен 10 или 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:>0width$}", 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, потому что стандартная библиотека предоставляет реализацию для данного типа. Для отображения собственных типов потребуется больше дополнительных шагов.

Задания

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

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

std::fmt, макросы, структуры, and traits

Debug

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

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


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

// Атрибут `derive` автоматически реализует
// необходимые методы, чтобы была возможность
// печатать данную `структуру` с помощью `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, and структуры

Display

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


# #![allow(unused_variables)]
#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, macros, структуры, типажи, and use

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

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

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

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

Кроме того, Вы также можете использовать макрос try!', который работает так же. Это немного более подробно и больше не рекомендуется, но вы все равно можете увидеть его в старом коде на Rust. Использованиеtry!` выглядит так:

try!(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, ?, and 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; // Mutable `i32`
    mutable = 21;
    
    // Ошибка! Тип переменной изменить нельзя.
    mutable = true;
    
    // Переменные могут быть переопределены с помощью затемнения.
    let mutable = true;
}

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

the std library, mut, inference, and shadowing

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

Целочисленное 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 строки, чтобы увидеть ошибку компилятораr

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

    // Срезы могут указывать на часть массива
    println!("заимствуем часть массива как срез");
    analyze_slice(&ys[1 .. 4]);

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

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

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

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

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

Структуры

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

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

// Единичная структура
struct Nil;

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

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

// Структуры могут быть использованы как поля другой структуры
#[allow(dead_code)]
struct Rectangle {
    p1: Point,
    p2: Point,
}

fn main() {
    // Создаём структуру с помощью короткой инициализации полей
    let name = "Петя";
    let age = 27;
    let peter = Person { name, age };
    
    // Дебаг вывод структуры
    println!("{:?}", peter);
    
    
    // Создаём структуру `Point`
    let point: Point = Point { x: 0.3, y: 0.4 };

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

    // Деструктурируем `Point` создавая связь с помощью `let`
    let Point { x: my_x, y: my_y } = point;

    let _rectangle = Rectangle {
        // инициализация структуры так же является выражением
        p1: Point { x: my_y, y: my_x },
        p2: point,
    };

    // Создаём связь с единичной структурой
    let _nil = Nil;

    // Создаём связь с кортежной структурой
    let pair = Pair(1, 0.1);

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

    println!("Pair хранит в себе {:?} и {:?}", integer, decimal);
}

Задание

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

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

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

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

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

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

// Создадим `enum`, который классифицирует веб-событие. Обратите внимание как
// имена и тип вместе определяют вариант:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Каждый из них уникален.
enum WebEvent {
    // `enum` так же может быть `единичным`,
    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!("clicked at 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);
}

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

атрибуты, сопоставление с образцом, функции, и строки

Декларация 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;
    // Эквивалентно to `Work::Civilian`.
    let work = Civilian;

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

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

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

сопоставление с образцом и use

С-подобные

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

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

Частным случаем является литерал "string". Он может быть напрямую назначен переменной с временем жизни 'static, т.к сигнатура его типа: &'static str требует время жизни 'static. Для всех остальных ссылочных типов должно быть указано время жизни 'static. Это может показаться незначительным, но в требовании явной аннотации типов и скрываются различия.

// Константы объявлены в глобальной области видимости.
static LANGUAGE: &'static 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 { "меньше" });

    // Ошибка! `константы` нельзя изменить.
    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!("Целое: {:?}", copied_integer);
    println!("Логическое: {:?}", a_boolean);
    println!("Встречайте единичное значение: {:?}", unit);

    // Компилятор предупреждает о неиспользуемых переменных; эти предупреждения можно
    // отключить используя подчёркивание перед именем переменной
    let _unused_variable = 3u32;
    let noisy_unused_variable = 2u32;
    // ИСПРАВЬТЕ ^ Добавьте подчёркивание
}

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

По умолчанию связывание переменных является неизменяемым, но с помощью модификатора 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);

        // Эта переменная *затеняет* собой внешнюю
        let long_lived_binding = 5_f32;

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

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

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

    // Это связывание так же *скрывает* собой предыдущие
    let long_lived_binding = 'a';

    println!("outer long: {}", long_lived_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);
}

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

Типы

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;

    println!("Casting: {} -> {} -> {}", decimal, integer, character);

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

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

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

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

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

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

    // повторяем примеры
    // 1000 as u8 -> 232
    println!("1000 as a i8 is : {}", 1000 as i8);
    // и дополнительный код 232 - это -24
    println!(" 232 as a i8 is : {}", 232 as i8);
}

Литералы

Числовые литералы могут быть обозначены добавлением типа в качестве суффикса. Например, чтобы указать, что литерал 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));
}

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

  • fun(&foo) используется для передаче аргумента в функцию по ссылке, вместо передачи по значению (fun(foo)). Подробнее см. заимствование или соответствующую главу в книге.
  • 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 используется, чтобы задать новое имя существующему типу. Имя типа должно быть в стиле CamelCase, иначе компилятор выдаст предупреждение. Исключением являются примитивные типы: 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>.

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

Атрибуты

Conversion

Rust addresses conversion between types by the use of traits. The generic conversions will use the From and Into traits. However there are more specific ones for the more common cases, in particular when converting to and from Strings.

From and Into

The From and Into traits are inherently linked, and this is actually part of its implementation. If you are able to convert type A from type B, then it should be easy to believe that we should be able to convert type B to type A.

From

The From trait allows for a type to define how to create itself from another type, hence providing a very simple mechanism for converting between several types. There are numerous implementations of this trait within the standard library for conversion of primitive and common types.

For example we can easily convert a str into a String


# #![allow(unused_variables)]
#fn main() {
let my_str = "hello";
let my_string = String::from(my_str);
#}

We can do similar for defining a conversion for our own type.

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!("My number is {:?}", num);
}

Into

The Into trait is simply the reciprocal of the From trait. That is, if you have implemented the From trait for your type you get the Into implementation for free.

Using the Into trait will typically require specification of the type to convert into as the compiler is unable to determine this most of the time. However this is a small trade-off considering we get the functionality for free.

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;
    // Try removing the type declaration
    let num: Number = int.into();
    println!("My number is {:?}", num);
}

To and from Strings

ToString

To convert any type to a String it is as simple as implementing the ToString trait for the type.

use std::string::ToString;

struct Circle {
    radius: i32
}

impl ToString for Circle {
    fn to_string(&self) -> String {
        format!("Circle of radius {:?}", self.radius)
    }
}

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

Parsing a String

One of the more common types to convert a string into is a number. The idiomatic approach to this is to use the parse function and provide the type for the function to parse the string value into, this can be done either without type inference or using the 'turbofish' syntax.

This will convert the string into the type specified so long as the FromStr trait is implemented for that type. This is implemented for numerous types within the standard library. To obtain this functionality on a user defined type simply implement the FromStr trait for that type.

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

    let sum = parsed + turbo_parsed;
    println!{"Sum: {:?}", 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 is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", 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 loops

for and range

Конструкция 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);
        }
    }
}

Alternatively, a..=b can be used for a range that is inclusive on both ends. The above can be written as:

#![feature(inclusive_range_syntax)]

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    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 and iterators

The for in construct is able to interact with an Iterator in several ways. As discussed in with the Iterator trait, if not specified, the for loop will apply the into_iter function on the collection provided to convert the collection into an iterator. This is not the only means to convert a collection into an iterator however, the other functions available include iter and iter_mut.

These 3 functions will return different views of the data within your collection.

  • iter - This borrows each element of the collection through each iteration. Thus leaving the collection untouched and available for reuse after the loop.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter() {
        match name {
            &"Ferris" => println!("There is a rustacean among us!"),
            _ => println!("Hello {}", name),
        }
    }
}
  • into_iter - This consumes the collection so that on each iteration the exact data is provided. Once the collection has been consumed it is no longer available for reuse as it has been 'moved' within the loop.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        match name {
            "Ferris" => println!("There is a rustacean among us!"),
            _ => println!("Hello {}", name),
        }
    }
}
  • iter_mut - This mutably borrows each element of the collection, allowing for the collection to be modified in place.
fn main() {
    let mut names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter_mut() {
        match name {
            &mut "Ferris" => println!("There is a rustacean among us!"),
            _ => println!("Hello {}", name),
        }
    }
}

In the above snippets note the type of match branch, that is the key difference in the types or iteration. The difference in type then of course implies differing actions that are able to be performed.

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

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

match

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

fn main() {
    let number = 13;
    // ЗАДАНИЕ ^ Попробуйте присвоить `number` другое значение

    println!("Tell me about {}", number);
    match number {
        // Сопоставление с одним значением
        1 => println!("One!"),
        // Сопоставление с несколькими значениями
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        // Сопоставление с диапазоном значений
        13...19 => println!("A teen"),
        // Обработка остальных случаев
        _ => println!("Ain't special"),
    }

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

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

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

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

Кортежи

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

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

    println!("Tell me about {:?}", pair);
    // Match можно использовать для деструктуризации кортежей
    match pair {
        // Деструктурируем два значения
        (0, y) => println!("Первое значение `0`, а `y` равно `{:?}`", y),
        (x, 0) => println!("`x` равно `{:?}`, а второе значение `0`", x),
        _      => 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(...)], цветовая модель and enum

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

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

  • Разыменование использует *
  • Деструктуризация использует &, 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);
        },
    }
}

Структуры

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

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

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

структуры, The ref pattern

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

Внутри конструкции 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!("Мне кажется, что я ещё не родился..."),
        // Можно использовать `match` для конкретного значения в пределе 1 ... 12, но
        // как тогда определять возраст ребёнка, например? Вместо этого, свяжем `n` с
        // последовательностью от 1 до 12. Теперь мы можем сообщить о возрасте.
        n @ 1  ... 12 => println!("Я ребёнок! Мне {:?} лет.", n),
        n @ 13 ... 19 => println!("Я подросток. Мне {:?} лет.", n),
        // Больше пределов нет. Возвращаем результат.
        n             => println!("Я уже довольно старый, мне {:?}.", n),
    }
}

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

функции

if let

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


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

match optional {
    Some(i) => {
        println!("Это действительно очень длинная строка и `{:?}`", i);
        // ^ Необходимо два вложения для того, чтобы просто деструктурировать
        // `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 is foobar");
    }
    
    // Переменная b не соответствует Foo::Bar
    // So this will print nothing
    if let Foo::Bar = b {
        println!("b is foobar");
    }
    
    // Переменная c соответствует Foo::Qux, которая имеет значение
    // аналогичное Some() как в предыдущем примере:
    if let Foo::Qux(value) = c {
        println!("c is {}", value);
    }
}

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

enum, Option, и RFC

while let

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


# #![allow(unused_variables)]
#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 используя функции!

// В отличие от С/С++, нет никаких ограничений касаемо порядка определений функций
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 + 1 {
        fizzbuzz(n);
    }
}

методы

Методы - это функций прикреплённые к объектам. Эти методы имеют допуск к данным объекта и его другим методам через ключевое слово self. Методы определяются под блоком impl.

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

// Блок реализаций, все методы `Point` идут сюда
impl 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!("Destroying Pair({}, {})", first, second);

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

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

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

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

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

    // Хорошо, изменяемый объект может вызывать изменяемые методы
    square.translate(1.0, 1.0);

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

    pair.destroy();

    // Ошибка! `destroy` вызывает "съеденный" `pair`
    //pair.destroy();
    // ЗАДАНИЕ ^ Попробуйте удалить комментарий
}

Замыкания

Замыкания в 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();
    print();

    let mut count = 0;

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

    // Вызываем замыкание.
    inc();
    inc();

    //let reborrow = &mut count;
    // ^ TODO: попробуйте раскомментировать эту строку.

    // Тип без возможности копирования.
    let movable = Box::new(3);

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

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

Using move before vertical pipes forces closure to take ownership of captured variables:

fn main() {
    // `Vec` has non-copy semantics.
    let haystack = vec![1, 2, 3];

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

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

    // `println!("There're {} elements in vec", haystack.len());`
    // ^ Uncommenting above line will result in compile-time error
    // because borrow checker doesn't allow re-using variable after it
    // has been moved.
    
    // Removing `move` from closure's signature will cause closure
    // to borrow _haystack_ variable immutably, hence _haystack_ is still
    // available and uncommenting above line will not cause an error.
}

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

Box and std::mem::drop

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

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

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

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

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

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

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

// Функция, которая принимает замыкание в качестве аргумента и вызывает его.
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, and FnOnce

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

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


# #![allow(unused_variables)]
#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, and 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, and FnOnce

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

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

Возможные типажи для возвращаемых значений немного отличаются от прежних:

  • Fn: как раньше
  • FnMut: как раньше
  • FnOnce: здесь присутствуют некоторые неожиданности, поэтому необходим тип FnBox, но он нестабилен в настоящее время. В будущем ожидаются изменения этой ситуации.

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

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

    Box::new(move || println!("Это a: {}", text))
}

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

    Box::new(move || println!("Это a: {}", text))
}

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

    fn_plain();
    fn_mut();
}

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

Упаковка, Fn, FnMut, and Обобщённые типы.

Примеры из библиотеки 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

Iterator::find - это функция, которая принимает итератор и возвращает первый элемент, который удовлетворяет предикату, в виде Option. Её объявление:

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

    // `find` принимает `&mut self`, что означает заимствование и
    // изменение, но не поглощение `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();

    // Ссылка на это даёт `&&i32`. Приводим к `i32`.
    println!("Найти 2 в vec1: {:?}", iter     .find(|&&x| x == 2));
    // Ссылка на это даёт `&i32`. Приводим к `i32`.
    println!("Найти 2 в vec2: {:?}", into_iter.find(| &x| x == 2));

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

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

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

std::iter::Iterator::find

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

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!("imperative style: {}", acc);

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

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

Diverging functions

Diverging functions never return. They are marked using !, which is an empty type.


# #![allow(unused_variables)]
#fn main() {
fn foo() -> ! {
    panic!("This call never returns.");
}
#}

As opposed to all the other types, this one cannot be instantiated, because the set of all possible values this type can have is empty. Note, that it is different from the () type, which has exactly one possible value.

For example, this functions returns as usual, although there is no information in the return value.

fn some_fn() {
    ()
}

fn main() {
    let a: () = some_fn();
    println!("This functions returns and you can see this line.")
}

As opposed to this function, which will never return the control back to the caller.

#![feature(never_type)]

fn main() {
    let x: ! = panic!("This call never returns.");
    println!("You will never see this line!");
}

Although this might seem like an abstract concept, it is in fact very useful and often handy. The main advantage of this type is that it can be cast to any other one and therefore used at places where an exact type is required, for instance in match branches. This allows us to write code like this:

fn main() {
    fn sum_odd_numbers(up_to: u32) -> u32 {
        let mut acc = 0;
        for i in 0..up_to {
            // Notice that the return type of this match expression must be u32
            // because of the type of the "addition" variable.
            let addition: u32 = match i%2 == 1 {
                // The "i" variable is of type u32, which is perfectly fine.
                true => i,
                // On the other hand, the "continue" expression does not return
                // u32, but it is still fine, because it never returns and therefore
                // does not violate the type requirements of the match expression.
                false => continue,
            };
            acc += addition;
        }
        acc
    }
    println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9));
}

It is also the return type of functions that loop forever (e.g. loop {}) like network servers or functions that terminates the process (e.g. exit()).

Модули

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

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

Видимость

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

// Привязать путь `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
|   |-- mod.rs
|   `-- nested.rs
`-- split.rs

В split.rs:

// Эта декларация найдёт файл с именем `my.rs` или `my/mod.rs` и вставит
// его содержимое внутрь модуля с именем `my` в этой области видимости
mod my;

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

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

    function();

    my::indirect_access();

    my::nested::function();
}

В my/mod.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 к rustc.

Библиотеки

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

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();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

Библиотеки получают префикс «lib», и по умолчанию имеют то же имя, что и их контейнеры, но это имя можно изменить с помощью атрибута crate_name.

extern crate

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

// Ссылка на `библиотеку`. Импортируем элементы, как модуль `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:

# A binary
cargo new foo

# OR A library
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 есть встроенная поддержка примеров, модульных тестов, интеграционных тестов и тестов на производительность.

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

Testing

As we know testing is integral to any piece of software! Rust has first-class support for unit and integration testing (see this chapter in TRPL).

From the testing chapters linked above, we see how to write unit tests and integration tests. Organizationally, we can place unit tests in the modules they test and integration tests in their own tests/ directory:

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

Each file in tests is a separate integration test.

cargo naturally provides an easy way to run all of your tests!

cargo test

You should see output like this:

$ 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

You can also run tests whose name matches a pattern:

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

One word of caution: Cargo may run multiple tests concurrently, so make sure that they don't race with each other. For example, if they all output to a file, you should make them write to different files.

Атрибуты

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

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

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

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

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

#[attribute(value, value2)]


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

dead_code

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

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

fn used_function() {}

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

fn noisy_unused_function() {}
// ИСПРАВЬТЕ ^ Добавьте атрибут `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!("вызвана rary's `public_function()`");
}

fn private_function() {
    println!("вызвана rary's `private_function()`");
}

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

    private_function();
}

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

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

cfg

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

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

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

// Эта функция будет скомпилирована только в том случае, если целевая ОС будет 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!");
    }
}

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

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

Обобщения

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

Обобщить параметр типа можно используя угловые скобки и верхний верблюжий регистр: <Aaa, Bbb, ...>. "Обобщённые параметры типа" обычно представлены как <T>. В Rust, "обобщённым" также принято называть все, что может принимать один или более обобщённых параметров типа <T>. Любой тип, указанный в качестве параметра обобщённого типа, является обобщённым, а всё остальное является конкретным (не обобщённым).

Например, объявление обобщённой функции foo принимающей аргумент T любого типа:

fn foo<T>(arg: T) { ... }

Поскольку T был объявлен как обобщённый тип, посредством <T>, он считается обобщённым когда используется как (arg: T). Это работает даже если T был определён как структура.

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

// A concrete type `A`.
// Конкретный тип `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_variables)]
#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());
}

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

functions returning references, impl, and 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, Структуры, и Типажи

Ограничения

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

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

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

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

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

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 и типажи

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

The newtype idiom gives compile time guarantees that the right type of value is supplied to a program.

For example, an age verification function that checks age in years, must be given a value of type 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));
}

Uncomment the last print statement to observe that the type supplied must be Years.

See also:

structs

Associated items

"Associated Items" refers to a set of rules pertaining to items of various types. It is an extension to trait generics, and allows traits to internally define new items.

One such item is called an associated type, providing simpler usage patterns when the trait is generic over its container type.

See also:

RFC

The Problem

A trait that is generic over its container type has type specification requirements - users of the trait must specify all of its generic types.

In the example below, the Contains trait allows the use of the generic types A and B. The trait is then implemented for the Container type, specifying i32 for A and B so that it can be used with fn difference().

Because Contains is generic, we are forced to explicitly state all of the generic types for fn difference(). In practice, we want a way to express that A and B are determined by the input C. As you will see in the next section, associated types provide exactly that capability.

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains<A, B> {
    fn contains(&self, &A, &B) -> bool; // Explicitly requires `A` and `B`.
    fn first(&self) -> i32; // Doesn't explicitly require `A` or `B`.
    fn last(&self) -> i32;  // Doesn't explicitly require `A` or `B`.
}

impl Contains<i32, i32> for Container {
    // True if the numbers stored are equal.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }

    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    fn last(&self) -> i32 { self.1 }
}

// `C` contains `A` and `B`. In light of that, having to express `A` and
// `B` again is a nuisance.
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!("Does container contain {} and {}: {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(&container));
}

See also:

structs, and traits

Associated types

The use of "Associated types" improves the overall readability of code by moving inner types locally into a trait as output types. Syntax for the trait definition is as follows:


# #![allow(unused_variables)]
#fn main() {
// `A` and `B` are defined in the trait via the `type` keyword.
// (Note: `type` in this context is different from `type` when used for
// aliases).
trait Contains {
    type A;
    type B;

    // Updated syntax to refer to these new types generically.
    fn contains(&self, &Self::A, &Self::B) -> bool;
}
#}

Note that functions that use the trait Contains are no longer required to express A or B at all:

// Without using associated types
fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> { ... }

// Using associated types
fn difference<C: Contains>(container: &C) -> i32 { ... }

Let's rewrite the example from the previous section using associated types:

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains {
    // Define generic types here which methods will be able to utilize.
    type A;
    type B;

    fn contains(&self, &Self::A, &Self::B) -> bool;
    fn first(&self) -> i32;
    fn last(&self) -> i32;
}

impl Contains for Container {
    // Specify what types `A` and `B` are. If the `input` type
    // is `Container(i32, i32)`, the `output` types are determined
    // as `i32` and `i32`.
    type A = i32;
    type B = i32;

    // `&Self::A` and `&Self::B` are also valid here.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }
    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    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!("Does container contain {} and {}: {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());
    
    println!("The difference is: {}", difference(&container));
}

Phantom type parameters

A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time.

Data types can use extra generic type parameters to act as markers or to perform type checking at compile time. These extra parameters hold no storage values, and have no runtime behavior.

In the following example, we combine std::marker::PhantomData with the phantom type parameter concept to create tuples containing different data types.

use std::marker::PhantomData;

// A phantom tuple struct which is generic over `A` with hidden parameter `B`.
#[derive(PartialEq)] // Allow equality test for this type.
struct PhantomTuple<A, B>(A,PhantomData<B>);

// A phantom type struct which is generic over `A` with hidden parameter `B`.
#[derive(PartialEq)] // Allow equality test for this type.
struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }

// Note: Storage is allocated for generic type `A`, but not for `B`.
//       Therefore, `B` cannot be used in computations.

fn main() {
    // Here, `f32` and `f64` are the hidden parameters.
    // PhantomTuple type specified as `<char, f32>`.
    let _tuple1: PhantomTuple<char, f32> = PhantomTuple('Q', PhantomData);
    // PhantomTuple type specified as `<char, f64>`.
    let _tuple2: PhantomTuple<char, f64> = PhantomTuple('Q', PhantomData);

    // Type specified as `<char, f32>`.
    let _struct1: PhantomStruct<char, f32> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };
    // Type specified as `<char, f64>`.
    let _struct2: PhantomStruct<char, f64> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };
    
    // Compile-time Error! Type mismatch so these cannot be compared:
    //println!("_tuple1 == _tuple2 yields: {}",
    //          _tuple1 == _tuple2);
    
    // Compile-time Error! Type mismatch so these cannot be compared:
    //println!("_struct1 == _struct2 yields: {}",
    //          _struct1 == _struct2);
}

See also:

Derive, struct, and TupleStructs

Testcase: unit clarification

A useful method of unit conversions can be examined by implementing Add with a phantom type parameter. The Add trait is examined below:

// This construction would impose: `Self + RHS = Output`
// where RHS defaults to Self if not specified in the implementation.
pub trait Add<RHS = Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

// `Output` must be `T<U>` so that `T<U> + T<U> = T<U>`.
impl<U> Add for T<U> {
    type Output = T<U>;
    ...
}

The whole implementation:

use std::ops::Add;
use std::marker::PhantomData;

/// Create void enumerations to define unit types.
#[derive(Debug, Clone, Copy)]
enum Inch {}
#[derive(Debug, Clone, Copy)]
enum Mm {}

/// `Length` is a type with phantom type parameter `Unit`,
/// and is not generic over the length type (that is `f64`).
///
/// `f64` already implements the `Clone` and `Copy` traits.
#[derive(Debug, Clone, Copy)]
struct Length<Unit>(f64, PhantomData<Unit>);

/// The `Add` trait defines the behavior of the `+` operator.
impl<Unit> Add for Length<Unit> {
     type Output = Length<Unit>;

    // add() returns a new `Length` struct containing the sum.
    fn add(self, rhs: Length<Unit>) -> Length<Unit> {
        // `+` calls the `Add` implementation for `f64`.
        Length(self.0 + rhs.0, PhantomData)
    }
}

fn main() {
    // Specifies `one_foot` to have phantom type parameter `Inch`.
    let one_foot:  Length<Inch> = Length(12.0, PhantomData);
    // `one_meter` has phantom type parameter `Mm`.
    let one_meter: Length<Mm>   = Length(1000.0, PhantomData);

    // `+` calls the `add()` method we implemented for `Length<Unit>`.
    //
    // Since `Length` implements `Copy`, `add()` does not consume
    // `one_foot` and `one_meter` but copies them into `self` and `rhs`.
    let two_feet = one_foot + one_foot;
    let two_meters = one_meter + one_meter;

    // Addition works.
    println!("one foot + one_foot = {:?} in", two_feet.0);
    println!("one meter + one_meter = {:?} mm", two_meters.0);

    // Nonsensical operations fail as they should:
    // Compile-time Error: type mismatch.
    //let one_feter = one_foot + one_meter;
}

See also:

Borrowing (&), Bounds (X: Y), enum, impl & self, Overloading, ref, Traits (X for Y), and TupleStructs.

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

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

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

Mutability

Mutable data can be mutably borrowed using &mut T. This is called a mutable reference and gives read/write access to the borrower. In contrast, &T borrows the data via an immutable reference, and the borrower can read the data but not modify it:

#[allow(dead_code)]
#[derive(Clone, Copy)]
struct Book {
    // `&'static str` is a reference to a string allocated in read only memory
    author: &'static str,
    title: &'static str,
    year: u32,
}

// This function takes a reference to a book
fn borrow_book(book: &Book) {
    println!("I immutably borrowed {} - {} edition", book.title, book.year);
}

// This function takes a reference to a mutable book and changes `year` to 2014
fn new_edition(book: &mut Book) {
    book.year = 2014;
    println!("I mutably borrowed {} - {} edition", book.title, book.year);
}

fn main() {
    // Create an immutable Book named `immutabook`
    let immutabook = Book {
        // string literals have type `&'static str`
        author: "Douglas Hofstadter",
        title: "Gödel, Escher, Bach",
        year: 1979,
    };

    // Create a mutable copy of `immutabook` and call it `mutabook`
    let mut mutabook = immutabook;
    
    // Immutably borrow an immutable object
    borrow_book(&immutabook);

    // Immutably borrow a mutable object
    borrow_book(&mutabook);
    
    // Borrow a mutable object as mutable
    new_edition(&mut mutabook);
    
    // Error! Cannot borrow an immutable object as mutable
    new_edition(&mut immutabook);
    // FIXME ^ Comment out this line
}

See also:

static

Замораживание

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

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

    {
        // Заимствовать `_mutable_integer`
        let _large_integer = &_mutable_integer;

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

        // `_large_integer` покидает область видимости
    }

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

Aliasing

Data can be immutably borrowed any number of times, but while immutably borrowed, the original data can't be mutably borrowed. On the other hand, only one mutable borrow is allowed at a time. The original data can be borrowed again only after the mutable reference goes out of scope.

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;

        // Data can be accessed via the references and the original owner
        println!("Point has coordinates: ({}, {}, {})",
                 borrowed_point.x, another_borrow.y, point.z);

        // Error! Can't borrow point as mutable because it's currently
        // borrowed as immutable.
        //let mutable_borrow = &mut point;
        // TODO ^ Try uncommenting this line

        // Immutable references go out of scope
    }

    {
        let mutable_borrow = &mut point;

        // Change data via mutable reference
        mutable_borrow.x = 5;
        mutable_borrow.y = 2;
        mutable_borrow.z = 1;

        // Error! Can't borrow `point` as immutable because it's currently
        // borrowed as mutable.
        //let y = &point.y;
        // TODO ^ Try uncommenting this line

        // Error! Can't print because `println!` takes an immutable reference.
        //println!("Point Z coordinate is {}", point.z);
        // TODO ^ Try uncommenting this line

        // Ok! Mutable references can be passed as immutable to `println!`
        println!("Point has coordinates: ({}, {}, {})",
                 mutable_borrow.x, mutable_borrow.y, mutable_borrow.z);

        // Mutable reference goes out of scope
    }

    // Immutable references to point are allowed again
    let borrowed_point = &point;
    println!("Point now has coordinates: ({}, {}, {})",
             borrowed_point.x, borrowed_point.y, borrowed_point.z);
}

The ref pattern

When doing pattern matching or destructuring via the let binding, the ref keyword can be used to take references to the fields of a struct/tuple. The example below shows a few instances where this can be useful:

#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }

fn main() {
    let c = 'Q';

    // A `ref` borrow on the left side of an assignment is equivalent to
    // an `&` borrow on the right side.
    let ref ref_c1 = c;
    let ref_c2 = &c;

    println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2);

    let point = Point { x: 0, y: 0 };

    // `ref` is also valid when destructuring a struct.
    let _copy_of_x = {
        // `ref_to_x` is a reference to the `x` field of `point`.
        let Point { x: ref ref_to_x, y: _ } = point;

        // Return a copy of the `x` field of `point`.
        *ref_to_x
    };

    // A mutable copy of `point`
    let mut mutable_point = point;

    {
        // `ref` can be paired with `mut` to take mutable references.
        let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;

        // Mutate the `y` field of `mutable_point` via a mutable reference.
        *mut_ref_to_y = 1;
    }

    println!("point is ({}, {})", point.x, point.y);
    println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y);

    // A mutable tuple that includes a pointer
    let mut mutable_tuple = (Box::new(5u32), 3u32);
    
    {
        // Destructure `mutable_tuple` to change the value of `last`.
        let (_, ref mut last) = mutable_tuple;
        *last = 2u32;
    }
    
    println!("tuple is {:?}", 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. ─────────────────────────────────────┘

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

Explicit annotation

The borrow checker uses explicit lifetime annotations to determine how long references should be valid. In cases where lifetimes are not elided1, Rust requires explicit annotations to determine what the lifetime of a reference should be. The syntax for explicitly annotating a lifetime uses an apostrophe character as follows:

foo<'a>
// `foo` has a lifetime parameter `'a`

Similar to closures, using lifetimes requires generics. Additionally, this lifetime syntax indicates that the lifetime of foo may not exceed that of 'a. Explicit annotation of a type has the form &'a T where 'a has already been introduced.

In cases with multiple lifetimes, the syntax is similar:

foo<'a, 'b>
// `foo` has lifetime parameters `'a` and `'b`

In this case, the lifetime of foo cannot exceed that of either 'a or 'b.

See the following example for explicit lifetime annotation in use:

// `print_refs` takes two references to `i32` which have different
// lifetimes `'a` and `'b`. These two lifetimes must both be at
// least as long as the function `print_refs`.
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

// A function which takes no arguments, but has a lifetime parameter `'a`.
fn failed_borrow<'a>() {
    let _x = 12;

    // ERROR: `_x` does not live long enough
    //let y: &'a i32 = &_x;
    // Attempting to use the lifetime `'a` as an explicit type annotation 
    // inside the function will fail because the lifetime of `&_x` is shorter
    // than that of `y`. A short lifetime cannot be coerced into a longer one.
}

fn main() {
    // Create variables to be borrowed below.
    let (four, nine) = (4, 9);
    
    // Borrows (`&`) of both variables are passed into the function.
    print_refs(&four, &nine);
    // Any input which is borrowed must outlive the borrower. 
    // In other words, the lifetime of `four` and `nine` must 
    // be longer than that of `print_refs`.
    
    failed_borrow();
    // `failed_borrow` contains no references to force `'a` to be 
    // longer than the lifetime of the function, but `'a` is longer.
    // Because the lifetime is never constrained, it defaults to `'static`.
}
1

elision implicitly annotates lifetimes and so is different.

See also:

generics and closures

Функции

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

  • любая ссылка должна иметь аннотированное время жизни
  • любая возвращаемая ссылка должна иметь то же время жизни, что входящая ссылка или 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);
}

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

Структуры

Ограничения

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

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

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

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

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

generics, bounds in generics, and multiple bounds in generics

Coercion

A longer lifetime can be coerced into a shorter one so that it works inside a scope it normally wouldn't work in. This comes in the form of inferred coercion by the Rust compiler, and also in the form of declaring a lifetime difference:

// Here, Rust infers a lifetime that is as short as possible.
// The two references are then coerced to that lifetime.
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Longer lifetime
    
    {
        let second = 3; // Shorter lifetime
        
        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&first, &second));
    };
}

Static

A 'static lifetime is the longest possible lifetime, and lasts for the lifetime of the running program. A 'static lifetime may also be coerced to a shorter lifetime. There are two ways to make a variable with 'static lifetime, and both are stored in the read-only memory of the binary:

  • Make a constant with the static declaration.
  • Make a string literal which has type: &'static str.

See the following example for a display of each method:

// Make a constant with `'static` lifetime.
static NUM: i32 = 18;

// Returns a reference to `NUM` where its `'static` 
// lifetime is coerced to that of the input argument.
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

fn main() {
    {
        // Make a `string` literal and print it:
        let static_string = "I'm in read-only memory";
        println!("static_string: {}", static_string);

        // When `static_string` goes out of scope, the reference
        // can no longer be used, but the data remains in the binary.
    }
    
    {
        // Make an integer to use for `coerce_static`:
        let lifetime_num = 9;

        // Coerce `NUM` to lifetime of `lifetime_num`:
        let coerced_static = coerce_static(&lifetime_num);

        println!("coerced_static: {}", coerced_static);
    }
    
    println!("NUM: {} stays accessible!", NUM);
}

See also:

'static constants

Elision

Some lifetime patterns are overwelmingly common and so the borrow checker will implicitly add them to save typing and to improve readability. This process of implicit addition is called elision. Elision exists in Rust solely because these patterns are common.

The following code shows a few examples of elision. For a more comprehensive description of elision, see lifetime elision in the book.

// `elided_input` and `annotated_input` essentially have identical signatures
// because the lifetime of `elided_input` is elided by the compiler:
fn elided_input(x: &i32) {
    println!("`elided_input`: {}", x);
}

fn annotated_input<'a>(x: &'a i32) {
    println!("`annotated_input`: {}", x);
}

// Similarly, `elided_pass` and `annotated_pass` have identical signatures
// because the lifetime is added implicitly to `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));
}

See also:

elision

Типажи

Типаж (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");
    // ЗАДАНИЕ ^ Попробуйте убрать аннотацию типа

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Атрибут Derive

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

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

  • Типажи сравнения: Eq, PartialEq, Ord, PartialOrd
  • Clone, для создания T из &T с помощью копии.
  • Copy, чтобы создать тип семантикой копирования, вместо семантики перемещения.
  • Hash, чтобы вычислить хеш из &T.
  • Default, чтобы создать пустой экземпляр типа данных.
  • Zero, для создания нулевого экземпляра числового типа данных.
  • 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

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

В 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: 1, 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);
    }
}

Типаж Clone

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

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

// Единичная структура без ресурсов
#[derive(Debug, Clone, Copy)]
struct Nil;

// Кортежная структура с ресурсами, которая реализует типаж `Clone`
#[derive(Clone, Debug)]
struct Pair(Box<i32>, Box<i32>);

fn main() {
    // Объявим экземпляр `Nil`
    let nil = Nil;
    // Скопируем `Nil`, который не имеет ресурсов для перемещения
    let copied_nil = nil;

    // Оба `Nil`s могут быть использованы независимо
    println!("оригинал: {:?}", nil);
    println!("копия: {:?}", copied_nil);

    // Объявим экземпляр `Pair`
    let pair = Pair(Box::new(1), Box::new(2));
    println!("оригинал: {:?}", pair);

    // Скопируем `pair` в `moved_pair`, перенаправляя ресурсы
    let moved_pair = pair;
    println!("копия: {:?}", moved_pair);

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

    // Скопируем `moved_pair` в `cloned_pair` (включая ресурсы)
    let cloned_pair = moved_pair.clone();
    // Сбросим оригинальную пару используя std::mem::drop
    drop(moved_pair);

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

    // Полученный результат из .clone() все ещё можно использовать!
    println!("клон: {:?}", cloned_pair);
}

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

Syntax

In following subsections, we will show how to define macros in Rust. There are three basic ideas:

Указатели

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

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 (элемент)
  • pat (образец)
  • path (квалифицированное имя)
  • stmt (единственный оператор)
  • tt (единственное дерево лексем)
  • ty (тип)

Перегрузка

Макросы могут быть перегружены, принимая различные комбинации аргументов. В этом плане, 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:ident, $b: ident, $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)

A DSL is a mini "language" embedded in a Rust macro. It is completely valid Rust because the macro system expands into normal Rust constructs, but it looks like a small language. This allows you to define concise or intuitive syntax for some special functionality (within bounds).

Suppose that I want to define a little calculator API. I would like to supply an expression an have the output printed to console.

macro_rules! calculate {
    (eval $e:expr) => {{
        {
            let val: usize = $e; // Force types to be integers
            println!("{} = {}", stringify!{$e}, val);
        }
    }};
}

fn main() {
    calculate! {
        eval 1 + 2 // hehehe `eval` is _not_ a Rust keyword!
    }

    calculate! {
        eval (1 + 2) * (3 / 4)
    }
}

Output:

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

This was a very simple example, but much more complex interfaces have been developed, such as lazy_static or clap.

Variadic Interfaces

A variadic interface takes an arbitrary number of arguments. For example, println! can take an arbitrary number of arguments, as determined by the format string.

We can extend our calculate! macro from the previous section to be variadic:

macro_rules! calculate {
    // The pattern for a single `eval`
    (eval $e:expr) => {{
        {
            let val: usize = $e; // Force types to be integers
            println!("{} = {}", stringify!{$e}, val);
        }
    }};

    // Decompose multiple `eval`s recursively
    (eval $e:expr, $(eval $es:expr),+) => {{
        calculate! { eval $e }
        calculate! { $(eval $es),+ }
    }};
}

fn main() {
    calculate! { // Look ma! Variadic `calculate!`!
        eval 1 + 2,
        eval 3 + 4,
        eval (2 * 3) + 1
    }
}

Output:

1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7

Error handling

Error handling is the process of handling the possibility of failure. For example, failing to read a file and then continuing to use that bad input would clearly be problematic. Noticing and explicitly managing those errors saves the rest of the program from various pitfalls.

There are various ways to deal with errors in Rust, which are described in the following subchapters. They all have more or less subtle differences and different use cases. As a rule of thumb:

An explicit panic is mainly useful for tests and dealing with unrecoverable errors. For prototyping it can be useful, for example when dealing with functions that haven't been implemented yet, but in those cases the more descriptive unimplemented is better. In tests panic is a reasonable way to explicitly fail.

The Option type is for when a value is optional or when the lack of a value is not an error condition. For example the parent of a directory - / and C: don't have one. When dealing with Options, unwrap is fine for prototyping and cases where it's absolutely certain that there is guaranteed to be a value. However expect is more useful since it lets you specify an error message in case something goes wrong anyway.

When there is a chance that things do go wrong and the caller has to deal with the problem, use Result. You can unwrap and expect them as well (please don't do that unless it's a test or quick prototype).

For a more rigorous discussion of error handling, refer to the error handling section in the official book.

panic

Самый простой механизм обработки ошибок, с которым мы познакомимся – это panic. Он печатает сообщение с ошибкой, начинает процедуру раскрутки стека и, чаще всего, завершает программу.

В данном примере мы явно вызывает panic в случае ошибки:

fn give_princess(gift: &str) {
    // Принцесса ненавидит змей, поэтому нам нужно остановиться, если она не одобрит!
    if gift == "змея" { panic!("AAAaaaaa!!!!"); }

    println!("Я люблю тебя, {}!!!!!", gift);
}

fn main() {
    give_princess("плюшевый мишка");
    give_princess("змея");
}

Option & unwrap

In the last example, we showed that we can induce program failure at will. We told our program to panic if the princess received an inappropriate gift - a snake. But what if the princess expected a gift and didn't receive one? That case would be just as bad, so it needs to be handled!

We could test this against the null string ("") as we do with a snake. Since we're using Rust, let's instead have the compiler point out cases where there's no gift.

An enum called Option<T> in the std library is used when absence is a possibility. It manifests itself as one of two "options":

  • Some(T): An element of type T was found
  • None: No element was found

These cases can either be explicitly handled via match or implicitly with unwrap. Implicit handling will either return the inner element or panic.

Note that it's possible to manually customize panic with expect, but unwrap otherwise leaves us with a less meaningful output than explicit handling. In the following example, explicit handling yields a more controlled result while retaining the option to panic if desired.

// The commoner has seen it all, and can handle any gift well.
// All gifts are handled explicitly using `match`.
fn give_commoner(gift: Option<&str>) {
    // Specify a course of action for each case.
    match gift {
        Some("snake") => println!("Yuck! I'm throwing that snake in a fire."),
        Some(inner)   => println!("{}? How nice.", inner),
        None          => println!("No gift? Oh well."),
    }
}

// Our sheltered princess will `panic` at the sight of snakes.
// All gifts are handled implicitly using `unwrap`.
fn give_princess(gift: Option<&str>) {
    // `unwrap` returns a `panic` when it receives a `None`.
    let inside = gift.unwrap();
    if inside == "snake" { panic!("AAAaaaaa!!!!"); }

    println!("I love {}s!!!!!", inside);
}

fn main() {
    let food  = Some("cabbage");
    let snake = Some("snake");
    let void  = None;

    give_commoner(food);
    give_commoner(snake);
    give_commoner(void);

    let bird = Some("robin");
    let nothing = None;

    give_princess(bird);
    give_princess(nothing);
}

Combinators: map

match is a valid method for handling Options. However, you may eventually find heavy usage tedious, especially with operations only valid with an input. In these cases, combinators can be used to manage control flow in a modular fashion.

Option has a built in method called map(), a combinator for the simple mapping of Some -> Some and None -> None. Multiple map() calls can be chained together for even more flexibility.

In the following example, process() replaces all functions previous to it while staying compact.

#![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);

// Peeling food. If there isn't any, then return `None`.
// Otherwise, return the peeled food.
fn peel(food: Option<Food>) -> Option<Peeled> {
    match food {
        Some(food) => Some(Peeled(food)),
        None       => None,
    }
}

// Chopping food. If there isn't any, then return `None`.
// Otherwise, return the chopped food.
fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
    match peeled {
        Some(Peeled(food)) => Some(Chopped(food)),
        None               => None,
    }
}

// Cooking food. Here, we showcase `map()` instead of `match` for case handling.
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    chopped.map(|Chopped(food)| Cooked(food))
}

// A function to peel, chop, and cook food all in sequence.
// We chain multiple uses of `map()` to simplify the code.
fn process(food: Option<Food>) -> Option<Cooked> {
    food.map(|f| Peeled(f))
        .map(|Peeled(f)| Chopped(f))
        .map(|Chopped(f)| Cooked(f))
}

// Check whether there's food or not before trying to eat it!
fn eat(food: Option<Cooked>) {
    match food {
        Some(food) => println!("Mmm. I love {:?}", food),
        None       => println!("Oh no! It wasn't edible."),
    }
}

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)));
    // Let's try the simpler looking `process()` now.
    let cooked_potato = process(potato);

    eat(cooked_apple);
    eat(cooked_carrot);
    eat(cooked_potato);
}

See also:

closures, Option, Option::map()

Combinators: and_then

map() was described as a chainable way to simplify match statements. However, using map() on a function that returns an Option<T> results in the nested Option<Option<T>>. Chaining multiple calls together can then become confusing. That's where another combinator called and_then(), known in some languages as flatmap, comes in.

and_then() calls its function input with the wrapped value and returns the result. If the Option is None, then it returns None instead.

In the following example, cookable_v2() results in an Option<Food>. Using map() instead of and_then() would have given an Option<Option<Food>>, which is an invalid type for eat().

#![allow(dead_code)]

#[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }

// We don't have the ingredients to make Sushi.
fn have_ingredients(food: Food) -> Option<Food> {
    match food {
        Food::Sushi => None,
        _           => Some(food),
    }
}

// We have the recipe for everything except Cordon Bleu.
fn have_recipe(food: Food) -> Option<Food> {
    match food {
        Food::CordonBleu => None,
        _                => Some(food),
    }
}

// To make a dish, we need both the ingredients and the recipe.
// We can represent the logic with a chain of `match`es:
fn cookable_v1(food: Food) -> Option<Food> {
    match have_ingredients(food) {
        None       => None,
        Some(food) => match have_recipe(food) {
            None       => None,
            Some(food) => Some(food),
        },
    }
}

// This can conveniently be rewritten more compactly with `and_then()`:
fn cookable_v2(food: Food) -> Option<Food> {
    have_ingredients(food).and_then(have_recipe)
}

fn eat(food: Food, day: Day) {
    match cookable_v2(food) {
        Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),
        None       => println!("Oh no. We don't get to eat on {:?}?", 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);
}

See also:

closures, Option, and Option::and_then()

Result

Result is a richer version of the Option type that describes possible error instead of possible absence.

That is, Result<T, E> could have one of two outcomes:

  • Ok<T>: An element T was found
  • Err<E>: An error was found with element E

By convention, the expected outcome is Ok while the unexpected outcome is Err.

Like Option, Result has many methods associated with it. unwrap(), for example, either yields the element T or panics. For case handling, there are many combinators between Result and Option that overlap.

In working with Rust, you will likely encounter methods that return the Result type, such as the parse() method. It might not always be possible to parse a string into the other type, so parse() returns a Result indicating possible failure.

Let's see what happens when we successfully and unsuccessfully parse() a string:

fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
    // Let's try using `unwrap()` to get the number out. Will it bite us?
    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!("double is {}", twenty);

    let tt = multiply("t", "2");
    println!("double is {}", tt);
}

In the unsuccessful case, parse() leaves us with an error for unwrap() to panic on. Additionally, the panic exits our program and provides an unpleasant error message.

To improve the quality of our error message, we should be more specific about the return type and consider explicitly handling the error.

map for Result

Panicking in the previous example's multiply does not make for robust code. Generally, we want to return the error to the caller so it can decide what is the right way to respond to errors.

We first need to know what kind of error type we are dealing with. To determine the Err type, we look to parse(), which is implemented with the FromStr trait for i32. As a result, the Err type is specified as ParseIntError.

In the example below, the straightforward match statement leads to code that is overall more cumbersome.

use std::num::ParseIntError;

// With the return type rewritten, we use pattern matching without `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 is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("10", "2");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);
}

Luckily, Option's map, and_then, and many other combinators are also implemented for Result. Result contains a complete listing.

use std::num::ParseIntError;

// As with `Option`, we can use combinators such as `map()`.
// This function is otherwise identical to the one above and reads:
// Modify n if the value is valid, otherwise pass on the error.
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 is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("10", "2");
    print(twenty);

    // The following now provides a much more helpful error message.
    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

Early returns

In the previous example, we explicitly handled the errors using combinators. Another way to deal with this case analysis is to use a combination of match statements and early returns.

That is, we can simply stop executing the function and return the error if one occurs. For some, this form of code can be easier to both read and write. Consider this version of the previous example, rewritten using early returns:

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 is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

At this point, we've learned to explicitly handle errors using combinators and early returns. While we generally want to avoid panicking, explicitly handling all of our errors is cumbersome.

In the next section, we'll introduce ? for the cases where we simply need to unwrap without possibly inducing panic.

Introducing ?

Sometimes we just want the simplicity of unwrap without the possibility of a panic. Until now, unwrap has forced us to nest deeper and deeper when what we really wanted was to get the variable out. This is exactly the purpose of ?.

Upon finding an Err, there are two valid actions to take:

  1. panic! which we already decided to try to avoid if possible
  2. return because an Err means it cannot be handled

? is almost1 exactly equivalent to an unwrap which returns instead of panics on Errs. Let's see how we can simplify the earlier example that used combinators:

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 is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

The try! macro

Before there was ?, the same functionality was achieved with the try! macro. The ? operator is now recommended, but you may still find try! when looking at older code. The same multiply function from the previous example would look like this using try!:

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 is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}
1

See re-enter ? for more details.

Multiple error types

The previous examples have always been very convenient; Results interact with other Results and Options interact with other Options.

Sometimes an Option needs to interact with a Result, or a Result<T, Error1> needs to interact with a Result<T, Error2>. In those cases, we want to manage our different error types in a way that makes them composable and easy to interact with.

In the following code, two instances of unwrap generate different error types. Vec::first returns an Option, while parse::<i32> returns a Result<i32, ParseIntError>:

fn double_first(vec: Vec<&str>) -> i32 {
    let first = vec.first().unwrap(); // Generate error 1
    2 * first.parse::<i32>().unwrap() // Generate error 2
}

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));
    // Error 1: the input vector is empty

    println!("The first doubled is {}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

Over the next sections, we'll see several strategies for handling these kind of problems.

Pulling Results out of Options

The most basic way of handling mixed error types is to just embed them in each other.

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!("The first doubled is {:?}", double_first(numbers));

    println!("The first doubled is {:?}", double_first(empty));
    // Error 1: the input vector is empty

    println!("The first doubled is {:?}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

There are times when we'll want to stop processing on errors (like with ?) but keep going when the Option is None. A couple of combinators come in handy to swap the Result and 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)
    });

    let opt = opt.map_or(Ok(None), |r| r.map(Some))?;

    Ok(opt)
}

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

Defining an error type

Sometimes it simplifies the code to mask all of the different errors with a single type of error. We'll show this with a custom error.

Rust allows us to define our own error types. In general, a "good" error type:

  • Represents different errors with the same type
  • Presents nice error messages to the user
  • Is easy to compare with other types
    • Good: Err(EmptyVec)
    • Bad: Err("Please use a vector with at least one element".to_owned())
  • Can hold information about the error
    • Good: Err(BadChar(c, position))
    • Bad: Err("+ cannot be used here".to_owned())
  • Composes well with other errors
use std::error;
use std::fmt;
use std::num::ParseIntError;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug, Clone)]
// Define our error types. These may be customized for our error handling cases.
// Now we will be able to write our own errors, defer to an underlying error
// implementation, or do something in between.
struct DoubleError;

// Generation of an error is completely separate from how it is displayed.
// There's no need to be concerned about cluttering complex logic with the display style.
//
// Note that we don't store any extra info about the errors. This means we can't state
// which string failed to parse without modifying our types to carry that information.
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

// This is important for other errors to wrap this one.
impl error::Error for DoubleError {
    fn description(&self) -> &str {
        "invalid first item to double"
    }

    fn cause(&self) -> Option<&error::Error> {
        // Generic error, underlying cause isn't tracked.
        None
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
       // Change the error to our new type.
       .ok_or(DoubleError)
       .and_then(|s| s.parse::<i32>()
            // Update to the new error type here also.
            .map_err(|_| DoubleError)
            .map(|i| 2 * i))
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", 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));
}

Boxing errors

A way to write simple code while preserving the original errors is to Box them. The drawback is that the underlying error type is only known at runtime and not statically determined.

The stdlib helps in boxing our errors by having Box implement conversion from any type that implements the Error trait into the trait object Box<Error>, via From.

use std::error;
use std::fmt;
use std::num::ParseIntError;

// Change the alias to `Box<error::Error>`.
type Result<T> = std::result::Result<T, Box<error::Error>>;

#[derive(Debug, Clone)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

impl error::Error for EmptyVec {
    fn description(&self) -> &str {
        "invalid first item to double"
    }

    fn cause(&self) -> Option<&error::Error> {
        // Generic error, underlying cause isn't tracked.
        None
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
       .ok_or_else(|| EmptyVec.into())  // Converts to Box
       .and_then(|s| s.parse::<i32>()
            .map_err(|e| e.into())  // Converts to Box
            .map(|i| 2 * i))
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", 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));
}

See also:

Dynamic dispatch and Error trait

Other uses of ?

Notice in the previous example that our immediate reaction to calling parse is to map the error from a library error into a boxed error:

.and_then(|s| s.parse::<i32>()
    .map_err(|e| e.into())

Since this is a simple and common operation, it would be convenient if it could be elided. Alas, because and_then is not sufficiently flexible, it cannot. However, we can instead use ?.

? was previously explained as either unwrap or return Err(err). This is only mostly true. It actually means unwrap or return Err(From::from(err)). Since From::from is a conversion utility between different types, this means that if you ? where the error is convertible to the return type, it will convert automatically.

Here, we rewrite the previous example using ?. As a result, the map_err will go away when From::from is implemented for our error type:

use std::error;
use std::fmt;
use std::num::ParseIntError;

// Change the alias to `Box<error::Error>`.
type Result<T> = std::result::Result<T, Box<error::Error>>;

#[derive(Debug)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

impl error::Error for EmptyVec {
    fn description(&self) -> &str {
        "invalid first item to double"
    }

    fn cause(&self) -> Option<&error::Error> {
        // Generic error, underlying cause isn't tracked.
        None
    }
}

// The same structure as before but rather than chain all `Results`
// and `Options` along, we `?` to get the inner value out immediately.
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!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", 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));
}

This is actually fairly clean now. Compared with the original panic, it is very similar to replacing the unwrap calls with ? except that the return types are Result. As a result, they must be destructured at the top level.

See also:

From::from and ?

Wrapping errors

An alternative to boxing errors is to wrap them in your own error type.

use std::error;
use std::num::ParseIntError;
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
    EmptyVec,
    // We will defer to the parse error implementation for their error.
    // Supplying extra info requires adding more data to the type.
    Parse(ParseIntError),
}

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec =>
                write!(f, "please use a vector with at least one element"),
            // This is a wrapper, so defer to the underlying types' implementation of `fmt`.
            DoubleError::Parse(ref e) => e.fmt(f),
        }
    }
}

impl error::Error for DoubleError {
    fn description(&self) -> &str {
        match *self {
            DoubleError::EmptyVec => "empty vectors not allowed",
            // This already impls `Error`, so defer to its own implementation.
            DoubleError::Parse(ref e) => e.description(),
        }
    }

    fn cause(&self) -> Option<&error::Error> {
        match *self {
            DoubleError::EmptyVec => None,
            // The cause is the underlying implementation error type. Is implicitly
            // cast to the trait object `&error::Error`. This works because the
            // underlying type already implements the `Error` trait.
            DoubleError::Parse(ref e) => Some(e),
        }
    }
}

// Implement the conversion from `ParseIntError` to `DoubleError`.
// This will be automatically called by `?` if a `ParseIntError`
// needs to be converted into a `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!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", 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));
}

This adds a bit more boilerplate for handling errors and might not be needed in all applications. There are some libraries that can take care of the boilerplate for you.

See also:

From::from and Enums

Iterating over Results

An Iter::map operation might fail, for example:

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let possible_numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Results: {:?}", possible_numbers);
}

Let's step through strategies for handling this.

Ignore the failed items with filter_map()

filter_map calls a function and filters out the results that are 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!("Results: {:?}", numbers);
}

Fail the entire operation with collect()

Result implements FromIter so that a vector of results (Vec<Result<T, E>>) can be turned into a result with a vector (Result<Vec<T>, E>). Once an Result::Err is found, the iteration will terminate.

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Result<Vec<_>, _> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Results: {:?}", numbers);
}

This same technique can be used with Option.

Collect all valid values and failures with 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: {:?}", numbers);
    println!("Errors: {:?}", errors);
}

When you look at the results, you'll note that everything is still wrapped in Result. A little more boilerplate is needed for this.

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: {:?}", numbers);
    println!("Errors: {:?}", errors);
}

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

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

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

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

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

Упаковка, стек и куча

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

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

use std::mem;

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

#[allow(dead_code)]
struct Rectangle {
    p1: Point,
    p2: 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 {
        p1: origin(),
        p2: Point { x: 3.0, y: 4.0 }
    };

    // Выделенный в куче квадрат
    let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
        p1: origin(),
        p2: origin()
    });

    // Результат функции может быть упакован
    let boxed_point: Box<Point> = Box::new(origin());

    // Два уровня косвенной адресации
    let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

    println!("Point занимает {} байт в стеке",
             mem::size_of_val(&point));
    println!("Rectangle занимает {} байт в стеке",
             mem::size_of_val(&rectangle));

    // размер упаковки = размер указателя
    println!("Boxed point занимает {} байт в стеке",
             mem::size_of_val(&boxed_point));
    println!("Boxed rectangle занимает {} байт в стеке",
             mem::size_of_val(&boxed_rectangle));
    println!("Boxed box занимает {} байт в стеке",
             mem::size_of_val(&box_in_a_box));

    // Копировать данные, что находятся в `boxed_point`, в `unboxed_point`
    let unboxed_point: Point = *boxed_point;
    println!("Unboxed point занимает {} байт в стеке",
             mem::size_of_val(&unboxed_point));
}

Вектора

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

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!("Добавляем четвёртый элемент в вектор");
    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);
    }

    // К итератору вектора можно применить адаптер `enumerate`,
    // число итераций хранится в переменной (`i`)
    for (i, x) in xs.iter().enumerate() {
        println!("In position {} we have value {}", i, x);
    }

    // Благодаря методу `iter_mut`, можно обойти вектор
    // при этом изменить каждое значение в нем.
    for x in xs.iter_mut() {
        *x *= 3;
    }
    println!("Обновленный вектор: {:?}", xs);
}

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

Strings

There are two types of strings in Rust: String and &str.

A String is stored as a vector of bytes (Vec<u8>), but guaranteed to always be a valid UTF-8 sequence. String is heap allocated, growable and not null terminated.

&str is a slice (&[u8]) that always points to a valid UTF-8 sequence, and can be used to view into a String, just like &[T] is a view into Vec<T>.

fn main() {
    // (all the type annotations are superfluous)
    // A reference to a string allocated in read only memory
    let pangram: &'static str = "the quick brown fox jumps over the lazy dog";
    println!("Pangram: {}", pangram);

    // Iterate over words in reverse, no new string is allocated
    println!("Words in reverse");
    for word in pangram.split_whitespace().rev() {
        println!("> {}", word);
    }

    // Copy chars into a vector, sort and remove duplicates
    let mut chars: Vec<char> = pangram.chars().collect();
    chars.sort();
    chars.dedup();

    // Create an empty and growable `String`
    let mut string = String::new();
    for c in chars {
        // Insert a char at the end of string
        string.push(c);
        // Insert a string at the end of string
        string.push_str(", ");
    }

    // The trimmed string is a slice to the original string, hence no new
    // allocation is performed
    let chars_to_trim: &[char] = &[' ', ','];
    let trimmed_str: &str = string.trim_matches(chars_to_trim);
    println!("Used characters: {}", trimmed_str);

    // Heap allocate a string
    let alice = String::from("I like dogs");
    // Allocate new memory and store the modified string there
    let bob: String = alice.replace("dog", "cat");

    println!("Alice says: {}", alice);
    println!("Bob says: {}", bob);
}

More str/String methods can be found under the std::str and std::string modules

Literals and escapes

There are multiple ways to write string literals with special characters in them. All result in a similar &str so it's best to use the form that is the most convenient to write. Similarly there are multiple ways to write byte string literals, which all result in &[u8; N].

Generally special characters are escaped with a backslash character: \. This way you can add any character to your string, even unprintable ones and ones that you don't know how to type. If you want a literal backslash, escape it with another one: \\

String or character literal delimiters occuring within a literal must be escaped: "\"", '\''.

fn main() {
    // You can use escapes to write bytes by their hexadecimal values...
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // ...or Unicode code points.
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!("Unicode character {} (U+211D) is called {}",
                unicode_codepoint, character_name );


    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

Sometimes there are just too many characters that need to be escaped or it's just much more convenient to write a string out as-is. This is where raw string literals come into play.

fn main() {
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);

    // If you need quotes in a raw string, add a pair of #s
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // If you need "# in your string, just use more #s in the delimiter.
    // There is no limit for the number of #s you can use.
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

Want a string that's not UTF-8? (Remember, str and String must be valid UTF-8) Or maybe you want an array of bytes that's mostly text? Byte strings to the rescue!

use std::str;

fn main() {
    // Note that this is not actually a &str
    let bytestring: &[u8; 20] = b"this is a bytestring";

    // Byte arrays don't have Display so printing them is a bit limited
    println!("A bytestring: {:?}", bytestring);

    // Bytestrings can have byte escapes...
    let escaped = b"\x52\x75\x73\x74 as bytes";
    // ...but no unicode escapes
    // let escaped = b"\u{211D} is not allowed";
    println!("Some escaped bytes: {:?}", escaped);


    // Raw bytestrings work just like raw strings
    let raw_bytestring = br"\u{211D} is not escaped here";
    println!("{:?}", raw_bytestring);

    // Converting a byte array to str can fail
    if let Ok(my_str) = str::from_utf8(raw_bytestring) {
        println!("And the same as text: '{}'", my_str);
    }

    let quotes = br#"You can also use "fancier" formatting, \
                    like with normal raw strings"#;

    // Bytestrings don't have to be UTF-8
    let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82"; // "ようこそ" in SHIFT-JIS

    // But then they can't always be converted to str
    match str::from_utf8(shift_jis) {
        Ok(my_str) => println!("Conversion successful: '{}'", my_str),
        Err(e) => println!("Conversion failed: {:?}", e),
    };
}

For conversions between character encodings check out the encoding crate.

A more detailed listing of the ways to write string literals and escape characters is given in the 'Tokens' chapter of the 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

We've seen that the Option enum can be used as a return value from functions that may fail, where None can be returned to indicate failure. However, sometimes it is important to express why an operation failed. To do this we have the Result enum.

The Result<T, E> enum has two variants:

  • Ok(value) which indicates that the operation succeeded, and wraps the value returned by the operation. (value has type T)
  • Err(why), which indicates that the operation failed, and wraps why, which (hopefully) explains the cause of the failure. (why has type E)
mod checked {
    // Mathematical "errors" we want to catch
    #[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 {
            // This operation would `fail`, instead let's return the reason of
            // the failure wrapped in `Err`
            Err(MathError::DivisionByZero)
        } else {
            // This operation is valid, return the result wrapped in `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 {
    // This is a three level match pyramid!
    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() {
    // Will this fail?
    println!("{}", op(1.0, 10.0));
}

?

Chaining results using match can get pretty untidy; luckily, the ? operator can be used to make things pretty again. ? is used at the end of an expression returning a Result, and is equivalent to a match expression, where the Err(err) branch expands to an early Err(From::from(err)), and the Ok(ok) branch expands to an ok expression.

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())
        }
    }

    // Intermediate function
    fn op_(x: f64, y: f64) -> MathResult {
        // if `div` "fails", then `DivisionByZero` will be `return`ed
        let ratio = div(x, y)?;

        // if `ln` "fails", then `NonPositiveLogarithm` will be `return`ed
        let ln = ln(ratio)?;

        sqrt(ln)
    }

    pub fn op(x: f64, y: f64) {
        match op_(x, y) {
            Err(why) => panic!(match why {
                MathError::NonPositiveLogarithm
                    => "logarithm of non-positive number",
                MathError::DivisionByZero
                    => "division by zero",
                MathError::NegativeSquareRoot
                    => "square root of negative number",
            }),
            Ok(value) => println!("{}", value),
        }
    }
}

fn main() {
    checked::op(1.0, 10.0);
}

Be sure to check the documentation, as there are many methods to map/compose 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

Where vectors store values by an integer index, HashMaps store values by key. HashMap keys can be booleans, integers, strings, or any other type that implements the Eq and Hash traits. More on this in the next section.

Like vectors, HashMaps are growable, but HashMaps can also shrink themselves when they have excess space. You can create a HashMap with a certain starting capacity using HashMap::with_capacity(uint), or use HashMap::new() to get a HashMap with a default initial capacity (recommended).

use std::collections::HashMap;

fn call(number: &str) -> &str {
    match number {
        "798-1364" => "We're sorry, the call cannot be completed as dialed. 
            Please hang up and try again.",
        "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred.
            What can I get for you today?",
        _ => "Hi! Who is this again?"
    }
}

fn main() { 
    let mut contacts = HashMap::new();

    contacts.insert("Daniel", "798-1364");
    contacts.insert("Ashley", "645-7689");
    contacts.insert("Katie", "435-8291");
    contacts.insert("Robert", "956-1745");

    // Takes a reference and returns Option<&V>
    match contacts.get(&"Daniel") {
        Some(&number) => println!("Calling Daniel: {}", call(number)),
        _ => println!("Don't have Daniel's number."),
    }

    // `HashMap::insert()` returns `None`
    // if the inserted value is new, `Some(value)` otherwise
    contacts.insert("Daniel", "164-6743");

    match contacts.get(&"Ashley") {
        Some(&number) => println!("Calling Ashley: {}", call(number)),
        _ => println!("Don't have Ashley's number."),
    }

    contacts.remove(&"Ashley"); 

    // `HashMap::iter()` returns an iterator that yields 
    // (&'a key, &'a value) pairs in arbitrary order.
    for (contact, &number) in contacts.iter() {
        println!("Calling {}: {}", contact, call(number)); 
    }
}

For more information on how hashing and hash maps (sometimes called hash tables) work, have a look at Hash Table Wikipedia

Alternate/custom key types

Any type that implements the Eq and Hash traits can be a key in HashMap. This includes:

  • bool (though not very useful since there is only two possible keys)
  • int, uint, and all variations thereof
  • String and &str (protip: you can have a HashMap keyed by String and call .get() with an &str)

Note that f32 and f64 do not implement Hash, likely because floating-point precision errors would make using them as hashmap keys horribly error-prone.

All collection classes implement Eq and Hash if their contained type also respectively implements Eq and Hash. For example, Vec<T> will implement Hash if T implements Hash.

You can easily implement Eq and Hash for a custom type with just one line: #[derive(PartialEq, Eq, Hash)]

The compiler will do the rest. If you want more control over the details, you can implement Eq and/or Hash yourself. This guide will not cover the specifics of implementing Hash.

To play around with using a struct in HashMap, let's try making a very simple user logon system:

use std::collections::HashMap;

// Eq requires that you derive PartialEq on the type.
#[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: {}", username);
    println!("Password: {}", password);
    println!("Attempting logon...");

    let logon = Account {
        username: username,
        password: password,
    };

    match accounts.get(&logon) {
        Some(account_info) => {
            println!("Successful logon!");
            println!("Name: {}", account_info.name);
            println!("Email: {}", account_info.email);
        },
        _ => println!("Login failed!"),
    }
}

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

Consider a HashSet as a HashMap where we just care about the keys ( HashSet<T> is, in actuality, just a wrapper around HashMap<T, ()>).

"What's the point of that?" you ask. "I could just store the keys in a Vec."

A HashSet's unique feature is that it is guaranteed to not have duplicate elements. That's the contract that any set collection fulfills. HashSet is just one implementation. (see also: BTreeSet)

If you insert a value that is already present in the HashSet, (i.e. the new value is equal to the existing and they both have the same hash), then the new value will replace the old.

This is great for when you never want more than one of something, or when you want to know if you've already got something.

But sets can do more than that.

Sets have 4 primary operations (all of the following calls return an iterator):

  • union: get all the unique elements in both sets.

  • difference: get all the elements that are in the first set but not the second.

  • intersection: get all the elements that are only in both sets.

  • symmetric_difference: get all the elements that are in one set or the other, but not both.

Try all of these in the following example:

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()` returns false if
    // there was a value already present.
    assert!(b.insert(4), "Value 4 is already in set B!");
    // FIXME ^ Comment out this line

    b.insert(5);

    // If a collection's element type implements `Debug`,
    // then the collection implements `Debug`.
    // It usually prints its elements in the format `[elem1, elem2, ...]`
    println!("A: {:?}", a);
    println!("B: {:?}", b);

    // Print [1, 2, 3, 4, 5] in arbitrary order
    println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());

    // This should print [1]
    println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());

    // Print [2, 3, 4] in arbitrary order.
    println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());

    // Print [1, 5]
    println!("Symmetric Difference: {:?}",
             a.symmetric_difference(&b).collect::<Vec<&i32>>());
}

(Examples are adapted from the documentation.)

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

Многие другие типы предоставляются стандартной библиотекой для вспомогательных целей, например:

  • Потоки
  • Каналы
  • Файловые операции Ввода/Вывода

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

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

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

Потоки

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

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

Эти потоки управляются ОС.

Testcase: map-reduce

Rust makes it very easy to parallelise data processing, without many of the headaches traditionally associated with such an attempt.

The standard library provides great threading primitives out of the box. These, combined with Rust's concept of Ownership and aliasing rules, automatically prevent data races.

The aliasing rules (one writable reference XOR many readable references) automatically prevent you from manipulating state that is visible to other threads. (Where synchronisation is needed, there are synchronisation primitives like Mutexes or Channels.)

In this example, we will calculate the sum of all digits in a block of numbers. We will do this by parcelling out chunks of the block into different threads. Each thread will sum its tiny block of digits, and subsequently we will sum the intermediate sums produced by each thread.

Note that, although we're passing references across thread boundaries, Rust understands that we're only passing read-only references, and that thus no unsafety or data races can occur. Because we're move-ing the data segments into the thread, Rust will also ensure the data is kept alive until the threads exit, so no dangling pointers occur.

use std::thread;

// This is the `main` thread
fn main() {

    // This is our data to process.
    // We will calculate the sum of all digits via a threaded  map-reduce algorithm.
    // Each whitespace separated chunk will be handled in a different thread.
    //
    // TODO: see what happens to the output if you insert spaces!
    let data = "86967897737416471853297327050364959
11861322575564723963297542624962850
70856234701860851907960690014725639
38397966707106094172783238747669219
52380795257888236525459303330302837
58495327135744041048897885734297812
69920216438980873548808413720956532
16278424637452589860345374828574668";

    // Make a vector to hold the child-threads which we will spawn.
    let mut children = vec![];

    /*************************************************************************
     * "Map" phase
     *
     * Divide our data into segments, and apply initial processing
     ************************************************************************/

    // split our data into segments for individual calculation
    // each chunk will be a reference (&str) into the actual data
    let chunked_data = data.split_whitespace();

    // Iterate over the data segments.
    // .enumerate() adds the current loop index to whatever is iterated
    // the resulting tuple "(index, element)" is then immediately
    // "destructured" into two variables, "i" and "data_segment" with a
    // "destructuring assignment"
    for (i, data_segment) in chunked_data.enumerate() {
        println!("data segment {} is \"{}\"", i, data_segment);

        // Process each data segment in a separate thread
        //
        // spawn() returns a handle to the new thread,
        // which we MUST keep to access the returned value
        //
        // 'move || -> u32' is syntax for a closure that:
        // * takes no arguments ('||')
        // * takes ownership of its captured variables ('move') and
        // * returns an unsigned 32-bit integer ('-> u32')
        //
        // Rust is smart enough to infer the '-> u32' from
        // the closure itself so we could have left that out.
        //
        // TODO: try removing the 'move' and see what happens
        children.push(thread::spawn(move || -> u32 {
            // Calculate the intermediate sum of this segment:
            let result = data_segment
                        // iterate over the characters of our segment..
                        .chars()
                        // .. convert text-characters to their number value..
                        .map(|c| c.to_digit(10).expect("should be a digit"))
                        // .. and sum the resulting iterator of numbers
                        .sum();

            // println! locks stdout, so no text-interleaving occurs
            println!("processed segment {}, result={}", i, result);

            // "return" not needed, because Rust is an "expression language", the
            // last evaluated expression in each block is automatically its value.
            result

        }));
    }


    /*************************************************************************
     * "Reduce" phase
     *
     * Collect our intermediate results, and combine them into a final result
     ************************************************************************/

    // collect each thread's intermediate results into a new Vec
    let mut intermediate_sums = vec![];
    for child in children {
        // collect each child thread's return-value
        let intermediate_sum = child.join().unwrap();
        intermediate_sums.push(intermediate_sum);
    }

    // combine all intermediate sums into a single final sum.
    //
    // we use the "turbofish" ::<> to provide sum() with a type hint.
    //
    // TODO: try without the turbofish, by instead explicitly
    // specifying the type of final_result
    let final_result = intermediate_sums.iter().sum::<u32>();

    println!("Final sum result: {}", final_result);
}


Assignments

It is not wise to let our number of threads depend on user inputted data. What if the user decides to insert a lot of spaces? Do we really want to spawn 2,000 threads? Modify the program so that the data is always chunked into a limited number of chunks, defined by a static constant at the beginning of the program.

See also:

Channels

Rust provides asynchronous channels for communication between threads. Channels allow a unidirectional flow of information between two end-points: the Sender and the Receiver.

use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use std::thread;

static NTHREADS: i32 = 3;

fn main() {
    // Channels have two endpoints: the `Sender<T>` and the `Receiver<T>`,
    // where `T` is the type of the message to be transferred
    // (type annotation is superfluous)
    let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();

    for id in 0..NTHREADS {
        // The sender endpoint can be copied
        let thread_tx = tx.clone();

        // Each thread will send its id via the channel
        thread::spawn(move || {
            // The thread takes ownership over `thread_tx`
            // Each thread queues a message in the channel
            thread_tx.send(id).unwrap();

            // Sending is a non-blocking operation, the thread will continue
            // immediately after sending its message
            println!("thread {} finished", id);
        });
    }

    // Here, all the messages are collected
    let mut ids = Vec::with_capacity(NTHREADS as usize);
    for _ in 0..NTHREADS {
        // The `recv` method picks a message from the channel
        // `recv` will block the current thread if there are no messages available
        ids.push(rx.recv());
    }

    // Show the order in which the messages were sent
    println!("{:?}", ids);
}

Path

The Path struct represents file paths in the underlying filesystem. There are two flavors of Path: posix::Path, for UNIX-like systems, and windows::Path, for Windows. The prelude exports the appropriate platform-specific Path variant.

A Path can be created from an OsStr, and provides several methods to get information from the file/directory the path points to.

Note that a Path is not internally represented as an UTF-8 string, but instead is stored as a vector of bytes (Vec<u8>). Therefore, converting a Path to a &str is not free and may fail (an Option is returned).

use std::path::Path;

fn main() {
    // Create a `Path` from an `&'static str`
    let path = Path::new(".");

    // The `display` method returns a `Show`able structure
    let _display = path.display();

    // `join` merges a path with a byte container using the OS specific
    // separator, and returns the new path
    let new_path = path.join("a").join("b");

    // Convert the path into a string slice
    match new_path.to_str() {
        None => panic!("new path is not a valid UTF-8 sequence"),
        Some(s) => println!("new path is {}", s),
    }
}

Be sure to check at other Path methods (posix::Path or windows::Path) and the Metadata struct.

See also

OsStr and Metadata.

File I/O

The File struct represents a file that has been opened (it wraps a file descriptor), and gives read and/or write access to the underlying file.

Since many things can go wrong when doing file I/O, all the File methods return the io::Result<T> type, which is an alias for Result<T, io::Error>.

This makes the failure of all I/O operations explicit. Thanks to this, the programmer can see all the failure paths, and is encouraged to handle them in a proactive manner.

open

The open static method can be used to open a file in read-only mode.

A File owns a resource, the file descriptor and takes care of closing the file when it is droped.

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // Create a path to the desired file
    let path = Path::new("hello.txt");
    let display = path.display();

    // Open the path in read-only mode, returns `io::Result<File>`
    let mut file = match File::open(&path) {
        // The `description` method of `io::Error` returns a string that
        // describes the error
        Err(why) => panic!("couldn't open {}: {}", display,
                                                   why.description()),
        Ok(file) => file,
    };

    // Read the file contents into a string, returns `io::Result<usize>`
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("couldn't read {}: {}", display,
                                                   why.description()),
        Ok(_) => print!("{} contains:\n{}", display, s),
    }

    // `file` goes out of scope, and the "hello.txt" file gets closed
}

Here's the expected successful output:

$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt contains:
Hello World!

(You are encouraged to test the previous example under different failure conditions: hello.txt doesn't exist, or hello.txt is not readable, etc.)

create

The create static method opens a file in write-only mode. If the file already existed, the old content is destroyed. Otherwise, a new file is created.

static LOREM_IPSUM: &'static 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::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;

fn main() {
    let path = Path::new("out/lorem_ipsum.txt");
    let display = path.display();

    // Open a file in write-only mode, returns `io::Result<File>`
    let mut file = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}",
                           display,
                           why.description()),
        Ok(file) => file,
    };

    // Write the `LOREM_IPSUM` string to `file`, returns `io::Result<()>`
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => {
            panic!("couldn't write to {}: {}", display,
                                               why.description())
        },
        Ok(_) => println!("successfully wrote to {}", display),
    }
}

Here's the expected successful output:

$ mkdir out
$ rustc create.rs && ./create
successfully wrote to out/lorem_ipsum.txt
$ cat out/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.

(As in the previous example, you are encouraged to test this example under failure conditions.)

There is also a more generic open_mode method that can open files in other modes like: read+write, append, etc.

Child processes

The process::Output struct represents the output of a finished child process, and the process::Command struct is a process builder.

use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("--version")
        .output().unwrap_or_else(|e| {
            panic!("failed to execute process: {}", e)
    });

    if output.status.success() {
        let s = String::from_utf8_lossy(&output.stdout);

        print!("rustc succeeded and stdout was:\n{}", s);
    } else {
        let s = String::from_utf8_lossy(&output.stderr);

        print!("rustc failed and stderr was:\n{}", s);
    }
}

(You are encouraged to try the previous example with an incorrect flag passed to rustc)

Pipes

The std::Child struct represents a running child process, and exposes the stdin, stdout and stderr handles for interaction with the underlying process via pipes.

use std::error::Error;
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() {
    // Spawn the `wc` command
    let process = match Command::new("wc")
                                .stdin(Stdio::piped())
                                .stdout(Stdio::piped())
                                .spawn() {
        Err(why) => panic!("couldn't spawn wc: {}", why.description()),
        Ok(process) => process,
    };

    // Write a string to the `stdin` of `wc`.
    //
    // `stdin` has type `Option<ChildStdin>`, but since we know this instance
    // must have one, we can directly `unwrap` it.
    match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
        Err(why) => panic!("couldn't write to wc stdin: {}",
                           why.description()),
        Ok(_) => println!("sent pangram to wc"),
    }

    // Because `stdin` does not live after the above calls, it is `drop`ed,
    // and the pipe is closed.
    //
    // This is very important, otherwise `wc` wouldn't start processing the
    // input we just sent.

    // The `stdout` field also has type `Option<ChildStdout>` so must be unwrapped.
    let mut s = String::new();
    match process.stdout.unwrap().read_to_string(&mut s) {
        Err(why) => panic!("couldn't read wc stdout: {}",
                           why.description()),
        Ok(_) => print!("wc responded with:\n{}", s),
    }
}

Wait

If you'd like to wait for a process::Child to finish, you must call Child::wait, which will return a process::ExitStatus.

use std::process::Command;

fn main() {
    let mut child = Command::new("sleep").arg("5").spawn().unwrap();
    let _result = child.wait().unwrap();

    println!("reached end of main");
}
$ rustc wait.rs && ./wait
reached end of main
# `wait` keeps running for 5 seconds
# `sleep 5` command ends, and then our `wait` program finishes

Filesystem Operations

The std::io::fs module contains several functions that deal with the filesystem.

use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;

// A simple implementation of `% 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),
    }
}

// A simple implementation of `% echo s > path`
fn echo(s: &str, path: &Path) -> io::Result<()> {
    let mut f = File::create(path)?;

    f.write_all(s.as_bytes())
}

// A simple implementation of `% touch path` (ignores existing files)
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`");
    // Create a directory, returns `io::Result<()>`
    match fs::create_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(_) => {},
    }

    println!("`echo hello > a/b.txt`");
    // The previous match can be simplified using the `unwrap_or_else` method
    echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`mkdir -p a/c/d`");
    // Recursively create a directory, returns `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`");
    // Create a symbolic link, returns `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`");
    // Read the contents of a directory, returns `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`");
    // Remove a file, returns `io::Result<()>`
    fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`rmdir a/c/d`");
    // Remove an empty directory, returns `io::Result<()>`
    fs::remove_dir("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });
}

Here's the expected successful output:

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

And the final state of the a directory is:

$ tree a
a
|-- b.txt
`-- c
    `-- b.txt -> ../b.txt

1 directory, 2 files

An alternative way to define the function cat is with ? notation:

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

See also:

cfg!

Program arguments

Standard Library

The command line arguments can be accessed using std::env::args, which returns an iterator that yields a String for each argument:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    // The first argument is the path that was used to call the program.
    println!("My path is {}.", args[0]);

    // The rest of the arguments are the passed command line parameters.
    // Call the program like this:
    //   $ ./args arg1 arg2
    println!("I got {:?} arguments: {:?}.", args.len() - 1, &args[1..]);
}
$ ./args 1 2 3
My path is ./args.
I got 3 arguments: ["1", "2", "3"].

Crates

Alternatively, there are numerous crates that can provide extra functionality when creating command-line applications. The Rust Cookbook exhibits best practices on how to use one of the more popular command line argument crates, clap.

Argument parsing

Matching can be used to parse simple arguments:

use std::env;

fn increase(number: i32) {
    println!("{}", number + 1);
}

fn decrease(number: i32) {
    println!("{}", number - 1);
}

fn help() {
    println!("usage:
match_args <string>
    Check whether given string is the answer.
match_args {{increase|decrease}} <integer>
    Increase or decrease given integer by one.");
}

fn main() {
    let args: Vec<String> = env::args().collect();

    match args.len() {
        // no arguments passed
        1 => {
            println!("My name is 'match_args'. Try passing some arguments!");
        },
        // one argument passed
        2 => {
            match args[1].parse() {
                Ok(42) => println!("This is the answer!"),
                _ => println!("This is not the answer."),
            }
        },
        // one command and one argument passed
        3 => {
            let cmd = &args[1];
            let num = &args[2];
            // parse the number
            let number: i32 = match num.parse() {
                Ok(n) => {
                    n
                },
                Err(_) => {
                    eprintln!("error: second argument not an integer");
                    help();
                    return;
                },
            };
            // parse the command
            match &cmd[..] {
                "increase" => increase(number),
                "decrease" => decrease(number),
                _ => {
                    eprintln!("error: invalid command");
                    help();
                },
            }
        },
        // all the other cases
        _ => {
            // show a help message
            help();
        }
    }
}
$ ./match_args Rust
This is not the answer.
$ ./match_args 42
This is the answer!
$ ./match_args do something
error: second argument not an integer
usage:
match_args <string>
    Check whether given string is the answer.
match_args {increase|decrease} <integer>
    Increase or decrease given integer by one.
$ ./match_args do 42
error: invalid command
usage:
match_args <string>
    Check whether given string is the answer.
match_args {increase|decrease} <integer>
    Increase or decrease given integer by one.
$ ./match_args increase 42
43

Foreign Function Interface

Rust provides a Foreign Function Interface (FFI) to C libraries. Foreign functions must be declared inside an extern block annotated with a #[link] attribute containing the name of the foreign library.

use std::fmt;

// this extern block links to the libm library
#[link(name = "m")]
extern {
    // this is a foreign function
    // that computes the square root of a single precision complex number
    fn csqrtf(z: Complex) -> Complex;

    fn ccosf(z: Complex) -> Complex;
}

// Since calling foreign functions is considered unsafe,
// it's common to write safe wrappers around them.
fn cos(z: Complex) -> Complex {
    unsafe { ccosf(z) }
}

fn main() {
    // z = -1 + 0i
    let z = Complex { re: -1., im: 0. };

    // calling a foreign function is an unsafe operation
    let z_sqrt = unsafe { csqrtf(z) };

    println!("the square root of {:?} is {:?}", z, z_sqrt);

    // calling safe API wrapped around unsafe operation
    println!("cos({:?}) = {:?}", z, cos(z));
}

// Minimal implementation of single precision complex numbers
#[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)
        }
    }
}

Testing

Rust is a programming language that cares a lot about correctness and it includes support for writing software tests within the language itself.

Testing comes in three styles:

Also Rust has support for specifying additional dependencies for tests:

See Also

Unit testing

Tests are Rust functions that verify that the non-test code is functioning in the expected manner. The bodies of test functions typically perform some setup, run the code we want to test, then assert whether the results are what we expect.

Most unit tests go into a tests mod with the #[cfg(test)] attribute. Test functions are marked with the #[test] attribute.

Tests fail when something in the test function panics. There are some helper macros:

  • assert!(expression) - panics if expression evaluates to false.
  • assert_eq!(left, right) and assert_ne!(left, right) - testing left and right expressions for equality and inequality respectively.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// This is a really bad adding function, its purpose is to fail in this
// example.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        // This assert would fire and test will fail.
        // Please note, that private functions can be tested too!
        assert_eq!(bad_add(1, 2), 3);
    }
}

Tests can be run with 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

Testing panics

To check functions that should panic under certain circumstances, use attribute #[should_panic]. This attribute accepts optional parameter expected = with the text of the panic message. If your function can panic in multiple ways, it helps make sure your test is testing the correct panic.

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

Running these tests gives us:

$ 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

Running specific tests

To run specific tests one may specify the test name to cargo test command.

$ 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

To run multiple tests one may specify part of a test name that matches all the tests that should be run.

$ 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

Ignoring tests

Tests can be marked with the#[ignore] attribute to exclude some tests. Or to run them with command cargo test -- --ignored


# #![allow(unused_variables)]
#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 1 test
test tests::ignored_test ... ignored

test result: ok. 0 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

Documentation testing

The primary way of documenting a Rust project is through annotating the source code. Documentation comments are written in markdown and support code blocks in them. Rust takes care about correctness, so these code blocks are compiled and used as tests.

/// First line is a short summary describing function.
///
/// The next lines present detailed documentation. Code blocks start with
/// triple backquotes and have implicit `fn main()` inside
/// and `extern crate <cratename>`. Assume we're testing `doccomments` crate:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Usually doc comments may include sections "Examples", "Panics" and "Failures".
///
/// The next function divides two numbers.
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rust,should_panic
/// // panics on division by zero
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    }

    a / b
}

Tests can be run with 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

Motivation behind documentation tests

The main purpose of documentation tests is to serve as an examples that exercise the functionality, which is one of the most important guidelines. It allows using examples from docs as complete code snippets. But using ? makes compilation fail since main returns unit. The ability to hide some source lines from documentation comes to the rescue: one may write fn try_main() -> Result<(), ErrorType>, hide it and unwrap it in hidden main. Sounds complicated? Here's an example:

/// Using hidden `try_main` in doc tests.
///
/// ```
/// # // hidden lines start with `#` symbol, but they're still compileable!
/// # fn try_main() -> Result<(), String> { // line that wraps the body shown in doc
/// let res = try::try_div(10, 2)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() { // starting main that'll unwrap()
/// #    try_main().unwrap(); // calling try_main and unwrapping
/// #                         // so that test will panic in case of error
/// # }
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Divide-by-zero"))
    } else {
        Ok(a / b)
    }
}

See Also

Integration testing

Unit tests are testing one module in isolation at a time: they're small and can test private code. Integration tests are external to your crate and use only its public interface in the same way any other code would. Their purpose is to test that many parts of your library work correctly together.

Cargo looks for integration tests in tests directory next to src.

File src/lib.rs:

// Assume that crate is called adder, will have to extern it in integration test.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

File with test: tests/integration_test.rs:

// extern crate we're testing, same as any other code would do.
extern crate adder;

#[test]
fn test_add() {
    assert_eq!(adder::add(3, 2), 5);
}

Running tests with cargo test command:

$ 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

Each Rust source file in tests directory is compiled as a separate crate. One way of sharing some code between integration tests is making module with public functions, importing and using it within tests.

File tests/common.rs:

pub fn setup() {
    // some setup code, like creating required files/directories, starting
    // servers, etc.
}

File with test: tests/integration_test.rs

// extern crate we're testing, same as any other code will do.
extern crate adder;

// importing common module.
mod common;

#[test]
fn test_add() {
    // using common code.
    common::setup();
    assert_eq!(adder::add(3, 2), 5);
}

Modules with common code follow the ordinary modules rules, so it's ok to create common module as tests/common/mod.rs.

Development dependencies

Sometimes there is a need to have a dependencies for tests (examples, benchmarks) only. Such dependencies are added to Cargo.toml in [dev-dependencies] section. These dependencies are not propagated to other packages which depend on this package.

One such example is using a crate that extends standard assert! macros.
File Cargo.toml:

# standard crate data is left out
[dev-dependencies]
pretty_assertions = "0.4.0"

File src/lib.rs:

// externing crate for test-only use
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

See Also

Cargo docs on specifying dependencies.

Meta

Some topics aren't exactly relevant to how you program but provide you tooling or infrastructure support which just makes things better for everyone. These topics include:

  • Documentation: Generate library documentation for users via the included rustdoc.
  • Testing: Create testsuites for libraries to give confidence that your library does exactly what it's supposed to.
  • Benchmarking: Create benchmarks for functionality to be confident that they run quickly.

Documentation

Doc comments are very useful for big projects that require documentation. When running Rustdoc, these are the comments that get compiled into documentation. They are denoted by a ///, and support Markdown.

#![crate_name = "doc"]

/// A human being is represented here
pub struct Person {
    /// A person must have a name, no matter how much Juliet may hate it
    name: String,
}

impl Person {
    /// Returns a person with the name given them
    ///
    /// # Arguments
    ///
    /// * `name` - A string slice that holds the name of the person
    ///
    /// # Example
    ///
    /// ```
    /// // You can have rust code between fences inside the comments
    /// // If you pass --test to Rustdoc, it will even test it for you!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Gives a friendly hello!
    ///
    /// Says "Hello, [name]" to the `Person` it is called on.
    pub fn hello(& self) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

To run the tests, first build the code as a library, then tell rustdoc where to find the library so it can link it into each doctest program:

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

(When you run cargo test on a library crate, Cargo will automatically generate and run the correct rustc and rustdoc commands.)

Unsafe Operations

As an introduction to this section, to borrow from the official docs, "one should try to minimize the amount of unsafe code in a code base." With that in mind, let's get started! Unsafe blocks in Rust are used to bypass protections put in place by the compiler; specifically, there are four primary things that unsafe blocks are used for:

  • dereferencing raw pointers
  • calling a function over FFI (but this is covered in a previous chapter of the book)
  • calling functions which are unsafe
  • inline assembly

Raw Pointers

Raw pointers * and references &T function similarly, but references are always safe because they are guaranteed to point to valid data due to the borrow checker. Dereferencing a raw pointer can only be done through an unsafe block.

fn main() {
    let raw_p: *const u32 = &10;

    unsafe {
        assert!(*raw_p == 10);
    }
}

Calling Unsafe Functions

Some functions can be declared as unsafe, meaning it is the programmer's responsibility to ensure correctness instead of the compiler's. One example of this is std::slice::from_raw_parts which will create a slice given a pointer to the first element and a length.

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

For slice::from_raw_parts, one of the assumptions which must be upheld is that the pointer passed in points to valid memory and that the memory pointed to is of the correct type. If these invariants aren't upheld then the program's behaviour is undefined and there is no knowing what will happen.