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

в 12:00, , рубрики: Rust, Блог компании Маклауд, личный опыт, опыт использования, Программирование, Читальный зал, язык программирования rust
Как я влюбился в Rust и чего мне это стоило - 1

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

В этой статье я хочу рассказать, как и почему 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::io::{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 по двум ключам. Я написал ее в попытке создать наиболее простое и легкое кроссплатформенное решение для скрытия своих данных от посторонних глаз. Она поддерживает фильтрацию файлов по размерам, типам, именам, индексам и интервалам и работает с вложенными папками.

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

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

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


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

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

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

Автор: Владимир

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js