Математические функции rust

Обновлено: 07.07.2024

Предыдущую статью восприняли лучше, чем я ожидал, так что решился на продолжение эксперимента. Это своеобразный ответ на перевод статьи Programming in D for C Programmers за авторством Дмитрия aka vintage. Как мне кажется, в области применения C Rust более уместен, чем замена Go, как предполагалось в прошлой статье. Тем интереснее будет сравнить. Опять таки, код на С приводить не буду, тем более что аналог на D всё равно смотрится лаконичнее.

Получаем размер типа в байтах

Напомню, что в С (и в С++) для этой цели существует специальный оператор sizeof , который может применяться как к типам, так и к переменным. В D же размер доступен через свойство (которое тоже можно применять и к переменным):

В Rust используется функция, которая обращается к внутренностям компилятора (соответствующему intrinsic):

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

Забавный нюанс: в Rust пустые структуры (такие как Foo из примера) занимают 0 байт, соответственно массив любого размера таких структур тоже будет занимать 0 байт.
[Поиграться с кодом]

Получаем максимальное и минимальное значение типа

В D, опять-таки, используются свойства типов:

В Rust используются С-подобные константы:

Таблица соответствия типов

Сравнение не совсем правильное, так как в С используются платформозависимые типы, а в D наоборот — фиксированного размера. Для Rust подбирал именно аналоги фиксированного размера.

Особые значения чисел с плавающей точкой

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

Остаток от деления вещественных чисел

Тут никаких откровений — в Rust, как и в D, имеется оператор %.

Обработка NaN значений

И в D, и в Rust сравнение с NaN даст в результате false .

Асерты — полезный механизм выявления ошибок

Оба языка предоставляют асерты "из коробки", но в D они являются специальной языковой конструкцией:

A в Rust — просто макросами:

Впрочем, есть и интересное отличие: в D асерты в релизной сборке отключаются, кроме специального случая assert(0) , который используется для обозначения недостижимого при нормальном выполнении кода.

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

Итерирование по массиву (коллекции)

Особой разницы нет, хотя цикл for в Rust и не похож на своего родственника из С.

Инициализация элементов массива

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

В Rust присутствует специальный синтаксис для этого случая.

Создание массивов переменной длины

D имеет встроенную поддержку массивов переменной длины:

Rust, следуя своей "философии явности", требует задать значение, которым будут инициализированы новые элементы при вызове метода resize . Поэтому правильнее пример будет записать следующим образом:

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

Соединение строк

В D есть специальные перегружаемые операторы

=, предназначенные для соединения списков:

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

В Rust, с одной стороны, эти проблемы невозможны из-за необходимости явного приведения типов. С другой стороны, оператор += для строк всё-таки не реализован.

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

Как видим, языки в этом плане не особо различаются. Разве что в Rust форматирование не похоже на "привычное" из С.

Обращение к функциям до объявления

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

Функции без аргументов

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

Выход из нескольких блоков кода

Синтаксис break/continue с меткой практически идентичен.

Пространство имён структур

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

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

В данном случае особой разницы не видно, но в Rust конструкция match — это полноценное сравнение с образцом, что позволяет делать более хитрые вещи:

Выравнивание полей структур

В D есть специальный синтаксис, с помощью которого вы можете детально настроить выравнивание отдельных полей:

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

Анонимные структуры и объединения

D поддерживает анонимные структуры, что позволяет сохранять плоский внешний интерфейс для вложенных сущностей:

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

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

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

Определение структур и переменных

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

Получение смещения поля структуры

В D у полей есть специальное свойство offsetof :

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

Инициализация объединений

D требует явного указания на то, какому полю объединения присваивается значение:

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

Инициализация структур

В D структуры можно инициализировать как по порядку, так и с указанием имён полей:

В Rust указание имён обязательно:

Инициализация массивов

В D существует много способов инициализации массива, в том числе с указанием индексов инициализируемых элементов:

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

Экранирование спецсимволов в строках

Оба языка, наряду с экранированием отдельных символов, поддерживают так называемые "сырые строки":

ASCII против многобайтных кодировок

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

В Rust существует только один тип строк, которые представляют последовательность UTF-8 байт:

Константин aka kstep опубликовал на хабре серию переводов про строковые типы в Rust, так что если вас интересуют подробности, то рекомендую ознакомиться с ними. Ну или с официальной документацией (перевод).

Отображение перечисления на массив

Аналог на Rust с применением макроса collect! будет выглядеть следующим образом:

Создание новых типов

D позволяет создавать новые типы из имеющихся (strong typedef):

В том числе, с заданием дефолтного значения:

В Rust это делается через использование структуры-кортежа (tuple struct, перевод):

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

Сравнение структур
Сравнение строк

В обоих языках строки можно сравнивать на равенство и больше/меньше.

Сортировка массивов

D использует обобщённые реализации алгоритмов:

В Rust используется несколько другой подход: сортировка, как и некоторые другие алгоритмы, реализована для "срезов" (slice), а те контейнеры, для которых это имеет смысл, умеют к ним приводиться.

[Запустить]
Из мелких отличий: сравнение должно возвращать не bool , а Ordering (больше/меньше/равно).

Данное сравнение заставило задуматься, почему в Rust сделано не так как в D или С++. Навскидку не вижу преимуществ и недостатков обоих подходов, так что спишем просто на особенности языка.

Строковые литералы

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

Обход структур данных

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

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

Динамические замыкания

В Rust тоже имеются лябмды/делегаты/замыкания. Пример был выше по тексту, ну а если вам интересны подробности, то загляните в документацию (перевод).

Переменное число аргументов

В D есть специальная конструкция ". " позволяющая принять несколько параметров в качестве одного типизированного массива:

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

Заключение

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

Любое значение в Rust является определённым типом данных (data type), которое говорит о том, какие данные указаны, так что Rust знает как с ними работать. Рассмотрим два подмножества тип данных: скалярные (простые) и составные (сложные).

Не забывайте, что Rust является статически типизированным (statically typed) языком. Это означает, что он должен знать типы всех переменных во время компиляции. Обычно компилятор может вывести (infer) какой тип мы хотим использовать, основываясь на значении и на том, как мы с ним работаем. В случаях, когда может быть выведено несколько типов, необходимо вручную добавлять аннотацию типа. Например, когда мы конвертировали String в число с помощью вызова parse в разделе "Сравнение предположения с загаданным номером" Главы 2, мы должны добавить такую аннотацию:

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

В будущем вы увидите различные аннотации для разных типов данных.

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

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

Целые числа

Целое число, integer, является числом без дробной составляющей. Мы использовали целочисленный тип в Главе 2, это был u32 . Данное объявление типа указывает, что значение связанное с ним должно быть беззнаковым целым (unsigned integer). Типы знаковых целых (signed integer) начинаются с буквы i , вместо буквы u и занимают до 32 бит памяти. Таблица 3-1 показывает встроенные целые типы Rust. Каждый вариант в колонках Знаковый и Беззнаковый, к примеру i16 , может использоваться для объявления значения целочисленного типа.

Таблица 3-1: целые типы Rust

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

Каждый знаковый вариант может хранить числа от -(2 n - 1 ) до 2 n -1 - 1 включительно, где n является количеством используемых бит. Так i8 может хранить числа от -(2 7 ) до 2 7 - 1, что равно от -128 до 127. Беззнаковые варианты могут хранить числа от 0 до 2 n - 1, так u8 может хранить числа от 0 до 2 8 - 1, т.е. от 0 до 255.

Также есть типы isize и usize , размер которых зависит от компьютера, на котором работает ваша программа: они имеют размер 64 бит, если операционная система использует 64-битную архитектуру, и 32 бита, если 32-битную.

Вы можете записывать целочисленные литералы в любой форме из таблицы 3-2. Заметим, что все числовые литералы, кроме байтового, позволяют использовать суффиксы, такие как 57u8 и _ в качестве визуального разделителя, например 1_000 .

Таблица 3-2: целочисленные литералы в Rust

Числовые литералыПример
Десятичный 98_222
Шестнадцатеричный 0xff
Восьмеричный 0o77
Бинарный 0b1111_0000
Байтовый (только u8 ) b'A'

Как узнать, какой литерал необходимо использовать? Если вы не уверены, то вариант предоставляемый в Rust по умолчанию является хорошим выбором. Для целых чисел типом по умолчанию является i32 : в общем случае, данный тип самый быстрый даже на 64-битных системах. Основной ситуацией, когда вам необходимо использование isize или usize , является индексирование некоторых коллекций.

Целочисленное переполнение
  • Обернуть все режимы с помощью wrapping_* методов, например wrapping_add
  • Вернуть значение None в случае переполнения при помощи методов checked_*
  • Вернуть значение и логическое значение, указывающее, было ли переполнение с помощью методов overflowing_*
  • Подавить минимальные или максимальные значения с помощью методов saturating_*
Числа с плавающей запятой

В Rust есть два примитивных типа для чисел с плавающей точкой (floating-point numbers), которые являются числами с десятичными точками. Числа с плавающей точкой в Rust представлены типами f32 и f64 , имеющими размер 32 и 64 бита соответственно. Типом по умолчанию является f64 , потому что все современные CPU работают с ним приблизительно с такой же скоростью, как и с f32 , но с большей точностью.

Пример для чисел с плавающей точкой в действии:

Числа с плавающей точкой представлены согласно стандарту IEEE-754. Тип f32 является числом с плавающей точкой одинарной точности, а f64 имеет двойную точность.

Числовые операции

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

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

Логический тип данных

Как и в большинстве языков программирования, логический тип в Rust может иметь два значения: true и false , и занимает в памяти один байт. Логический тип в Rust аннотируется при помощи bool . Например:

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

Символьный тип данных

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

Тип char имеет размер в четыре байта и представляет собой скалярное юникод значение (Unicode Scalar Value), а значит, он может представить больше символов, чем есть в ASCII. Акцентированные буквы, китайские, японские и корейские символы, эмодзи и пробелы нулевой ширины - всё является корректными значениями char в Rust. Скалярное юникод значение имеет диапазон от U+0000 до U+D7FF и от U+E000 до U+10FFFF включительно. Тем не менее, "символ" на самом деле не является концептом в Юникод, так что человеческая интуиция о том, что такое "символ" может не совпадать с тем, чем является тип char в Rust. Более детально мы обсудим эту тему в разделе "Сохранение UTF-8 текста в строки" Главы 8.

Сложные типы данных

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

Кортежи

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

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

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

Программа создаёт кортеж, привязывает его к переменной tup . Затем в let используется шаблон для превращения tup в три отдельных переменные: x , y и z . Такого рода операция называется деструктуризацией (destructuring), потому что она разрушает один кортеж на три части. В конце программа печатает значение y , которое равно 6.4 .

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

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

Массивы

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

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

Массивы являются полезными, когда вы хотите разместить данные в стеке, вместо выделения памяти в куче (мы обсудим стек и кучу в Главе 4) или когда мы хотим быть уверенными, что у нас есть место под фиксированное количество элементов. Массив не такой гибкий, как вектор. Вектор является похожим типом для коллекций, предоставленным стандартной библиотекой, которому позволено увеличиваться или уменьшаться в размере. Если вы не уверены, использовать массив или вектор, то возможно следует воспользоваться вектором. В Главе 8 обсуждаются вектора более детально.

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

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

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

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

Массив в переменной a будет включать 5 элементов, значение которых будет равно 3 . Данная запись аналогична коду let a = [3, 3, 3, 3, 3]; , но является более краткой.

Доступ к элементам массива

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

В данном примере, переменная first получит значение равное 1 , потому что это значение доступно по индексу [0] из массива. Переменная с именем second получит значение равное 2 из массива по индексу [1] .

Некорректный доступ к элементу массива

Что произойдёт, если вы попытаетесь получить доступ к элементу массива, который находится за пределами массива? Допустим, вы изменили пример на следующий, в котором используется код, аналогичный игре в угадывание в главе 2, для получения индекса массива от пользователя:

Этот код успешно компилируется. Если запустить этот код с помощью cargo run и ввести 0, 1, 2, 3 или 4, то программа распечатает соответствующее значение по указанному индексу из массива. Если вы введёте число за границей массива, например 10, то вы увидите следующий результат:


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

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

В этой с татье я представлю Rust с точки зрения разработчика JavaScript, одновременно сравнивая эти два языка. Надеюсь, что после прочтения статьи вам тоже захочется освоить Rust!

Язык программирования Rust был создан компанией Mozilla, а его первая стабильная версия появилась на свет примерно в 2015 году. Hello, World на Rust выглядит так:

И совсем вроде не страшно. Можно даже сказать, что выглядит она почти как JavaScript, но это всего лишь программа hello world, а сама версия немного сложнее! Прежде чем переходить к функциональным средствам языка, давайте определим, какое место занимает Rust в окружении других языков программирования:


Между языками программирования существует чёткое разграничение:

  • Низкоуровневые: это такие языки, как C++. Они позволяют иметь доступ к управлению памятью, считаются низкоуровневыми, и они очень быстрые. Но и безопасность их тоже на низком уровне, потому что, имея доступ к памяти, очень легко можно понаделать ошибок, и такое случается!
  • Высокоуровневые: с другой стороны, существуют такие языки, как JavaScript. Они не позволяют иметь такой детализированный доступ к памяти (здесь есть сборщик мусора, который делает всё за нас) и считаются безопасными языками, поэтому иногда они могут быть медленными.

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

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

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

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

Ну почти как эта собачка! Если вы постоянно воюете с компилятором Rust, не переживайте: мы все через это проходим.

Неизменяемость

В функциональных языках приходится много работать с неизменяемыми структурами. JavaScript программистов никто не заставляет работать с неизменяемостью, но популярные библиотеки, такие как Redux и Immutable.js, научили нас это делать. Сегодня есть ключевые слова let и const для объявления изменяемых и неизменяемых переменных соответственно.

На Rust для объявления переменных мы будем использовать лишь только let , причём переменные эти будут неизменяемы по умолчанию. Если мы захотим использовать изменяемые данные, нам придётся добавить ключевое слово mut в объявление:

Владение

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

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

В Rust нет значений null и undefined , поэтому мы не можем описать некую переменную, которая не имеет значения. В предыдущем примере, когда мы присвоили a значение x , мы переместили значение от x к a , то есть теперь x не имеет валидного значения. То же самое происходит с функциями:

Когда мы вызываем метод do_something , то перемещаем значение из x в s , то есть в аргумент, полученный функцией. После выполнения функции мы возвращаемся к main , а x больше не имеет валидного значения.

Такое поведение не всегда желательно, но в Rust мы можем заимствовать, ведь здесь существует понятие заимствования! Если вам не хочется перемещать значение от одной переменной к другой, воспользуйтесь ссылкой:

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

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

Структуры

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

Структура struct Dog очень похожа на объект JavaScript, но она отличается от него. Структура — это форма каких-то данных, которые будут иметь два именованных поля: name и score . Ниже структуры struct располагается блок реализации (сокращённо impl ). Вот так мы можем объявлять методы, которые будут работать с данными. И заметьте: если понадобится связать функцию с этими данными, нам нужно будет передать self в качестве первого аргумента. Напоминает Python, не находите?

Опуская значение self , мы объявляем метод, который не связан с какими-то конкретными данными. Можно провести аналогию со статическим методом в классе JavaScript.

Затем вам понадобятся библиотеки — не начинать же всё совсем с нуля. Поэтому точно так же, как мы обзаводимся пакетами Node на JavaScript, мы будем поступать и с пакетами Rust. Зайдите на crates.io, официальное хранилище крейтов, чтобы подробнее разузнать о пакетах на Rust.

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

    : несмотря на то, что платформы не достигли пока такой зрелости, как Ruby on Rails, кое-что сделать с их помощью вы бы могли! Рекомендую обратить внимание на платформу Rocket, если вы желаете заняться веб-разработкой. Вы даже можете создавать GraphQL API с помощью Juniper! : полностью освоив управление памятью, можно переходить к созданию игр, Rust отлично для этого подходит! Если вас манит разработка игр, рекомендую игровой движок Amethyst. : машинное обучение. Это ещё одна тема, которая сейчас очень популярна. Экосистема Rust пока ещё не укомплектована полностью и прямо сейчас не может на равных конкурировать с Python в том, что касается машинного обучения, но если вам интересна эта тема, зайдите на сайт!

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

Если хотите её опробовать, рекомендую почитать официальную книгу с документацией по Rust и WebAssembly.

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

Если вы хотите приступить к освоению Rust, рекомендую начать с этого официального ресурса и попробовать написать имеющиеся программы на JavaScript с помощью Rust. Как и во многом другом, практика — это ключ к успеху!

В заключение отметим, что эта статья написана по мотивам доклада, представленного автором на семинаре разработчиков JS Coders meetup event. Со слайдами вы можете ознакомиться здесь.


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

В 2020 году по итогам опроса разработчиков Stack Overflow самым любимым языком программирования уже пятый год подряд был признан Rust. Многие разработчики уверены в том, что Rust скоро обгонит C и C++ благодаря своему средству проверки заимствований и решению давних проблем, таких как управление памятью, а также неявная и явная типизация.

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

Вот что мы рассмотрим в статье.

  • Что такое Rust?
  • «Hello World!» на Rust.
  • Основы синтаксиса Rust.
  • Промежуточный Rust: владение и структуры.
  • Система сборки Rust: Cargo.
  • Продвинутые концепции для дальнейшего изучения.

Rust — это мультипарадигмальный статически типизированный язык программирования с открытым исходным кодом, используемый для создания операционных систем, компиляторов и других программно-аппаратных средств. Он был разработан Грейдоном Хором в Mozilla Research в 2010 году.

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

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

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

Зачем изучать Rust?

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

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

Лучший способ освоить Rust — реальная практика. Для начала напишем первую программу hello-world .

Разберем все части этого кода.

fn

fn — это сокращение от function («Функция»). В Rust (как и в большинстве других языков программирования) функция как бы говорит: «Сообщите мне информацию, а я сделаю то-то и то-то и затем дам ответ».

main

Функция main — это то место, где начинается программа.

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

Фигурные скобки. Ими обозначается начало и конец тела кода. Тело сообщает, что делает функция main .

println!

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

("Hello, world!")

А это список параметров для вызова макроса. Мы как бы говорим: «Вызовите макрос println с этими параметрами». Макрос println такой же, как функция main , только у него параметр вместо списка параметров. Позже мы еще увидим функции и параметры.

"Hello, world!"

Дальше идет строка. Строки состоят из нескольких собранных вместе букв или символов. Для обозначения строки эти символы помещаются в кавычки ( " ). Затем строки передаются для макросов типа println! и других функций, с которыми мы еще поиграем.

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

Теперь рассмотрим основные части программы на Rust и способы их реализации.

Переменные и их изменяемость

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

Имя переменной должно быть информативным, т. е. описывать, чем является ее значение. Например:

Здесь создана переменная my_name со значением "Ryan" .

Совет💡 Всегда давайте переменным названия, начинающиеся со строчной буквы, а новое слово начинайте с заглавной.

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

Например, вот этот код выдаст ошибку во время компиляции:

Ошибка в строке 4, где мы попытались установить значение x = 6 . Но значение x уже задано в строке 2 и изменить его нельзя.

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

Представьте, что у вас есть две функции: functionA , которая использует переменную, имеющую значение 10 , и функция functionB , которая изменяет эту же переменную. Выполнение функции functionA будет прервано!

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

Чтобы переопределить это значение по умолчанию и создать изменяемую переменную, объявим ее вот так:

Изменяемые переменные чаще всего используются как переменные-итераторы или как переменные в структурах цикла while .

Типы данных

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

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

Указав между именем переменной и ее значением : &[type] , мы явно определим тип для этой переменной.

В этом случае наш пример с объявлением my_name будет переписан следующим образом:

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

Допустим, имеется переменная answer , которая записывает ответ пользователя в форму.

Rust неявно определит строковый тип для этой переменной, так как она приводится в кавычках. Тогда как переменная наверняка булева, что подразумевает выбор между двумя вариантами: true и false .

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

Основные типы на Rust:

  • Integer: целочисленный (целые числа).
  • Float: числа с плавающей запятой (с десятичными знаками).
  • Boolean: логический (булев) с выбором между двумя вариантами ( true или false ).
  • String: строковый (набор символов, заключенных в кавычки).
  • Char: скалярное значение Юникод, представляющее конкретный символ.
  • Never: тип без значения (обозначается как ! ).

Функции

Функции — это наборы связанного кода на Rust, объединенные под кратким условным обозначением и вызываемые из других частей программы.

Пока что мы использовали только базовую функцию main() . Rust также позволяет создавать дополнительные, собственные функции, и это очень важная для большинства программ возможность. Функции часто представляют собой одну повторяющуюся задачу, например addUser (добавление пользователя) или changeUsername (изменение имени пользователя). Эти функции затем повторно используются всякий раз, когда требуется выполнить то же самое поведение.

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

Вот формат для объявления функции:

fn

Это уже знакомое нам сокращение от function («Функция»). За ним в коде Rust следует объявление функции.

[functionName]

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

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

[parameterIdentifier]

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

[parameterType]

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

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

[functionBody]

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

Добавим немного кода. Переделаем hello-world в функцию say_hello() :

Совет💡 Увидели () — значит, вы имеете дело с вызовом функции. Если параметров нет, получаем внутри скобок пустое поле параметров. Сами скобки все равно остаются, указывая на то, что это функция.

Функция создана, теперь вызовем ее из других частей программы. Программа начинается в main() , поэтому вызовем say_hello() оттуда.

Вот как будет выглядеть полная программа:

Комментарии

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

Второй способ — предварять комментарий косой чертой со звездочкой /* и завершать его звездочкой с косой чертой */ . Преимущества такого способа оформления комментариев: 1) есть возможность помещать комментарии в середину строки кода и 2) так легче писать многострочные комментарии. Недостаток в том, что во многих случаях приходится задействовать больше символов, чем просто // .

Совет💡 используйте комментарии для «закомментирования» разделов кода, выполнение которых не требуется, но которые позже понадобится добавить.

Условные инструкции

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

Все условные инструкции содержат проверяемую переменную и целевое значение, а оператор условия ( == , < или > ) определяет их соотношение. В зависимости от состояния переменной применительно к целевому значению возвращается одно из двух логических выражений: true («истинно»), если переменная удовлетворяет целевому значению, и false («ложно»), если нет.

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

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

Вот как выглядит формат оператора if :

Есть три основных условных оператора: if , if else и while .

  • if : если условие истинно, происходит выполнение. В противном случае пропускаем и идем дальше.
  • if else : если условие истинно, выполняется тело кода A. В противном случае выполняется тело кода B.
  • while : тело кода многократно выполняется, пока условие true («истинно»). Как только условие становится false («ложным»), мы идем дальше.

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

Владение

Владение — это центральная особенность Rust и одна из причин такой его популярности.

Во всех языках программирования должна предусматриваться система освобождения неиспользуемой памяти. В некоторых языках, таких как Java, JavaScript или Python, есть сборщики мусора, которые автоматически удаляют неиспользуемые ссылки. В низкоуровневых языках типа C или C++ от разработчиков требуется всякий раз, когда это необходимо, выделять и освобождать память вручную.

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

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

Вот эти правила владения.

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

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

Главный вывод касается разного использования s и x . Сначала x владеет значением 5 , но после выхода ее из области видимости функции main() переменная x должна передать владение параметру number . Ее использование в качестве параметра позволяет продолжить область видимости выделения памяти под значение 5 за пределы исходной функции.

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

Структуры

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

Аналогом этих структур в таких языках, как Java и Python, являются классы.

Вот синтаксис объявления структуры:

  • struct сообщает Rust, что следующее объявление определит тип данных struct.
  • [identifier] — это имя типа данных, используемого при передаче параметров, таких как string или i32 , в строковые и целочисленные типы соответственно.
  • <> эти фигурные скобки обозначают начало и конец переменных, необходимых для структуры.
  • [fieldName] — это место, где вы называете первую переменную, которую должны иметь все экземпляры этой структуры. Переменные внутри структуры называются полями.
  • [fieldType] — это место, где во избежание путаницы явно определяется тип данных переменной.

Например, создадим структуру struct Car , которая включает в себя переменную строкового типа brand и переменную целочисленного типа year .

Каждый создаваемый экземпляр типа Car должен иметь значения для этих полей. Поэтому создадим экземпляр Car для конкретного автомобиля со значениями для brand (модели) и year (года выпуска).

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

Оттуда будем использовать значения этих полей с синтаксисом [variableIdentifier].[field] . Rust интерпретирует эту инструкцию как «каково значение [поля] для идентификатора [переменной]?».

Вот как выглядит вся структура целиком:

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

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

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

Если скачать Rust с официального сайта, Cargo автоматически устанавливается вместе с компилятором ( rustc ) и генератором документации ( rustdoc ) как часть набора инструментальных средств Rust. Убедиться, что Cargo установлен, помогает ввод в командной строке следующей команды:

Для создания проекта с Cargo запустите в интерфейсе командной строки операционной системы следующее:

Первой командой создается новый каталог hello_cargo . А второй командой этот новый каталог выбирается.

Генерируется манифест под названием Cargo.toml , который содержит все метаданные, необходимые Cargo для компиляции пакета, а также файл main.rs , отвечающий за компиляцию проекта.

Чтобы все это увидеть, наберите:

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

Все зависимости приведены в категории dependencies .

После завершения проекта введите команду $ cargo run : проект будет скомпилирован и запущен.

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

Читайте также: