Найти тему
Ржавый код

Начало работы с Rust - Упрощенное практическое руководство

Оглавление

Это руководство даст нам обзор Rust:

  • Основные конструкции
  • Владение
  • Соответствие образцу
  • Обработка ошибок

Как установить Rust

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

Начало работы с Rust

Теперь мы, наконец, собираемся написать какой-нибудь код (это то, для чего мы здесь, верно?).

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

Давайте закатаем рукава и возьмемся за дело.

Hello World

Я отказываюсь использовать классический Hello World, поэтому здесь мы будем использовать Hello Rust (считайте, что это Hello World 1.1 или что-то в этом роде 🙂.

Во-первых, нам нужно создать новый проект rust. Для этой задачи мы можем использовать cargo, менеджер пакетов Rust. `cargo new` - это команда cargo, которая создает для нас новый проект.

Продолжайте и откройте терминал, затем введите следующее:

-2

Cargo создаст новую папку `hello_rust` с исходным кодом нового проекта. Если мы проверим проект, то должны увидеть что-то вроде этого:

-3

Cargo.toml - это файл, в котором cargo, помимо прочего, определяет пакет проекта и все его зависимости.

На данный момент мы заинтересованы в `main.rs` . Продолжайте и откройте этот файл в NeoVim или вашем любимом редакторе.

Как вы можете видеть, программа "Hello World" уже существует.

-4

Первое, что мы видим в этом файле, - это `fn main`. Rust определяет функцию с помощью ключевого слова `fn`. Основная функция в программе Rust называется `main`. Она не принимает никаких параметров.

Следующая интересная строка - `println!("Привет, Rust!");`, макрос Rust для вывода строки в стандартный вывод. Для простоты давайте предположим, что `println!` принимает один строковый параметр. Обратите внимание, что макрос должен вызываться с помощью `!` (восклицательный знак), чтобы развернуть макрос. Это пока не имеет смысла, мы подробно рассмотрим макросы когда нибудь.

А пока просто исправьте программу, заменив world на Rust.

-5

В терминале введите следующее, чтобы запустить нашу улучшенную версию программы:

-6

Если все прошло хорошо, мы должны увидеть "Hello, Rust!" сообщение.

Достижение разблокировано! Вы написали свою первую программу на Rust.

Основные конструкции Rust

Наш Hello Rust - хорошее начало, но нам нужно что-то более существенное, если мы хотим освоить Rust.

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

Сказав это, давайте рассмотрим некоторые основные конструкции в Rust.

Переменные

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

Давайте возьмем нашу программу Hello Rust и извлекем строку из строки 2 в переменную:

-7

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

Rust по умолчанию предпочитает неизменяемость. Повторное присвоение переменной приведет к ошибке. Просто для развлечения и исследования давайте заменим нашу переменную `message` на следующую:

-8

После некоторых предупреждений мы должны увидеть сообщение об ошибке:

-9

Что же Rust говорит нам, так это то, что сообщение является неизменяемым, поэтому мы не можем повторно назначить его. Способ устранить эту проблему - объявить переменную как изменяемую. Мы можем сделать это в Rust, добавив ключевое слово `mut` перед именем переменной:

-10

Приведенный выше код компилируется, что означает, что теперь мы можем повторно назначить `message` новой строке.

Еще одна вещь, на которую следует обратить внимание, - это соглашение об именовании. Переменные в Rust используют соглашение `snake_case` (змеиная нотация). Например: пусть `my_message = "Хранится в переменной loooong";`.

Одна интересная вещь, которую следует упомянуть, заключается в том, что Rust может определить тип переменной, которую мы только что объявили. `let message = "Hello, Rust!";` достаточно, чтобы Rust понял, что тип этой переменной должен быть `&str`.

ПРИМЕЧАНИЕ: Если вам интересно, что означает `&str`, это ссылка (&) на примитивный тип `str` (строка, для упрощения). Причина, по которой это должно быть ссылкой, выходит за рамки этой вводной статьи, но в конце концов мы до нее доберемся.

Если мы хотим, мы можем указать тип, добавив `&str` после имени переменной: `let message: &str = "Hello, Rust";`.

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

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

-11

Приведенный выше фрагмент выведет "Hey, I got a new value!". Второе объявление сообщения затеняет первое. Обратите внимание на ключевое слово `let` в строке 2.

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

Константы

Rust предоставляет более простой способ хранения неизменяемых данных, называемых константами. Они похожи на неизменяемые переменные, но являются константами:

Объявляются с помощью `const` вместо `let`. Например: `const PI: f32 = 3,14159;`

  • Требуется указать тип.
  • Должны быть вычислены во время компиляции, поэтому они не могут хранить значения, вычисленные во время выполнения.
  • Может быть объявлена в глобальной области видимости (я бы не сказал, что это лучшая практика, но вы можете это сделать).
  • Не может быть мутирован, даже с помощью mut.

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

-12

Типы данных

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

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

  • Переменные - Как мы уже видели, Rust может выводить числовые, логические и строковые типы.
  • Универсальные типы - Выводится на основе типа значений, передаваемых в универсальные типы. Пусть массив: `Vec<_> = (0..10).collect();` означает, что массив будет иметь некоторый определенный тип Vec, который будет выведен из правой части присваивания.
  • Выражения - Возвращаемое значение инструкций потока управления. Например, `let result: bool = if n == 3 { true } else { false };` может быть записан как `let result = if n == 3 { true } else { false };`.

ПРИМЕЧАНИЕ: Помните, что `Vec<i32>` и `Vec<f64>` - это два совершенно разных типа в Rust. Напомним, что Rust не может определять постоянные типы.

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

Конструкции потока управления Rust - это то, чего можно было бы ожидать, если бы вы исходили из такого языка, как C++. Однако есть некоторые различия.

Несколько моментов, на которые следует обратить внимание в операторах if-else и циклах:

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

Операторы If-else

Типичный оператор `if-else`, присутствующий во многих других языках программирования.

-13

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

Loops

Базовые циклы в Rust напоминают циклы в C++:

-14

Как и следовало ожидать, в приведенном выше примере выводится значение `index` от 1 до 10. Переменная должна быть объявлена как изменяемая, поскольку она увеличивается внутри блока `while`.

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

-15

строка 4 - это то место, где происходит волшебство:

  1. pop возвращает результат.
  2. Цикл продолжает выполняться до тех пор, пока pop не вернет None, что означает, что никаких элементов не осталось.
  3. Некоторые из них будут указывать значение элемента.

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

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

-16

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

Вы даже можете создавать свои собственные итераторы. Мы оставим подробное объяснение итераторов на другой день.

Функции

Функции в Rust аналогичны функциям в таких языках, как Scala или TypeScript.

Вот пример:

-17

Параметры указывают тип после имени переменной (строка 1).

Должны быть указаны возвращаемые типы, если только функция возвращает значение (строка 1).

Ключевое слово `return` можно опустить, и тогда не нужно ставить точку с запятой в конце, чтобы вернуть результат функции (строка 2).

Чтобы вызвать функцию, мы передаем ее требуемые параметры в круглых скобках (строка 6).

Комментарии

Комментариям в Rust предшествует `//`, как в Java, C++ или JavaScript. Здесь нет ничего удивительного.

-18

Владение

ПРЕДУПРЕЖДЕНИЕ: Я не смог найти способ объяснить право собственности без более длинного текстового введения. Заранее приношу свои извинения!

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

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

  • Сохранность в памяти
  • Свобода от условий гонки данных

О, я забыл упомянуть: во время компиляции. Да, Rust не будет компилироваться, если вы введете утечку памяти или состояние гонки данных. Это меняет правила игры. Попрощайтесь с двумя худшими кошмарами программиста. Что ж, если вы не используете unsafe, то можете продолжать веселиться ;) Оставив в стороне фейерверки и конфетти, я попытаюсь объяснить право собственности простым способом, который даст нам общее, поверхностное представление.

У каждой переменной есть время жизни. Например:

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

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

Концепция владения (кому принадлежит переменная) и заимствования (кто просит владельца одолжить переменную на некоторое время).:

  • Право собственности может быть передано от одной переменной к другой.
  • Заимствование - это другой способ использования переменной без получения фактического права собственности.

Давайте попробуем привести пример:

-19

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

Теперь давайте создадим строку и попробуем сделать то же самое:

-20

Компилятор жалуется в строке 5:

-21

Здесь много новой информации.

Rust говорит о том, что значение переместили. Типы, которые не являются примитивными типами, по умолчанию не имеют возможности копирования. Таким образом, Rust меняет владельца переменной. В строке 3 `second` является владельцем строки, объявленной в строке 2.

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

Чтобы исправить вышеприведенную программу, мы можем сделать следующее:

-22

Обратите внимание на & в строке 3. second не получает права собственности на значение, принадлежащее first, но вместо этого просит позаимствовать ссылку. first по-прежнему является владельцем строки, поэтому ее можно распечатать в строке 5. Наконец, second можно безопасно использовать в строке 6, поскольку он содержит ссылку на строку.

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

Соответствие образцу

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

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

Мы можем использовать самое простое сопоставление с шаблоном в операторах if:

-23

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

ПРИМЕЧАНИЕ: Тип параметра в Rust может быть либо `Some`, либо `None`. Это отличный способ избежать необходимости в `Null`.

Приведенный выше пример не является исчерпывающим сопоставлением с шаблоном. На самом деле, ничто не мешает нам удалить оператор `else`, фактически не обрабатывая ветвь `None` `case`. Но Rust также может справиться с исчерпывающим сопоставлением шаблонов.

-24

Этот пример является исчерпывающим сопоставлением с шаблоном. Если мы попытаемся удалить ветку `None`, мы получим сообщение об ошибке, указывающее нам включить `None`:

-25
-26

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

Обработка ошибок

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

  • Левый (или первый) вариант (думайте о варианте как о типе, чтобы было легче понять) в порядке (это означает, что все прошло хорошо, вот значение результата).
  • Правильный (или второй) вариант - Err (означает, что что-то пошло не так, вот ошибка).

ПРИМЕЧАНИЕ: Оба варианта потенциально могут представлять другие вещи, но результат ограничивает их вариантом типа <Happy, Error>.

Давайте посмотрим на пример:

-27

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

ПРИМЕЧАНИЕ: Под капотом результатом является перечисление, которое может быть либо Ok, либо Err, каждый из которых принимает вариант для указания нужного нам типа.

Мы также можем развернуть значение или результат без явной обработки ошибки. Иногда это удобно, когда ошибки не могут быть восстановлены, и мы просто хотим написать наш поток немного более кратко. Например, вызов `parse::<i32>` для строки вернет результат `<i32, ParseIntError>`. Мы можем попробовать вызвать `unwrap`, чтобы получить значение внутри `Ok`, если мы уверены или если ошибка в любом случае неустранима.

-28

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

ПРИМЕЧАНИЕ: Обычно я бы правильно обработал приведенную выше ошибку, поскольку это, вероятно, не мешает нам продолжать нашу логику (это зависит от контекста). Но `parse` - это довольно простой пример, поэтому он выполняет свою работу в целях иллюстрации. Ошибки, такие как невозможность инициализировать окно в приложении с графическим интерфейсом, являются более проблематичными и могут оправдать панику Rust.

Мы также можем использовать трейт ошибки, когда хотим обрабатывать пользовательские ошибки.

У Rust есть panic! макрос, который является способом явного прерывания программы при неустранимых обстоятельствах,

-29

выдает нам приятную ошибку завершения:

-30

Вывод

Rust - это смесь императивного и функционального программирования. На самом деле, Rust ближе к языкам ML, чем к таким языкам, как C++ или Java. Это делает Rust нетривиальным языком программирования с концепциями, которые не обязательно просты для понимания.

Компилятор Rust - это одновременно и наш друг, и наш учитель. Как хороший друг, он не позволит нам застрелиться. Но, как хороший учитель, он будет неумолим. Поверь мне, это к лучшему.

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

Статья на list-site.