Как я влюбился в Rust и чего мне это стоило

Kate

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


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


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

Какой язык я искал​


Лично я в первую очередь делю все ЯП на две большие группы: интерпретируемые и компилируемые. Для личных проектов (разумеется, крупнее скриптов автоматизации) я искал именно второй, так как ключевой для меня была возможность переносить исполняемые файлы на внешних и облачных дисках и запускать их на офисных ПК без каких-либо проблем.
Важным условием при выборе также была возможность без трудностей скомпилировать исполняемые файлы под Windows, Mac OS и дистрибутивы Linux, так как рабочих машин у меня несколько, а запускаться и работать код должен на каждой. Некоторые из проектов шли даже под Raspberry Pi, где мне вдобавок требовалось бережное отношение к памяти. Ну и напоследок я искал простоту в использовании (не в написании кода): чтобы библиотеки ставились (и писались) самым очевидным и удобным образом, чтобы структура проектной директории была простой и понятной, а общение с компилятором – приятным и безболезненным. За ковидный карантин я успел перепробовать множество разных языков, остановившись в итоге на Расте. Давайте узнаем, почему.


Путь к "Hello World"​


Так как, пожалуй, большинство читателей ранее с этим языком не взаимодействовали, я начну с самого начала: процесса первого знакомства. В процессе поиска своего идеального ЯП, очень часто я сталкивался с трудностями уже на этом этапе. Где-то были определенные сложности в выборе и настройке IDE, где-то установка или использование компилятора требовало множества разных манипуляций, которые сходу отпугивали и отбивали желание работать. Давайте взглянем, что предстоит пройти человеку, решившему с нуля написать на Расте простейший "Hello World".
Для начала загрузим rustup – программу, которая установит и будет поддерживать в актуальном состоянии все необходимое для написания программ. На Unix-подобных ОС сделать это можно одной командой:


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

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


После установки утилиты проверять обновления системы можно командой rustup update. В остальном для работы сама утилита нам больше не понадобится, ведь все остальные манипуляции мы будем проводить с системой сборки и пакетным менеджером системы – cargo.


Для начала проверим версию и убедимся, что все встало как надо, выполнив команду cargo --version.
Порядок? Идём дальше.
Сделаем cd в папку, где храним все проекты и попросим cargo создать новый командой cargo new hello-rust.
В папке будет создана новая директория со всеми необходимыми файлами:


hello-rust
|- Cargo.toml
|- src
|- main.rs

Cargo.toml здесь – файл манифеста, в котором хранятся все метаданные проекта. Подробнее о нем – чуть позже.
src/main.rs – не трудно догадаться, файл с исходным кодом нашего проекта. Сразу после создания проекта в нем появляется код, выводящий в терминал Hello, world!.
Можно, даже не открывая его, скомандовать cargo run и получить желаемое.


$ cargo run
Compiling hello-rust v0.1.0 (/Users/ag_dubs/rust/hello-rust)
Finished dev [unoptimized + debuginfo] target(s) in 1.34s
Running `target/debug/hello-rust`
Hello, world!

Вот и все. Для меня впервые путь к "Hello World" оказался невероятно дружелюбным и простым.
Но, разумеется, выводом текста в консоль никто ограничиваться не будет. Следующий шаг – учиться, учиться, и еще раз учиться.


Взглянуть целиком на официальный Quick Start Guide можно здесь


Приключения на пути к познанию​


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


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


Вот лишь малая часть информации, доступная на официальном сайте:


  • "Книга" Rust — полное руководство языка, изучив которое с нуля, можно добиться вполне уверенного понимания базовых и продвинутых элементов
  • Rust by Example – собрание множества примеров практического применения языка для решения разных задач с комментариями и упражнениями
  • Rustlings – консольная программа, помогающая первопроходцам освоиться с синтаксисом и основными понятиями Rust
  • Reference и Rustonomicon – справочные материалы для продвинутых пользователей, желающих отточить своё мастерство и познать самые тёмные уголки программирования на Расте
  • Embedded Book – руководство по использованию языка на микроконтроллерах и другом чистом железе
  • Rustdoc – справка по документированию проектов и библиотек
  • Cargo Book – материалы для работы с системой сборки проектов

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


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


Синтаксис и возможности​


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


База​


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


Ставить их везде, кстати, вовсе не обязательно:


if my_string.chars().count() == 3 {
println!("В строке три знака"); //Здесь точка с запятой нужна в любом случае
std::process::exit(-1) //А тут ее можно опустить, компилятору достаточно закрывающейся скобки
}

Во вторых – функции. Выглядят они в Расте так:


// Объявление функций всегда производится ключевым словом 'fn'. Тип возвращаемого значения (при наличии), указывается с помощью 'стрелочки'
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
// На ноль делить нельзя
if rhs == 0 {
return false
}

// Это выражение, результат которого – bool. Ключевое слово 'return' здесь необязательно, так как этот результат не перехватывается до выхода из функции
lhs % rhs == 0
}

Лично я – ярый сторонник именно такого вида записи, встречающегося и в других языках. Решение, принятое, например, в C++ или C# (с указанием типа возвращаемого значения вместо ключевого слова fn), на мой взгляд, куда менее очевидно, особенно если приходится иметь дело со сложными типами.


Далее вкратце перечислю мои самые любимые сахара:


Удобоваримый вид импорта модулей​


Импорт библиотек реализован здесь максимально кратким и эффективным образом, без лишних ключевых слов и с удобным наследованием:


use std::fs импортирует только модуль fs из std,


use std:🇮🇴:{Write, Read} возьмет структуры Write и Read из предыдущего,


use std::{io, fs::File, time::*} импортирует модуль io из std, структуру File модуля fs из std и все вложенные в модуль time из std модули и структуры. Одной строкой.


В крупных проектах с десятками зависимостей в одном файле такие возможности – просто спасение.


Атрибутные макросы​


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


Так, например, всего в одну строку можно задать включение определенного модуля лишь в сборку под MacOS: #[cfg(target_os = "macos")]. Идентичный этому синтаксис у всех подобных макросов: #[derive(PartialEq, Eq)], #[post("/user", data = "<new_user>")], #[test] и так далее. Лично мною такое решение воспринимается куда охотнее, чем аналогичные решения в тех же крестах.


Match​


Match в Расте – продвинутая версия знакомого многим switch/case. Давайте взглянем, на что он способен:


let my_age = 13;
match my_age {
// Проверка точного значения
1 => println!("Вам годик"),
// Проверка нескольких значений
2 | 3 | 5 | 7 | 11 => println!("Ваш возраст – простое число"),
// Проверка интервала (включительно)
13..=19 => println!("Вы подросток"),
// Проверка интервала (включительно) с выводом значения
n @ 80..=100 => println!("Вы старый дед {}и лет", n),
// Проверка вообще на что угодно с помощью if
i if i % 2 > 3 => println!("Остаток от деления вашего возраста на два больше трех"),
// Работа с остальными случаями
_ => println!("Вы кто")
}

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


Пара слов об обработке ошибок​


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


Error handling – это очень важно. Когда я пишу проект, что должен как можно дольше оставаться в поднятом состоянии и восстанавливаться от любых возможных ошибок, я хочу быть уверенным, что обработал 100% их всех. В этом мне помогает, на мой взгляд, одна из самых важных особенностей языка, ведь я всегда знаю, в каком месте может возникнуть ошибка.


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


// Объявим функцию, параметром которой будет опциональный тип `Option`, возвращающий (при наличии) `&str`.
fn give_guest(gift: Option<&str>) {
// 'Option' оборачивается в два значения: 'Some' и 'None'. Первый означает, что некий объект содержит в себе необходимые данные, второй, соответсвенно, что в нем ничего нет
// Проверим, что содержится внутри переменной 'gift', с помощью уже знакомого нам оператора 'match'
match gift {
Some("торт") => println!("Спасибо за торт"),
Some(inner) => println!("{}? Как мило.", inner),
None => println!("Нет подарка? Ну что ж."),
}
}

give_guest(Some("Наручные часы")); // Напечатает в консоль 'Наручные часы? Как мило.'
give_guest(None) // Напечатает 'Нет подарка? Ну что ж.'

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


fn next_birthday(current_age: Option<u8>) -> Option<String> {
// Если 'current_age' – 'None', данная функция вернёт 'None'
// Если 'current_age' – 'Some', внутренняя 'u8' получает значение 'next_age'
let next_age: u8 = current_age?;
Some(format!("В следующем году мне исполнится {}", next_age))
}

next_birthday(Some(8)); // Вернёт 'Some("В следующем году мне исполнится 8")'
next_birthday(None); // Вернёт 'None'

Похожее решение реализовано, например, в языке Swift, где вместо Some напрямую передаётся значение, а заменой None служит кейворд nil.


Option возвращается функциями, выполнение которых не всегда означает получение результата. А так как Раст не даст мне незаметно взять возвращённый функцией результат, заставив меня либо обработать и Some, и None, либо развернуть результат с помощью unwrap(), что вызовет невосстанавливаемое исключение (панику, подробнее о ней чуть позже), я гарантированно получаю уверенность в отсутствии неожиданных "вылетов" своей программы из-за отсутствия чего-либо, что должно быть, и чего нет.


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


// Например, мы пытаемся распарсить строку в число. Результатом встроенной функции 'parse' может быть либо тип 'Ok', содержащий числовой результат, либо 'Err', содержащий информацию об ошибке
fn print_num(string_number: &str) {
match string_number.parse::<i32>() {
Ok(number) => println!("Ваше число – {}", number),
Err(e) => println!("Ошибка: {}", e)
}
}

print_num("8") // Вернёт 'Ваше число – 8'
print_num("n") // Вернёт 'Ошибка: invalid digit found in string'

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


string_number.parse::<i32>().unwrap(); // В случае, если 'string_number' не является числом или превышает указанную разрядность, будет вызвана паника и программа прекратит выполнение:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }'

Панику можно вызвать самостоятельно:


fn drink(beverage: &str) {
// Представим, что у нас паническая боязнь лимонада
if beverage == "лимонад" { panic!("AAAaaaaa!!!!") }

println!("Ура, {}, я как раз хотел пить", beverage);
}

fn main() {
drink("вода"); // Напечатает 'Ура, вода, я как раз хотел пить'
drink("лимонад") // Паника: thread 'main' panicked at 'AAAaaaaa!!!!'
}

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


Возвращаясь к обсуждению синтаксиса: вместо while true Rust поддерживает одно ключевое слово loop, что, почему-то, особенно меня умиляет.


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


Последнее слово о cargo​


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


  • С помощью cargo new мы создали новый проект
  • cargo build или cargo run собирает и запускает наш код соответственно
  • cargo publish публикует проект на официальном регистре пакетов Rust

Но как добавить в проект зависимость? Очень просто. В этом нам поможет Cargo.toml – упомянутый ранее файл манифеста, автоматически созданный cargo вместе с нашим проектом.
Ознакомимся с его содержанием:


[package] # Здесь содержится основная информация о проекте
name = "hide"
version = "0.1.5"
authors = ["Otter18 <otter18@somemail.ru>"]
edition = "2018"

[dependencies] # А здесь – все необходимые зависимости с фиксированными версиями
directories = "3.0.1"
progress_bar = "0.1.3"

Процесс поиска и добавления модулей реализован здесь необыкновенно просто:


  1. Находим нужный модуль на crates.io
    Crates.io
  2. Вставляем строчку с именем и версией в файл манифеста
  3. Все. cargo сам скачает, установит и подключит зависимость при первой сборке проекта.

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


как при работе из командной строки:


error: cannot find macro 'pritln' in this scope
--> src/main.rs:2:5
|
2 | pritln!("Hello, world!");
| ^^^^^^ help: a macro with a similar name exists: 'println'

error: aborting due to previous error

так и с помощью множества официальных плагинов для разных редакторов кода:


Rust-enhanced Sublime Text plugin



Rust-enhanced Sublime Text plugin



Подводим итоги​


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


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


Теперь мои планы на будущее – ещё больше погрузиться в изучение этого языка, познав самые тёмные его уголки.


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


weegpw3oeq24oi2lpfh_zpzhnx0.jpeg





Облачные серверы от Маклауд отлично подходят за разработки под Rust.


Источник статьи: https://habr.com/ru/company/macloud/blog/557792/
 
Сверху