Skip to content

Latest commit

 

History

History
835 lines (517 loc) · 63.3 KB

rust_guide.md

File metadata and controls

835 lines (517 loc) · 63.3 KB

Руководство Rust

2 Hello, World!

Теперь, когда вы установили Rust, напишем первую вашу программу на Rust. Это традиция - в любом новом языке программирования делать первую программу, которая выводит текст "Hello, World!" на экран. Хорошо начинать с такой простой программы потому, что вы можете убедиться что ваш компилятор не только установлен, но и работает правильно. И вывод информации на экран является довольно обычным способом для этого.

Первое с чего мы должны начать это создать файл для нашего кода. Мне нравится размещать каталог projects в домашнем каталоге, и хранить все мои проекты там. Для Rust неважно, где располагается ваш код.

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

С учетом сказанного, давайте сделаем каталог в нашем каталоге с проектами.

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Если вы используете Windows и не используете PowerShell, ~ может не работать. Обратитесь к документации вашей оболочки для уточнения деталей.

Теперь создадим новый файл для текста программы. Я собираюсь использовать синтаксис editor filename для обозначения редактируемого файла в этих примерах, но вам следует использовать любой метод, который хотите. Назовем наш файл main.rs:

$ editor main.rs

Rust-файлы всегда заканчиваются расширением .rs. Если вы используете больше одного слова в имени вашего файла используйте подчеркивание. hello_world.rs лучше чем helloworld.rs.

Теперь когда файл открыт, запишите в него:

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

Сохраните файл и затем введите в вашем окне терминала:

$ rustc main.rs
$ ./main # или main.exe в Windows
Hello, world!

Успех! Разберем что же случилось подробнее.

fn main() {

}

Эти строки определяют функцию в Rust'е. Функция main особенная: это начало каждой программы на Rust. Первая строка говорит "Я объявляю функцию именуемую main, которая не получает параметров и ничего не возвращает". Если бы были параметры, они бы шли в скобках (( и )), и потому что мы ничего не возвращаем из этой функции, мы опустим эту запись полностью. Мы вернемся к этому позже.

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

Теперь эта строка:

println!("Hello, world!");

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

Во-вторых, это часть с println!(). Это вызов макроса Rust, которым представлено метапрограммирование в Rust'e. Если бы вместо этого была функция, это бы выглядело следующим образом: println(). Для достижения нашей цели, нас не должна волновать эта разница. Просто знайте, что иногда вы будете видеть !, и это означает, что вы вызываете макрос вместо обычной функции. Rust реализует println! как макрос вместо функции по веским причинам, но это очень углубленная тема. Вы узнаете больше, когда мы будем позже рассказывать о макросах. И последнее, что нужно отметить: макросы Rust'a значительно отличаются от макросов на C, если вы их использовали. Не бойтесь использовать макросы. В конце концов мы вернемся к деталям, а сейчас просто доверьтесь нам.

Дальше, "Hello, world!" это строка. Строки удивительно сложная тема в системном языке программирования и это statically allocated строка. Мы расскажем больше о различных видах распределения памяти позже. Передадим эту строку в качестве аргумента в println!, который выводит строки на экран. Это достаточно просто!

В завершение, строка заканчивается точкой с запятой(;). Rust выражение-ориентированный язык, что означает, что в нем большая часть вещей является выражением. ; используется для указания, что это выражение заканчивается, а следующее начинается. Большинство строк кода на Rust заканчивается ;. Мы рассмотрим это позже в отдельном разделе руководства.

На самом деле, завершением будет сборка и запуск нашей программы. Соберем нашим компилятором rustc, передав ему в качестве аргумента название нашего файла с кодом:

$ rustc main.rs

Это похоже на gcc или clang, если вы программировали раньше на C или C++. Rust выдаст двоичный исполняемый файл. Вы можете убедиться в этом с помощью ls:

$ ls
main main.rs

Или в Windows:

$ dir
main.exe main.rs

Это два файла: наш исходный код, с расширением .rs и исполняемый файл (main.exe в Windows, main в остальных случаях)

$ ./main  # или main.exe в Windows

Это выведет наш "Hello, world!" текст в наш терминал.

Если вы перешли из динамически-типизированных языков программирования вроде Ruby, Python или JavaScript, вы не можете использовать эти два шага отдельно. Rust компилируемый перед исполнением язык, это означает, что вы можете собрать программу, дать ее кому-то еще, и ему не нужно устанавливать Rust. Если вы передадите один из .rb или .py или .js файл, ему нужно будет установить Ruby/Python/JavaScript, но запустить только одну команду для каждого из них, чтобы скомпилировать и запустить вашу программу. Все это взаимоисключаемо в дизайне языков программирования, и Rust сделал свой выбор.

Поздравляем! Вы официально написали программу на Rust. Это делает вас Rust-программистом! Добро пожаловать.

3 Hello, Cargo!

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

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

Преобразуем наш Hello World в Cargo. Первая вещь, которую мы должны сделать для того, что бы начать использовать Cargo - это установить его. К счастью для нас, скрипт который мы запускали для установки Rust по умолчанию включает в себя установку Cargo. Если вы установили Rust каким-либо другим способом, вы должны проверить Cargo README для специальных инструкций по установке.

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

$ mkdir src
$ mv main.rs src/main.rs

Cargo ожидает, что ваши файлы с исходным кодом находятся в директории src. Это оставляет верхний уровень для других вещей, вроде README, файлов с текстом лицензии и других не относящихся к вашему коду. Cargo помогает нам сохранять наши проекты красивыми и аккуратными. Всему своё место и всё на своём месте.

Дальше, наш конфигурационный файл:

$ editor Cargo.toml

Убедитесь, что имя правильное: вам нужна заглавная C!

Разместите это внутри:

[package]

name = "hello_world"
version = "0.0.1"
authors = [ "Ваше имя <you@example.com>" ]

[[bin]]

name = "hello_world"

Этот файл в формате TOML. Позволим ему самому объясниться с вами:

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

TOML очень похож на INI, но с некоторыми дополнительными возможностями.

В любом случае, в этом файле есть две записи: package и bin. Первая сообщает Cargo метаданные вашего пакета. Вторая сообщает Cargo что мы заинтересованы в сборке исполняемого файла, а не библиотеки (хотя мы могли сделать и то и другое!), и как его следует назвать.

Как только мы с этим закончили, мы готовы к сборке! Попробуйте собрать:

$ cargo build
    Compiling hello_world v0.0.1 (file:///home/yourname/projects/hello_world)
$ ./target/hello_world
Hello, world!

Та-да! Мы собрали наш проект вызвав cargo build, и запустили его с помощью ./target/hello_world. Нас этим не купить по сравнению с более простым использованием rustc, но подумаем о будущем: если бы в нашем проекте было больше одного файла мы бы должны были вызвать rustc для каждого и передать ему кучу параметров, что бы собрать их все вместе. С Cargo когда наш проект вырастет нам понадобится вызвать только команду cargo build и она должна будет работать правильно.

Так же вы должны были заметить что Cargo создал новый файл: Cargo.lock.

[root]
name = "hello_world"
version = "0.0.1"

Этот файл используется Cargo для отслеживания зависимостей в вашем приложении. Прямо сейчас у нас нет ни одной, поэтому этот файл практически пуст. Вам не нужно править этот файл самостоятельно, Cargo сам с ним разберется.

Так! Мы успешно собрали hello_world с помощью Cargo. Несмотря на то, что наша программа проста, мы использовали большую часть реальных инструментов, которые вы будете использовать в своем дальнейшем пути Rust программиста.

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

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

Первая вещь которую мы должны знать о "связывании переменных" - это выглядит примерно так:

let x = 5i;

Во многих языках программирования это называется "переменная". Но связывание переменных Rust'а имеет пару трюков в рукаве. Rust имеет мощную особенность называемую 'шаблоном', который мы рассмотрим позже более детально, с левой стороны выражение let это полный шаблон, а не просто имя переменной. Это значит что мы можем делать вещи вроде этой:

let (x, y) = (1i, 2i);

После завершения этого выражения x будет единицей, a y двойкой. Шаблоны правда мощны, но пока это все, что мы можем с ними сделать. Так что придержите это в уме, продвигаясь дальше.

В этом примере, кстати, i показывает что число является целым числом.

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

Тем не менее, мы можем указать желаемый тип. Он следует после двоеточия (:):

let x: int = 5;

Если бы я попросил вас прочитать это вслух и по-порядку, вы бы сказали "x связан с типом int и значением пять"

По-умолчанию, связывание неизменяемо. Этот код не скомпилируется:

let x = 5i;
x = 10i;

Мы получим эту ошибку:

error: re-assignment of immutable variable `x`
    x = 10i;
    ^~~~~~~

Если вы хотите что бы связывание было изменяемым, вы можете использовать mut:

let mut x = 5i;
x = 10i;

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

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

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

let x;

...мы получим ошибку:

src/main.rs:2:9: 2:10 error: cannot determine a type for this local variable: unconstrained type
src/main.rs:2     let x;
                      ^

Хотя указание типа позволит собрать:

let x: int;

Давайте проверим. Измените ваш src/main.rs файл, что бы он выглядел следующим образом:

fn main() {
    let x: int;

    println!("Hello world!");
}

Вы можете использовать cargo build в командной строке, что бы собрать его. Вы получите предупреждение, но "Hello, world!" будет выведено:

   Compiling hello_world v0.0.1 (file:///home/you/projects/hello_world)
src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variable)] on by default
src/main.rs:2     let x: int;
                             ^

Rust предупредит нас о том, что мы никогда не используем связанную переменную, но, так как мы не используем ее, никакого вреда и нарушения в этом нет. Однако все изменится если мы попробуем использовать x. Сделаем это. Измените вашу программу так, что бы она выглядела следующим образом:

fn main() {
    let x: int;

    println!("The value of x is: {}", x);
}

И попробуйте собрать. Вы получите ошибку:

$ cargo build
   Compiling hello_world v0.0.1 (file:///home/you/projects/hello_world)
src/main.rs:4:39: 4:40 error: use of possibly uninitialized variable: `x`
src/main.rs:4     println!("The value of x is: {}", x);
                                                    ^
note: in expansion of format_args!
<std macros>:2:23: 2:77 note: expansion site
<std macros>:1:1: 3:2 note: in expansion of println!
src/main.rs:4:5: 4:42 note: expansion site
error: aborting due to previous error
Could not compile `hello_world`.

Rust не позволит нам использовать неинициализированную переменную. Далее, поговорим о том, что мы добавили в println!.

Если вы добавите две фигурные скобки ({}, иногда называемые "усами"...) в вашу печатаемую строку, Rust истолкует это как просьбу своего рода вставки значения. Строковая вставка это термин в информатике, который обозначает "вставить посреди строки". Мы добавили запятую, и затем x, что бы указать, что мы хотим вставить x в строку. Запятая используется для разделения параметров передаваемых нами в функции и макросы, если вы передаете больше одного.

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

5 If

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

В случае с if, тут выбор одного из двух путей:

let x = 5i;

if x == 5i {
    println!("x is five!");
}

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

Если вы хотите чего-либо в случае с false, используйте else:

let x = 5i;

if x == 5i {
    println!("x is five!");
} else {
    println!("x is not five :(");
}

Это все довольно стандартно. Однако, вы также можете сделать вот так:

let x = 5i;

let y = if x == 5i {
    10i
} else {
    15i
};

Что мы можем (и вероятно должны) написать так:

let x = 5i;

let y = if x == 5i { 10i } else { 15i };

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

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

Так в чем же разница? Выражение возвращает значение, а оператор - нет. Во многих языках программирования if это оператор, следовательно let x = if ... не имело бы смысла. Но в Rust'e if это выражение, которое в состоянии вернуть значение. Мы можем использовать это значение для инициализации при связывании. Кстати говоря, связывание это первый из двух видов операторов в Rust'е. Его более точное название это оператор объявления. Пока что let это единственный оператор объявления который мы видели. Давайте поговорим об этом еще немного.

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

x = y = 5

В Rust'е, однако, использование let для связывания переменных не является выражением. Следующее выдаст ошибку во время сборки:

let x = (let y = 5i); // expected identifier, found keyword `let`

Тут компилятор нам скажет, что ожидает увидеть начало выражения, а let может начать только оператор, а не выражение.

Обратите внимание, что присваивание уже связанной переменной (например, у = 5i) является выражением, хотя его значение не особенно полезно. В отличие от C, присваивание приравнивается к присвоенному значению (например, 5i в предыдущем примере), в Rust'е значение выражения присваивания это блочный (?) тип () (о котором мы расскажем позже).

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

Что за исключение, которое нас заставляет говорить "почти"? Ты уже видел это в этом коде:

let x = 5i;

let y: int = if x == 5i { 10i } else { 15i };

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

let x = 5i;

let y: int = if x == 5i { 10i; } else { 15i; };

Обратите внимание на точки с запятыми после 10 и 15. Rust выдаст нам следующую ошибку:

error: mismatched types: expected `int` but found `()` (expected int but found ())

Мы ожидали целое число, а получили (). () произносится как "блок", и это специальный тип в системе типов Rust'а. В Rust'е () это некорректное значение для переменной типа int. Это является допустимым значением только для переменных типа (), которые не очень полезны. Помните, как мы говорили, что операторы не возвращают значения? Это предназначение блока в таких случаях. Точка с запятой превращает любое выражение в оператор путем выбрасывания его значения и возвращения блока вместо этого.

Есть еще один случай, в котором вы не увидите точку с запятой в конце строки в коде Rust'a. Для этого нам понадобится следующее понятие: функции.

##6 Функции## Вы уже видели одну функцию раньше, функция main:

fn main() {
}

Это наипростейший из возможных способов объявления функции. Как мы уже говорили ранее,fn говорит "это функция", затем ее имя, затем одинокие скобки, потому что функция не принимает параметров, затем фигурные скобки указывающие на тело функции. Это функция с названием foo:

fn foo() {
}

Так что на счет передачи параметров? Эта функция которая печатает число:

fn print_number(x: int) {
    println!("x is: {}", x);
}

Это полная программа, которая использует print_number:

fn main() {
    print_number(5);
}

fn print_number(x: int) {
    println!("x is: {}", x);
}

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

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

fn main() {
    print_sum(5, 6);
}

fn print_sum(x: int, y: int) {
    println!("sum is: {}", x + y);
}

Вы разделяете параметры запятой, как при вызове функции, так и при ее объявлении.

В отличие от let, вы должны указывать типы параметров функции. Это не работает:

fn print_number(x, y) {
    println!("x is: {}", x + y);
}

Вы получите эту ошибку:

hello.rs:5:18: 5:19 error: expected `:` but found `,`
hello.rs:5 fn print_number(x, y) {

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

Что насчет возвращения значения? Это функция, которая добавляет единицу к целому числу:

fn add_one(x: int) -> int {
    x + 1
}

Функции Rust'a возвращают только одно значение, и вы объявляете тип после "стрелки", состоящей из тире (-) и знака больше (>). Вы должны были заметить отсутствующие здесь точку с запятой. Если мы их добавим сюда:

fn add_one(x: int) -> int {
    x + 1;
}

Мы получим ошибку:

error: not all control paths return a value
fn add_one(x: int) -> int {
     x + 1;
}

note: consider removing this semicolon:
     x + 1;
          ^

Помните нашу старую дисскуссию о точке запятой и ()? Наша функция требует вернуть int, но изза точки c запятой вместо этого возвращается (). Rust понимает, что верятно это не то чего мы хотим, поэтому он предлагает удалить точку с запятой.

Это очень похоже на наш предыдущий оператор if: результат блока ({}) это значение выражения. Другие выражение-ориентированные языки программирования, вроде Ruby, работают похоже, но они немного неприменимы в мире системного программирования. Когда люди впервые узнают об этом, они, как правило, предполагают, что это вступительные (? новичка? тутора? молодого языка программирования?) ошибки. Но поскольку система типов Rust'а так сильна, и поскольку блок его собственный уникальный тип, мы никогда не увидем, как удаление или возвращение точки с запятой станет причиной ошибки.

Но что на счет предварительного возврата? Rust имеет ключевое слово для этого - return:

fn foo(x: int) -> int {
    if x < 5 { return x; }

    x + 1
}

Использоваие return как последней строки функции будет работать, но это считается плохим тоном:

fn foo(x: int) -> int {
    if x < 5 { return x; }

    return x + 1;
}

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

7 Комментарии

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

Rust имеет два вида комментариев, которые должны вас беспокоить: строчные комментарии и doc-комментарии.

// Строчные комментарии это все что угодно после '//' и до конца строки.

let x = 5i; // это тоже строчный комментарий.

// Если у вас длинное объяснение для чего-либо, вы можете расположить строчные комментарии
// один за другим. Поместите пробел между // и вашим комментарием, так как это более читаемо.

Другое применение комментария - это doc-комментарий. Doc-комментарий использует /// вместо //, и поддерживает Markdown-разметку внутри:

/// `hello` это функция которая выводит на экран персональное приветствие
/// основанное на полученном имени
///
/// # Параметры
///
/// * `name` - Имя особы, которую вы хотите поприветствовать.
/// 
/// # Пример
///
/// ```rust
/// let name = "Steve";
/// hello(name); // выведет "Hello, Steve!"
/// ```
fn hello(name: &str) {
    println!("Hello, {}!", name);
}

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

Вы можете использовать инструмент rustdoc для генерации HTML-документации из этих doc-комментариев. Мы расскажем больше о rustdoc когда дойдем до модулей, в частности, вам понадобится экспортировать документацию для всего модуля.

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

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

8.1 Кортежи

Первый составной тип данных о котором мы поговорим называется кортежом. Кортеж это упорядоченный список фиксированного размера. Вроде этого:

let x = (1i, "hello");

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

let x: (int, &str) = (1, "hello");

Как вы видите, тип кортежа выглядит как кортеж, но на соответственных позициях имеется название типа данных который предпочтителен для значения. Внимательный читатель так же заметит что кортежи гетерогенны: в этом кортеже есть int и &str. Вы не видели &str как тип раньше, но мы обсудим детали строк позже. В системных языках программирования, строки немного более сложные чем в остальных. Сейчас просто читайте &str как "строковой срез", но скоро мы узнаем больше.

У вас есть доступ к полям кортежа через оператор деструктуризации. Это пример:

let (x, y, z) = (1i, 2i, 3i);

println!("x is {}", x);

Помните ранее я говорил, что стоящий в левой части выражения оператор let является несколько мощным чем просто обозначение связывания? Вот оно. Мы можем поместить шаблон в левую часть выражения c let , и если он совпадает с правым, мы можем связать несколько значений за раз. (прим. перев.: множественное присваивание, как в python, к примеру). В данном случае let 'деструктуризирует', или по-другому, 'разбивает' кортеж и связывает три части.

Этот шаблон очень мощный, и мы дальше увидим это повторно.

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

let mut x = (1i, 2i);
let y = (2i, 3i);

x = y;

Вы также можете проверить эквивалентность с ==. Опять же, это скомпилируется только если кортежи имеют одинаковые типы.

let x = (1i, 2i, 3i);
let y = (2i, 2i, 4i);

if x == y {
    println!("yes");
} else {
    println!("no");
}

Это выведет no, потому что некоторые значения не эквивалентны.

Одно из других способов использовать кортежи это возвращение нескольких значений из функции:

fn next_two(x: int) -> (int, int) { (x + 1i, x + 2i) }

fn main() {
    let (x, y) = next_two(5i);
    println!("x, y = {}, {}", x, y);
}

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

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

8.2 Структуры

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

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

fn main() {
    let origin = Point { x: 0i, y: 0i };

    println!("The origin is at ({}, {})", origin.x, origin.y);
}

Тут очень много чего происходит, так что давайте по-порядку. Мы объявляем структуру с ключевым словом struct и следующим за ним именем. По соглашению, имя структуры начинается с заглавной буквы и далее следуют стилю CamelCase: PointInSpace, но не Point_In_Space.

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

И наконец, потому что поле имеет имя, мы можем обратиться к нему через точечную нотацию: origin.x.

Значения в структуре иммутабельны, как и в других случаях связывания в Rust'е. Тем не менее, вы можете использовать mut, что бы сделать их изменяемыми:

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

fn main() {
    let mut point = Point { x: 0i, y: 0i };

    point.x = 5;

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

Это выведет The point is at (5, 0).

8.3 Кортежные структуры и новотипы

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

struct Color(int, int, int);
struct Point(int, int, int);

Эти две не эквивалентны, даже если имеют те же значения:

let black  = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Почти всегда лучше использовать структуру вместо кортежной структуры. Вместо этого мы бы так описали Color и Point:

struct Color {
    red: int,
    blue: int,
    green: int,
}

struct Point {
    x: int,
    y: int,
    z: int,
}

Теперь у нас есть актуальные имена, а не просто позиции. Хорошие имена важны, а со структурой у нас есть актуальные имена.

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

struct Inches(int);

let length = Inches(10);

let Inches(integer_length) = length;
println!("length is {} inches", integer_length);

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

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

И наконец, в Rust'е есть "перечислимый тип" (прим. перев.:тип-сумма, sum type, меченное объединение), перечисление. Перечисления являются невероятной особенностью Rust'a, и используются повсюду в стандартной библиотеке. Это перечисление представлено в стандартной библиотеке Rust.

enum Ordering {
    Less,
    Equal,
    Greater,
}

Ordering может быть только одним из Less, Equal, или Greater в один момент времени. Например:

fn cmp(a: int, b: int) -> Ordering {
    if a < b { Less }
    else if a > b { Greater }
    else { Equal }
}

fn main() {
    let x = 5i;
    let y = 10i;

    let ordering = cmp(x, y);

    if ordering == Less {
        println!("less");
    } else if ordering == Greater {
        println!("greater");
    } else if ordering == Equal {
        println!("equal");
    }
}

cmp это функция которая сравнивает два значения и возвращает Ordering. Нам вернется либо Less, либо Greater, либо Equal, в зависимости от того, являются ли два значения больше, меньше, либо равно.

Переменная ordering имеет тип Ordering, и она так же содержит одно из трех значений. Затем мы можем проверить с помощью сравнения if/else какое из них в ней.

Однако повторение if/else для сравнения может быть довольно утомительным. Rust имеет особенность, которая делает сравнение не только более читаемым, но также гарантирует, что вы ничего не пропустите. Прежде чем перейдем к ней, все-таки, давайте поговорим о другом виде перечисления: один со значением.

Это перечисление имеет два варианта, один из которых имеет значение:

enum OptionalInt {
    Value(int),
    Missing,
}

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

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

enum OptionalColor {
    Color(int, int, int),
    Missing,
}

И вы можете так же использовать что-то вроде этого:

enum StringResult {
    StringOK(String),
    ErrorReason(String),
}

Где StringResult это либо StringOK, с результатом вычисления, либо ErrorReason со значением типа String, объясняющим, что привело к ошибке вычисления. Эти варианты перечислений действительно очень хороши и встречаются практически в большей части стандартной библиотеки.

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

fn respond(greeting: &str) -> StringResult {
    if greeting == "Hello" {
        StringResult::StringOK("Good morning!".to_string())
    } else {
        StringResult::ErrorReason("I didn't understand you!".to_string())
    }
}

Обратите внимание, что нам нужно как имя перечисления, так и имя варианта: StringResult::StringOK, что не нужно в случае с Ordering, где нам надо указать только Greater вместо Ordering::Greater. Это обоснованно: Rust предварительно импортирует варианты Ordering, так же как и само перечисление. Мы можем использовать ключевое слово use что бы сделать что-либо подобное с StringResult:

use StringResult::StringOK;
use StringResult::ErrorReason;

enum StringResult {
    StringOK(String),
    ErrorReason(String),
}


fn respond(greeting: &str) -> StringResult {
    if greeting == "Hello" {
        StringOK("Good morning!".to_string())
    } else {
        ErrorReason("I didn't understand you!".to_string())
    }
}

Мы узнаем больше о use позже, но он используется для внедрения имен в текущее пространство. Объявления use должны идти перед чем-либо еще, что выглядит немного странно в этом примере, так как мы use варианты перед их определением. В любом случае в теле respond мы теперь можем использовать StringOK, вместо полной формы StringResult::StringOK. Импорт вариантов может быть удобным, но это может привести к конфликтам имен, так что делайте это осторожно. По этой причине считается хорошим тоном импортирование вариантов как можно реже.

Как вы можете видеть, перечисления со значениями могут быть весьма мощным инструментом представления данных и может быть еще более мощным, когда они обобщенные для разных типов. Но прежде чем мы перейдем к обобщенным типам, поговорим о том, как использовать перечисления со значением с сопоставлением шаблонов, инструментом, который позволит нам разобрать этот ?перечислимый тип? (в теории типов термин для перечислений) более элегантным путем и избежать этих грязных if/else'ов.

9 Сопоставление образцов

Зачастую простого if/else не достаточно, потому что у вас может быть более чем два варианта. В таком случае условия с else становятся невероятно сложными. Какое же решение?

В Rust'e есть ключевое слово match, которое позволит вам заменить сложные условия if/else чем-то более мощным. Посмотрите:

let x = 5i;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
    _ => println!("something else"),
}

match получает выражение и ветви основанные на его значениях. Каждое ответвление имеет форму val => expression. Когда значение совпадает, выполняется выражение в этом ответвлении. Имя оператора match пошло от термина сопоставление образца (pattern matching), реализацией которого является match.

Так что же в таком подходе намного лучше? Несколько вещей. Во-первых, match гарантирует исчерпывающую проверку. Видите в последнем ответвлении знак подчеркивания _? Если мы уберем эту ветку, Rust выдаст нам сообщение об ошибке:

error: non-exhaustive patterns: `_` not covered

Другими словами, Rust пытается сообщить нам, что мы забыли значение. Поскольку x целое число, Rust знает, что оно может иметь несколько больше значений. К примеру 6i. Но без _ нет ответвления, которое совпадет, и поэтому Rust откажется компилировать. Ветвь с _ это способ отловить любой вариант. Если ничего не совпадет в предыдущих ветках, _ поймает это. А так как у нас есть такое ответвление - у нас имеются варианты для всех возможных значений x, и поэтому наша программа скомпилируется.

Вдобавок, выражение match также деструктурирует перечисления. Помните этот код в разделе о перечислениях?

fn cmp(a: int, b: int) -> Ordering {
    if a < b { Less }
    else if a > b { Greater }
    else { Equal }
}

fn main() {
    let x = 5i;
    let y = 10i;

    let ordering = cmp(x, y);

    if ordering == Less {
        println!("less");
    } else if ordering == Greater {
        println!("greater");
    } else if ordering == Equal {
        println!("equal");
    }
}

Мы можем переписать это с match:

fn cmp(a: int, b: int) -> Ordering {
    if a < b { Less }
    else if a > b { Greater }
    else { Equal }
}

fn main() {
    let x = 5i;
    let y = 10i;

    match cmp(x, y) {
        Less    => println!("less"),
        Greater => println!("greater"),
        Equal   => println!("equal"),
    }
}

В этой версии меньше шума, а также исчерпывающе проверяет для того чтобы убедиться что мы рассмотрели все возможные варианты Ordering. К примеру, допустим, что в нашей if/else версии мы забыли вариант Greater, в таком случае, наша программа спокойно бы скомпилировалась. В случае с match такого не произойдет. Rust помогает нам удостовериться в том, что мы рассмотрели все основные.

Выражение match так же позволит нам получить значения содержащиеся в перечислении (деструктурировать) следующим образом:

enum OptionalInt {
    Value(int),
    Missing,
}

fn main() {
    let x = OptionalInt::Value(5);
    let y = OptionalInt::Missing;

    match x {
        OptionalInt::Value(n) => println!("x is {}", n),
        OptionalInt::Missing  => println!("x is missing!"),
    }

    match y {
        OptionalInt::Value(n) => println!("y is {}", n),
        OptionalInt::Missing  => println!("y is missing!"),
    }
}

Это то, как вы можете получить и использовать значения содержащиеся в перечислении. Это так же позволяет нам позволит нам обработать ошибки или неожиданные результаты вычислений, например, функция которая не гарантирует, что в состоянии вычислить результат (целое число в данном случае) может вернуть OptionalInt, и мы можем обработать это значение при помощи match. Как вы можете видеть, enum и match, используемые вместе, весьма полезны!

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

fn cmp(a: int, b: int) -> Ordering {
    if a < b { Less }
    else if a > b { Greater }
    else { Equal }
}

fn main() {
    let x = 5i;
    let y = 10i;

    println!("{}", match cmp(x, y) {
        Less    => "less",
        Greater => "greater",
        Equal   => "equal",
    });
}

Иногда, это неплохой вариант.