1. 1. Introduction
  2. 2. Введение
  3. 3. C чего начать
  4. 4. Изучение Rust
    1. 4.1. Угадайка
    2. 4.2. Обедающие философы
    3. 4.3. Вызов кода на Rust из других языков
  5. 5. Синтаксис и семантика
    1. 5.1. Связывание имён
    2. 5.2. Функции
    3. 5.3. Простые типы
    4. 5.4. Комментарии
    5. 5.5. Конструкция `if`
    6. 5.6. Циклы
    7. 5.7. Владение
    8. 5.8. Ссылки и заимствование
    9. 5.9. Время жизни
    10. 5.10. Изменяемость
    11. 5.11. Структуры
    12. 5.12. Перечисления
    13. 5.13. Конструкция `match`
    14. 5.14. Шаблоны сопоставления `match`
    15. 5.15. Синтаксис методов
    16. 5.16. Вектора
    17. 5.17. Строки
    18. 5.18. Обобщённое программирование
    19. 5.19. Типажи
    20. 5.20. Типаж `Drop`
    21. 5.21. Конструкция `if let`
    22. 5.22. Типажи-объекты
    23. 5.23. Замыкания
    24. 5.24. Универсальный синтаксис вызова функций
    25. 5.25. Контейнеры и модули
    26. 5.26. `const` и `static`
    27. 5.27. Атрибуты
    28. 5.28. Псевдонимы типов
    29. 5.29. Приведение типов
    30. 5.30. Ассоциированные типы
    31. 5.31. Безразмерные типы
    32. 5.32. Перегрузка операций
    33. 5.33. Преобразования при разыменовании
    34. 5.34. Макросы
    35. 5.35. Сырые указатели
    36. 5.36. Небезопасный код
  6. 6. Эффективное использование Rust
    1. 6.1. Стек и куча
    2. 6.2. Тестирование
    3. 6.3. Условная компиляция
    4. 6.4. Документация
    5. 6.5. Итераторы
    6. 6.6. Многозадачность
    7. 6.7. Обработка ошибок
    8. 6.8. Выбор гарантий
    9. 6.9. Интерфейс внешних функций
    10. 6.10. Типажи `Borrow` и `AsRef`
    11. 6.11. Каналы сборок
    12. 6.12. Using Rust without the standard library
  7. 7. Нестабильные возможности Rust
    1. 7.1. Плагины к компилятору
    2. 7.2. Встроенный ассемблерный код
    3. 7.3. Без stdlib
    4. 7.4. Внутренние средства
    5. 7.5. Элементы языка
    6. 7.6. Продвинутое руководство по компоновке
    7. 7.7. Тесты производительности
    8. 7.8. Синтаксис упаковки и шаблоны `match`
    9. 7.9. Шаблоны `match` для срезов
    10. 7.10. Ассоциированные константы
    11. 7.11. Пользовательские менеджеры памяти
  8. 8. Глоссарий
  9. 9. Syntax Index
  10. 10. Библиография

Макросы

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

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

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

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

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

Определение макросов (Макроопределения)

Вы, возможно, видели макрос vec!, который используется для инициализации вектора с произвольным количеством элементов.

let x: Vec<u32> = vec![1, 2, 3];Run

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

let x: Vec<u32> = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};Run

Мы можем реализовать это сокращение, используя макрос: 1

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}Run

Ого, тут много нового синтаксиса! Давайте разберем его.

macro_rules! vec { ... }Run

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

Сопоставление (Matching) (Синтаксис вызова макрокоманды)

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

( $( $x:expr ),* ) => { ... };Run

Это очень похоже на конструкцию match, но сопоставление происходит на уровне синтаксических деревьев Rust, на этапе компиляции. Точка с запятой не является обязательной для последнего (только здесь) варианта. «Образец» слева от => известен как шаблон совпадений (образец) (обнаружитель совпадений) (matcher). Он имеет свою собственную грамматику в рамках языка.

Образец $x:expr будет соответствовать любому выражению Rust, связывая его дерево синтаксиса с метапеременной $x. Идентификатор expr является спецификатором фрагмента; полные возможности перечислены далее в этой главе. Образец, окруженный $(...),*, будет соответствовать нулю или более выражениям, разделенным запятыми.

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

macro_rules! foo {
    (x => $e:expr) => (println!("mode X: {}", $e));
    (y => $e:expr) => (println!("mode Y: {}", $e));
}

fn main() {
    foo!(y => 3);
}Run

выведет

mode Y: 3

А с

foo!(z => 3);Run

мы получим ошибку компиляции

error: no rules expected the token `z`

Развертывание (Expansion) (Синтаксис преобразования макрокоманды)

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

$(
    temp_vec.push($x);
)*Run

Каждое соответствующее выражение $x будет генерировать одиночный оператор push в развернутой форме макроса. Повторение в развернутой форме происходит синхронно с повторением в форме образца (более подробно об этом чуть позже).

Поскольку $x уже объявлен в образце как выражение, мы не повторяем :expr с правой стороны. Кроме того, мы не включаем разделяющую запятую в качестве части оператора повторения. Вместо этого, у нас есть точка с запятой в пределах повторяемого блока.

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

macro_rules! foo {
    () => {{
        ...
    }}
}Run

Внешние скобки являются частью синтаксиса macro_rules!. На самом деле, вы можете использовать () или [] вместо них. Они просто разграничивают правую часть в целом.

Внутренние скобки являются частью расширенного синтаксиса. Помните, что макрос vec! используется в контексте выражения. Мы используем блок, для записи выражения с множественными операторами, в том числе включающее let привязки. Если ваш макрос раскрывается в одно единственное выражение, то дополнительной слой скобок не нужен.

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

Повторение (Repetition) (Многовариантность)

Операции повтора всегда сопутствуют два основных правила:

  1. $(...)* проходит через один «слой» повторений, для всех $name, которые он содержит, в ногу, и
  2. каждое $name должно быть под, по крайней мере, стольким количеством $(...)*, сколько было использовано при сопоставлении. Если оно под большим числом $(...)*, $name будет дублироваться, при необходимости.

Этот причудливый макрос иллюстрирует дублирования переменных из внешних уровней повторения.

macro_rules! o_O {
    (
        $(
            $x:expr; [ $( $y:expr ),* ]
        );*
    ) => {
        &[ $($( $x + $y ),*),* ]
    }
}

fn main() {
    let a: &[i32]
        = o_O!(10; [1, 2, 3];
               20; [4, 5, 6]);

    assert_eq!(a, [11, 12, 13, 24, 25, 26]);
}Run

Это наибольшая синтаксиса совпадений. Эти примеры используют конструкцию $(...)*, которая означает «ноль или более» совпадений. Также вы можете написать $(...)+, что будет означать «одно или более» совпадений. Обе формы записи включают необязательный разделитель, располагающийся сразу за закрывающей скобкой, который может быть любым символом, за исключением + или *.

Эта система повторений основана на «Macro-by-Example» (PDF ссылка).

Гигиена (Hygiene)

Некоторые языки реализуют макросы с помощью простой текстовой замены, что приводит к различным проблемам. Например, нижеприведенная C программа напечатает 13 вместо ожидаемого 25.

#define FIVE_TIMES(x) 5 * x

int main() {
    printf("%d\n", FIVE_TIMES(2 + 3));
    return 0;
}

После развертывания мы получаем 5 * 2 + 3, но умножение имеет больший приоритет чем сложение. Если вы часто использовали C макросы, вы, наверное, знаете стандартные идиомы для устранения этой проблемы, а также пять или шесть других проблем. В Rust мы можем не беспокоиться об этом.

macro_rules! five_times {
    ($x:expr) => (5 * $x);
}

fn main() {
    assert_eq!(25, five_times!(2 + 3));
}Run

Метапеременная $x обрабатывается как единый узел выражения, и сохраняет свое место в дереве синтаксиса даже после замены.

Другой распространенной проблемой в системе макросов является захват переменной (variable capture). Вот C макрос, использующий GNU C расширение, который эмулирует блоки выражениий в Rust.

#define LOG(msg) ({ \
    int state = get_log_state(); \
    if (state > 0) { \
        printf("log(%d): %s\n", state, msg); \
    } \
})

Вот простой случай использования, применение которого может плохо кончиться:

const char *state = "reticulating splines";
LOG(state)

Он раскрывается в

const char *state = "reticulating splines";
int state = get_log_state();
if (state > 0) {
    printf("log(%d): %s\n", state, state);
}

Вторая переменная с именем state затеняет первую. Это проблема, потому что команде печати требуется обращаться к ним обоим.

Эквивалентный макрос в Rust обладает требуемым поведением.

macro_rules! log {
    ($msg:expr) => {{
        let state: i32 = get_log_state();
        if state > 0 {
            println!("log({}): {}", state, $msg);
        }
    }};
}

fn main() {
    let state: &str = "reticulating splines";
    log!(state);
}Run

Это работает, потому что Rust имеет систему макросов с соблюдением гигиены. Раскрытие каждого макроса происходит в отдельном контексте синтаксиса, и каждая переменная обладает меткой контекста синтаксиса, где она была введена. Это как если бы переменная state внутри main была бы окрашена в другой «цвет» в отличае от переменной state внутри макроса, из-за чего они бы не конфликтовали.

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

macro_rules! foo {
    () => (let x = 3);
}

fn main() {
    foo!();
    println!("{}", x);
}Run

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

macro_rules! foo {
    ($v:ident) => (let $v = 3);
}

fn main() {
    foo!(x);
    println!("{}", x);
}Run

Это справедливо для let привязок и меток loop, но не для элементов. Код, приведенный ниже, компилируется:

macro_rules! foo {
    () => (fn x() { });
}

fn main() {
    foo!();
    x();
}Run

Рекурсия макросов

Раскрытие макроса также может включать в себя вызовы макросов, в том числе вызовы того макроса, который раскрывается. Эти рекурсивные макросы могут быть использованы для обработки древовидного ввода, как показано на этом (упрощенном) HTML сокращение:

macro_rules! write_html {
    ($w:expr, ) => (());

    ($w:expr, $e:tt) => (write!($w, "{}", $e));

    ($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{
        write!($w, "<{}>", stringify!($tag));
        write_html!($w, $($inner)*);
        write!($w, "</{}>", stringify!($tag));
        write_html!($w, $($rest)*);
    }};
}

fn main() {
    use std::fmt::Write;
    let mut out = String::new();

    write_html!(&mut out,
        html[
            head[title["Macros guide"]]
            body[h1["Macros are the best!"]]
        ]);

    assert_eq!(out,
        "<html><head><title>Macros guide</title></head>\
         <body><h1>Macros are the best!</h1></body></html>");
}Run

Отладка макросов

Чтобы увидеть результаты расширения макросов, выполните команду rustc --pretty expanded. Вывод представляет собой целый контейнер, так что вы можете подать его обратно в rustc, что иногда выдает лучшие сообщения об ошибках, чем при обычной компиляции. Обратите внимание, что вывод --pretty expanded может иметь разное значение, если несколько переменных, имеющих одно и то же имя (но разные контексты синтаксиса), находятся в той же области видимости. В этом случае --pretty expanded,hygiene расскажет вам о контекстах синтаксиса.

rustc, поддерживает два синтаксических расширения, которые помогают с отладкой макросов. В настоящее время, они неустойчивы и требуют feature gates.

Требования синтаксиса

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

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

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

Другое следствие разбора перед раскрытием макросов — это то, что вызов макроса должен состоять из допустимых лексем. Более того, скобки всех видов должны быть сбалансированы в месте вызова. Например, foo!([) не является разрешённым кодом. Такое поведение позволяет компилятору понимать где заканчивается вызов макроса.

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

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

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

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

И ещё: система макросов никак не обрабатывет неоднозначность разбора. Например, грамматика $($t:ty)* $e:expr всегда будет выдавать ошибку, потому что синтаксическому анализатору пришлось бы выбирать между разбором $t и разбором $e. Можно изменить синтаксис вызова так, чтобы грамматика отличалась в начале. В данном случае можно написать $(T $t:ty)* E $e:exp.

Области видимости, импорт и экспорт макросов

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

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

Макрос, определённый в теле функции, или где-то ещё не на уровне модуля, виден только внутри этого элемента (например, внутри одной функции).

Если модуль имеет атрибут macro_use, то его макросы также видны в его родительском модуле после элемента mod данного модуля. Если родитель тоже имеет атрибут macro_use, макросы также будут видны в модуле-родителе родителя, после элемента mod родителя. Это распространяется на любое число уровней.

Атрибут macro_use также можно поставить на подключение контейнера extern crate. В этом контексте оно управляет тем, какие макросы будут загружены из внешнего контейнера, т.е.

#[macro_use(foo, bar)]
extern crate baz;Run

Если атрибут записан просто как #[macro_use], будут загружены все макросы. Если атрибута нет, никакие макросы не будут загружены. Загружены могут быть только макросы, объявленные с атрибутом #[macro_export].

Чтобы загрузить макросы из контейнера без компоновки контейнера в выходной артефакт, можно использовать атрибут #[no_link].

Например:

macro_rules! m1 { () => (()) }

// здесь видны: m1

mod foo {
    // здесь видны: m1

    #[macro_export]
    macro_rules! m2 { () => (()) }

    // здесь видны: m1, m2
}

// здесь видны: m1

macro_rules! m3 { () => (()) }

// здесь видны: m1, m3

#[macro_use]
mod bar {
    // здесь видны: m1, m3

    macro_rules! m4 { () => (()) }

    // здесь видны: m1, m3, m4
}

// здесь видны: m1, m3, m4Run

Когда эта библиотека загружается с помощью #[macro_use] extern crate, виден только макрос m2.

Атрибуты, относящиеся к макросам, перечислены в справочнике Rust.

Переменная $crate

Если макрос используется в нескольких контейнерах, всё становится ещё сложнее. Допустим, mylib определяет

pub fn increment(x: u32) -> u32 {
    x + 1
}

#[macro_export]
macro_rules! inc_a {
    ($x:expr) => ( ::increment($x) )
}

#[macro_export]
macro_rules! inc_b {
    ($x:expr) => ( ::mylib::increment($x) )
}Run

inc_a работает только внутри mylib, а inc_b — только снаружи. Более того, inc_b сломается, если пользователь импортирует mylib под другим именем.

В Rust пока нет гигиеничных ссылок на контейнеры, но есть простой способ обойти эту проблему. Особая макро-переменная $crate раскроется в ::foo внутри макроса, импортированного из контейнера foo. А когда макрос определён и используется в одном и том же контейнере, $crate станет пустой. Это означает, что мы можем написать

#[macro_export]
macro_rules! inc {
    ($x:expr) => ( $crate::increment($x) )
}Run

чтобы определить один макрос, который будет работать и внутри, и снаружи библиотеки. Имя функции раскроется или в ::increment, или в ::mylib::increment.

Чтобы эта система работала просто и правильно, #[macro_use] extern crate ... может быть написано только в корне вашего контейнера, но не внутри mod. Это обеспечивает, что $crate раскроется в единственный идентификатор.

Во тьме глубин

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

Приведём такой радикальный пример использования данной возможности. С помощью рекурсивных макросов можно реализовать конечный автомат типа Bitwise Cyclic Tag. Стоит заметить, что мы не рекомендуем такой подход, а просто иллюстрируем возможности макросов.

macro_rules! bct {
    // cmd 0:  d ... => ...
    (0, $($ps:tt),* ; $_d:tt)
        => (bct!($($ps),*, 0 ; ));
    (0, $($ps:tt),* ; $_d:tt, $($ds:tt),*)
        => (bct!($($ps),*, 0 ; $($ds),*));

    // cmd 1p:  1 ... => 1 ... p
    (1, $p:tt, $($ps:tt),* ; 1)
        => (bct!($($ps),*, 1, $p ; 1, $p));
    (1, $p:tt, $($ps:tt),* ; 1, $($ds:tt),*)
        => (bct!($($ps),*, 1, $p ; 1, $($ds),*, $p));

    // cmd 1p:  0 ... => 0 ...
    (1, $p:tt, $($ps:tt),* ; $($ds:tt),*)
        => (bct!($($ps),*, 1, $p ; $($ds),*));

    // halt on empty data string
    ( $($ps:tt),* ; )
        => (());
}Run

В качестве упражнения предлагаем читателю определить ещё один макрос, чтобы уменьшить степень дублирования кода в определении выше.

Распространённые макросы

Вот некоторые распространённые макросы, которые вы увидите в коде на Rust.

panic!

Этот макрос вызывает панику текущего потока. Вы можете указать сообщение, с которым поток завершится:

panic!("о нет!");Run

vec!

Макрос vec! используется по всей книге, поэтому вы наверняка уже видели его. Он упрощает создание Vec<T>:

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

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

let v = vec![0; 100];Run

assert! and assert_eq!

Эти два макроса используются в тестах. assert! принимает логическое значение. assert_eq! принимает два значения и проверяет, что они равны. true засчитывается как успех, а false вызывает панику и проваливает тест. Вот так:

// Работает!

assert!(true);
assert_eq!(5, 3 + 2);

// а это нет :(

assert!(5 < 3);
assert_eq!(5, 3);Run

try!

try! используется для обработки ошибок. Он принимает нечто возвращающее Result<T, E> и возвращает T если было возвращено Ok<T>; иначе он делает возврат из функции со значением Err(E). Вроде такого:

use std::fs::File;

fn foo() -> std::io::Result<()> {
    let f = try!(File::create("foo.txt"));

    Ok(())
}Run

Такой код читается легче, чем этот:

use std::fs::File;

fn foo() -> std::io::Result<()> {
    let f = File::create("foo.txt");

    let f = match f {
        Ok(t) => t,
        Err(e) => return Err(e),
    };

    Ok(())
}Run

unreachable!

Этот макрос применяется, когда вы хотите пометить какой-то код, который никогда не должен исполняться:

if false {
    unreachable!();
}Run

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

let x: Option<i32> = None;

match x {
    Some(_) => unreachable!(),
    None => println!("Я знаю, что x — это None!"),
}Run

unimplemented!

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

Процедурные макросы

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


  1. Фактическое определение vec! в libcollections отличается от представленного здесь по соображениям эффективности и повторного использования.