golang

Фишки Rust, связанные не только с производительностью

  • суббота, 1 марта 2025 г. в 00:00:04
https://habr.com/ru/companies/piter/articles/886754/
Почти весь код, который я пишу из интереса, написан на Rust. Это не потому, что мне нужна высокая производительность, хотя, это безусловный плюс. Я пишу много на Rust, потому что на нем приятно писать. В Rust есть много всего, что может понравиться, помимо быстрой работы без ошибок сегментирования.

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

Выразительная безопасность типов


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

Я впервые смогла оценить подобную выразительность, когда изучала Haskell, и с тех пор искала её. Я нашла её в Rust. Один из других языков, который я часто использую на работе*, — это Go, и его система типов гораздо хуже подходит для выражения идей. Всё возможно, но система типов не помогает. Rust же позволяет воплотить ваш идеи прямо в типах: перечисления (enums), структуры (structs) и типажи (traits) дают огромное поле для манёвра

При этом Rust также отлично обеспечивает безопасность типов! В Python я могу выразить многое, но не могу полностью доверять получившемуся коду, пока тщательно его не протестирую. В Python у вас нет компилятора, который проверяет вашу работу! Очень полезно иметь под рукой компилятор Rust, чтобы убедиться, что вы правильно используете типы и соблюдаете ограничения. Возвращаясь к теме гонок данных (data races), система типов — один из механизмов, позволяющих их предотвратить! Существуют типажи, указывающие, безопасно ли передавать данные в другой поток или делиться ими с другим потоком. Если в вашем языке нет аналогов таких типажей, то, скорее всего, вы именно от программиста требуется самостоятельно обеспечить эти свойства!

Тем не менее, система типов Rust — это не абсолютное благо для меня. Например, создание работоспособного прототипа в Rust может занять больше времени, чем в Python, из-за строгости системы типов: либо вы выполняете её требования, либо ваш код не запустится. Кроме того, я считаю, что многие элементы Rust, использующие обобщённые типы (generics), очень сложно читать с листа. Создаётся ощущение, будто это «солянка из типажей». Что именно мы будем обобщать, зависит от реализации и привычек, так что это не обязательно присуще языку как таковому, но тесно с ним связано.

Он не так часто вылетает с ошибками.


Ладно, у меня есть претензия к Go. Не могу не упомянуть «ошибку на миллиард долларов» в формулировке Тони Хоара: нулевые указатели (null pointers). Go предоставляет указатели, и они могут быть null**! Это означает, что вы можете попытаться вызвать метод на нулевом указателе, что может привести к сбою вашей программы.

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

Сбои в Rust обычно происходят, когда программа либо намеренно паникует из-за неустранимой ошибки, либо непреднамеренно паникует, если использует unwrap для Option или Result. Лучше явно обрабатывать второй случай.

К счастью, можно настроить линтер Clippy так, чтобы он запрещал код, использующий unwrap (или expect)! Если вы добавите это в ваш файл Cargo.toml, он будет отклонять любой код, где используется unwrap.

[lints.clippy]
unwrap_used = "deny"

Иммунитет к гонкам данных


Так сложно написать многопоточный код, который работает корректно. Гонки данных — один из самых серьёзных факторов, приводящих к такой проблеме. Предотвращение гонок данных в Rust невероятно помогает при написании многопоточного кода.

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

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


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

В большинстве случаев вам всё равно приходится взаимодействовать с операционной системой, поэтому вы не можете полностью контролировать ЦП и память, но всё равно – этот контроль гораздо шире, чем в Python. И даже шире, чем в Go — языке, которым мы пользуемся, стремясь добиться хорошей производительности.

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

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

Сочетание функционального и императивного стиля


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

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

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

Полезные ошибки компилятора


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

В начале своей карьеры я страдала от C++. Помимо всех сбоев в продакшене и сопутствующего стресса, приходилось иметь дело с совершенно непостижимыми ошибками компилятора. Повредив шаблон, вы иногда получали тысячи строк ошибок – из-за одной пропущенной точки с запятой. И они не говорили вам о том, в чем заключалась ошибка, а скорее о том, что появилось после нее.

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

Это весело!


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

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

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

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



*Go применялся у меня на работе, потому что я его продвигала! Возможно, я бы иногда пользовалась Go для развлечения, если бы не получала его в достаточном количестве на своей основной работе. Это невероятно полезный язык, просто его система типов — не моя любимая.

** В Go они выражаются как nil, что является нулевым значением и эквивалентно null в других языках.



Книга по теме «Эффективный Rust. 35 конкретных способов улучшить код» Дэвида Дрисдейла в продаже с 07.04.2025

Для Хаброжителей скидка 30% по купону — Предзаказ