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

Первая стабильная версия Rust вышла в 2015 году, и с тех пор, начиная с 2016 года, он ежегодно признается самым любимым языком в ежегодном опросе разработчиков на Stack Overflow (теперь, в 2023 году, это называется "Востребованный"). Почему же разработчики, попробовав Rust, не могут перестать его использовать? В мире разрекламированных преемников C/C++ Rust, похоже, выходит на первое место. Как получилось, что язык, который появился на основной сцене всего в прошлом десятилетии, стал таким популярным?

Снимок экрана 2024-05-31 в 11.53.52.png
Снимок экрана 2024-05-31 в 11.53.52.png

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан НЕшколой для инженеров Inzhenerka.Tech совместно с автором курса-тренажера по Rust. Больше материала в нашем сообществе

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

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

Экосистема Rust

Большинство языков, которые я регулярно использую, прикрепляют управление пакетами и версиями как нечто второстепенное. Системы, такие как npm, pip и NuGet, сегодня вполне удобны, но так было не всегда, и они всё еще далеки от совершенства. Управление установленной версией самого языка по-прежнему остаётся проблемой для большинства языков.

Вы устанавливаете Rust с помощью rustup, инструмента, который позже помогает вам управлять версией Rust и сопутствующими инструментами.

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

Другой огромной частью экосистемы Rust является его документация. Я изучил язык исключительно по официальной документации и никогда не чувствовал необходимости искать учебные материалы в других местах. Между "книгой" и Rust By Example было охвачено всё, что мне нужно было знать. На самом деле, когда я находился на Stack Overflow с какой-то проблемой, самые полезные ответы обычно указывали на нужный раздел либо официальной документации, либо одного из этих двух источников.

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

Кря-кря! Утиная типизация с трейтом

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

Интересная концепция, которую я полюбил, работая с динамически типизированными языками, — это утиная типизация. Это принцип, согласно которому функция может принимать объект любого типа, если у него есть нужные свойства и методы, которые требуются функции. "Если он ходит как утка и крякает как утка, значит, это утка". Если функция, которую мы вызываем, требует, чтобы её входные данные могли плавать, то нас не должно волновать, что это утка. Нас должно интересовать только то, может ли она плавать.

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

Давайте рассмотрим пример. Вот трейд для плавания. Любой тип, который реализует трейд Swim, способен плавать

trait Swim {
    fn swim(&self);
}

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

fn cross_the_pond(animal: impl Swim) {
    animal.swim();
}

Давайте создадим несколько типов, которые можно передать в функцию cross_the_pond. Мы можем создать тип, называемый Duck, определив структуру и реализовав для неё трейд Swim.

struct Duck {
    name: String,
}
impl Swim for Duck {
fn swim(&self) {
println!("{} paddles furiously...", self.name);
}
}

Но утка — это не единственное, что умеет плавать. Давайте определим структуру Elephant и реализуем для неё трейд Swim.

struct Elephant {
    name: String,
}
impl Swim for Elephant {
fn swim(&self) {
println!("{} is actually just walking on the bottom...", self.name);
}
}

Наша функция main может создавать экземпляры уток и слонов и объединить всё это.

fn main() {
    let duck = Duck { name: String::from("Sir Quacks-a-lot") };
    let ellie = Elephant { name: String::from("Ellie BigEndian") };
println!("Crossing the pond...");

cross_the_pond(duck);
cross_the_pond(ellie);

}

Это выводит следующий результат:

Crossing the pond...
Sir Quacks-a-lot paddles furiously...
Ellie BigEndian is actually just walking on the bottom...

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

Тестирование кода внутри кода

Разработчики часто имеют чёткие мнения о структуре папок и соглашениях по именованию файлов. Все соглашаются с тем, что мы хотим, чтобы наши папки были максимально чистыми, но люди часто расходятся во мнениях о том, что это на самом деле означает. Одним из спорных вопросов является место для тестов. Должна ли быть отдельная папка для тестов? Должна ли структура папки с тестами отражать структуру исходной папки? Должны ли тесты смешиваться с исходным кодом? Следует ли префиксировать тестовые файлы с test_, чтобы тесты были сгруппированы вместе, или суффиксировать их _test, чтобы тесты были с кодом, который они тестируют?

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

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

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


// A public function that takes two integers and returns double their sum
pub fn add_and_double(x: i32, y: i32) -> i32 {
    2 * _add(x, y)
}
// A private helper function that adds two integers
fn _add(x: i32, y: i32) -> i32 {
x + y
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_double() {
    assert_eq!(add_and_double(2, 3), 10);
    assert_eq!(add_and_double(0, 0), 0);
}

#[test]
fn test_add() {
    assert_eq!(_add(2, 3), 5);
    assert_eq!(_add(0, 0), 0);
}

}

Атрибут #[cfg(test)] указывает компилятору компилировать модуль тестов только при запуске тестов, и эти тесты будут удалены в сборке для продакшена.

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

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

Взял взаймы, никогда не возвращал, затем переместил?

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

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

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

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

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

fn main() {
    let original_owner = String::from("Something");
    let new_owner = original_owner;
println!("{}", original_owner);

}

Я нашёл это очень запутанным. Давайте посмотрим на сообщение об ошибке:

error[E0382]: borrow of moved value: `original_owner`
 --> src/main.rs:6:20
  |
3 |     let original_owner = String::from("Something");
  |         -------------- move occurs because `original_owner` has type `String`, which does not implement the `Copy` trait
4 |     let new_owner = original_owner;
  |                     -------------- value moved here
5 |
6 |     println!("{}", original_owner);
  |                    ^^^^^^^^^^^^^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
4 |     let new_owner = original_owner.clone();
  |                                   ++++++++
For more information about this error, try rustc --explain E0382.

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

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

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

Заключительные мысли

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

Я обнаружил то, что почти 85% разработчиков, использовавших Rust, видят в нём, и когда в мой почтовый ящик придёт письмо об опросе Stack Overflow за 2024 год, и в опросе спросят, хочу ли я продолжать использовать Rust в следующем году, я, безусловно, отвечу «Да».

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан НЕшколой для инженеров Inzhenerka.Tech совместно с автором курса-тренажера по Rust. Больше материала в нашем сообществе

Комментарии (27)


  1. unreal_undead2
    31.05.2024 11:02
    +9

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


    1. Dominux
      31.05.2024 11:02
      +6

      Явное наследование - пример номинативной подтипизации. А вот имлементирование интерфейса (или трейта в расте) - пример структурной (автор по ошибке принял ее за утиную)


      1. pooqpooq
        31.05.2024 11:02

        Имплементирование трейта в расте — это тоже пример номинативной типизации, так как имплементация явно указывает имя. Типизация была бы структурной, если бы тайпчекер сам решал, что коли к вашему типу применимы такие-то методы, то он реализует такой-то трейт.


        1. Dominux
          31.05.2024 11:02

          Есть понятие явной имплементации трейта, а есть - не явной (как в го)


  1. rukhi7
    31.05.2024 11:02

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


    1. Octabun
      31.05.2024 11:02

      Утиная типизация предназначена не для писания Фурье преобразований, а для решения проблем которые первоначально решались ООП. Но это не мешает ей помогать не писать преобразование Фурье лишний раз.


  1. rukhi7
    31.05.2024 11:02

    или какой-нибудь простенький PID регулятор, например.


  1. Octabun
    31.05.2024 11:02

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

    Что прекрасно соответствует

    Данная статья это перевод с английского с некоторыми адаптациями.

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


  1. piton369
    31.05.2024 11:02

    Спасибо за статью!

    В интернетах я слышал два тезиса. Что программы на Rust хорошо защищены и что часто хакеры пишут свои программы на Rust. Кто что скажет по этому поводу и как такое сочетание возможно одновременно?


    1. fedorro
      31.05.2024 11:02
      +5

      А в чем тут противоречие? Хакеры тоже хотят писать защищенные программы. Вот если бы второй тезис звучал как "Хакеры часто ломают программы на Rust" - тогда да. Но в общем это два независимых тезиса.


  1. Dominux
    31.05.2024 11:02
    +4

    Утиная подтипизация не требует явного статического указания типа. Rust traits представляют из себя пример структурной подтипизации. В других языках программирования структурная подтипизация часто реализована в виде интерфейсов, в Python - через typing.Protocol и т.д.


  1. geher
    31.05.2024 11:02

    Начал изучать "этот ваш" Rust. Пока впечатление не очень. Безопасность при использовании памяти, это, конечно, хорошо, но некоторые моменты контринтуитивны. Короче, почти "еще один C++".

    И есть несколько вопросов, на которые пока не видно приемлемых ответов.

    Во-первых, есть ли в экосистеме готовое решение с приличной кроссплатформенной IDE, чтобы устаеовил - и все заработало? Я отсутствие подобного как-нибудь переживу, а большинство - не очень

    Во-вторых, есть ли приличные, опять же, кроссплатформенные средства для разработки GUI хотя бы уровня QT Creator?

    И еще смущает сильная завязанность экосистемы на единственный ресурс (по крайней мере это сквозит из всех щелей в любом учебнике по языку. Менеддер пакетов - это, конечно, хорошо, но хотелось бы и запасные варианты иметь.

    Для сравнения, тот же С++ с кучей библиотек вместе с QT Creator и основными библиотеками доступен в любом дистрибутиве линукс. Для винды тоже есть где скачать. Сами библиотеки как правило доступны из нескольких источников. В случае чего всегда есть возможность построить без особенного напряжения достаточную инфраструктуру для разработки почти любого пртдукта.


    1. andy128k
      31.05.2024 11:02
      +4

      1. Любой редактор с поддержкой языковых серверов + rust-analyzer. VS Code, например.

      2. Из приличных есть Gtk-rs, из кросс-платформенных -- Tauri, Slint.

      3. Есть, например, lib.rs как альтернатива crates.io


      1. geher
        31.05.2024 11:02

        Спасибо.

        С первым пунктом буду пробовать.

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

        Третье уже радует.


        1. andy128k
          31.05.2024 11:02
          +1

          У Gtk есть минимум два визуальных редактора: традиционный Glade и новый Cambalache. Также появился новый язык разметки -- Blueprint. Это всё, конечно же, не rust-специфично и доступно для всех языков.


    1. sdramare
      31.05.2024 11:02

      Есть RustRover от JetBrains, если вы привыкли к Idea/VS. А так lsp есть под все - vscode, neovim, emacs


    1. Octabun
      31.05.2024 11:02

      1. Не сильно обольщайтесь. То, что даёт языковый сервер и tree sitter действительно есть везде где поддерживаются языковый сервер и tree sitter, но это не делает текстовый редактор IDE в том смысле, в каком Visual Studio - IDE, а VS Code - нет.

      2. Если честно - то нет. Разрабатывать GUI в принципе - можно. Есть привязки к библиотекам типа GTK, Qt, SDL. Есть кросс-платформенные игровые движки типа Bevy. Есть средства разработки GUI поддерживающие Rust в том числе, типа Slint. Есть средства разработки GUI использующие Rust наряду с, типа Tauri - аналога Electron. Есть даже возможность прицепиться к Flutter...

      3. Non-issue. Менеджер пакетов не обязателен и все, а может и не все но почти, пакеты так или иначе лежат на GitHub. Что crates.io, что lib.rs - не ресурсы от которых может быть зависимость, это индексы ресурсов. Если кажется что cargo с её add - это ресурс от которого тоже может быть зависимость, то это не так, это стандарт понятия "пакет" и их два быть не может. В некотором смысле есть зависимость от Git и GitHub, но против выпиливания из состава человечества запасных вариантов не бывает.


      1. vtb_k
        31.05.2024 11:02
        +1

        Не сильно обольщайтесь. То, что даёт языковый сервер и tree sitter действительно есть везде где поддерживаются языковый сервер и tree sitter, но это не делает текстовый редактор IDE в том смысле, в каком Visual Studio - IDE, а VS Code - нет.

        Уже немного достал этот снобизм. Я в Емаксе с lsp+gopls намного продуктивнее чем мои коллеги c Goland. Плохому танцору как известно всегда что-то мешает.


        1. Octabun
          31.05.2024 11:02
          +1

          Где Вы видите снобизм? Примените эмпатию и попытайтесь понять о чём человек спрашивает. Вроде как очевидно - ему бы идеально подошёл Delphi, только для Rust. И все Emacs просто не о том.

          Я в Емаксе с lsp+gopls намного продуктивнее чем мои коллеги c Goland. Плохому танцору как известно всегда что-то мешает.

          А если найду? придумаю задачу где будет наоборот? Кто-нибудь заводил речь о Вас?

          Если бы человеку хоть немного подходил Emacs, то он бы сразу hx --health и ничего бы не спрашивал...


          1. vtb_k
            31.05.2024 11:02
            +1

            Где Вы видите снобизм?

            Я вам цитату привел, где высказан чистой воды снобизм.

            ему бы идеально подошёл Delphi,

            Человек написал про кроссплатформенную ИДЕ, делфи такой не является. Нужно внимательно читать ветку

            А если найду? придумаю задачу где будет наоборот?

            Удачи


            1. Octabun
              31.05.2024 11:02
              +1

              Человек написал про кроссплатформенную ИДЕ, делфи такой не является. 

              Давненько Вы на сайте Embarсadero не были...


      1. DarthVictor
        31.05.2024 11:02

        А что есть например в Visual Studio или WebStorm, и чего нет в VSCode?


        1. Octabun
          31.05.2024 11:02

          А что есть

          Можно найти продолжительные дискуссии по этому поводу. Я их не запоминаю ибо не фанат ни того ни другого.


        1. geher
          31.05.2024 11:02

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

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

          Именно потому многие предпочитают таки требующую лицензию студию свободно распространяемому VSCode.


          1. vtb_k
            31.05.2024 11:02

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

            В случае с Емакс я с вами согласен, но вот c VSCode это неправда. Просто открываете проект, нажимаете пару кнопок и готово. Абсолютно аналогичная ситуация с Visual studio.
            Разница разве что только в том, что студия просит скачать гигабайты непонятно чего при установке.


            1. geher
              31.05.2024 11:02

              А с VSCode ставятся компиляторы всех поддерживаемых языков и языковые серверы из коробки?


              1. vtb_k
                31.05.2024 11:02

                Буквально нажатием одной кнопки