Введение в Rust

Перевод | Автор оригинала: Gints Dreimanis

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

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

В этом руководстве я постараюсь дать краткое введение и ответить на все ваши вопросы о Rust.

Вот некоторые моменты, о которых я расскажу:

Что такое Rust?

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

Rust решает проблемы, с которыми C/C++ боролся долгое время, такие как ошибки памяти и создание параллельных программ.

У него три основных преимущества:

Давайте рассмотрим каждый из них по очереди.

Нет ошибок

Если вы хотите заниматься системным программированием, вам нужен низкоуровневый контроль, который обеспечивает управление памятью. К сожалению, ручное управление сопряжено с множеством проблем на таких языках, как C. Несмотря на наличие таких инструментов, как Valgrind, выявить проблемы с управлением памятью сложно.

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

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

Более простой параллелизм

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

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

Абстракции с нулевой стоимостью

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

Эти вещи важны? да. Например, около 70% проблем, решаемых Microsoft за последние 12 лет, связаны с ошибками памяти. То же самое и с Google Chrome.

Для чего используется Rust?

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

Вот несколько примеров использования Rust:

Например, вот несколько операционных систем, написанных на Rust: Redox, intermezzOS, QuiltOS, Rux, Tock.

Является ли Rust объектно-ориентированным?

Кто знает, что в наши дни означает объектно-ориентированный подход?

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

Rust - это функциональный язык программирования?

Несмотря на то, что Rust внешне очень похож на C, он находится под сильным влиянием семейства языков ML. (В это семейство входят такие языки, как OCaml, F # и Haskell.) Например, трэйты Rust - это в основном классы типов Haskell, а Rust имеет очень мощные возможности сопоставления с образцом.

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

В общем, поддержки функционального программирования в Rust достаточно, чтобы кто-то написал об этом книгу.

Подходит ли Rust для разработки игр?

Теоретически да. Поскольку Rust ориентирован на производительность и не использует сборщик мусора, игры, написанные на нем, должны быть производительными и предсказуемо быстрыми.

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

Подробнее о разработчике игр на Rust: мы уже в игре?

Подходит ли Rust для веб-разработки?

В Rust есть несколько фреймворков для веб-разработки, таких как Actix Web и Rocket, которые очень удобны и хорошо созданы. В частности, если вам нужна чистая скорость, Actix Web является одним из лучших тестов фреймворка.

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

Подробнее о веб-разработке на Rust: мы уже в сети?

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

Модель владения данными

Давайте углубимся в одну из особенностей Rust - его проверку заимствований.

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

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

Владение данными в Rust

Движущийся

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

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

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

два потока, работающие с одними и теми же данными

Представьте, что один из этих потоков изменяет данные в куче, пока другой читает их. О, какой жуткий ужас может выйти из этого! Мы называем это гонкой за данные.

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

Приведу пример:

let mut s1 = String::from("All men who repeat a line from Shakespeare are William Shakespeare.");
let mut s2 = s1;
s1.push_str("― Jorge Luis Borges");

Это не будет компилироваться, потому что право собственности на данные перемещается в s2, а s1 больше не может быть доступен после перемещения.

Заимствование

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

Чтобы решить эту проблему, мы можем заимствовать переменные, создавая на них ссылки. Использование этих ссылок не передает права собственности, но позволяет нам либо прочитать переменную (неизменяемая ссылка или &), либо даже изменить ее (изменяемая ссылка или mut &).

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

Вот почему компилятор применяет правило для ссылок на вещи.

Вы можете сделать либо:

Вот интуитивная метафора, которую я беззастенчиво заимствую у Rust, объяснил на простом английском.

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

Rust против C++

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

Сравнение Rust и C++

Почему выбирают Rust вместо C++

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

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

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

Почему выбирают C++ вместо Rust

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

Иногда это означает, что использовать Rust невозможно, потому что практически невозможно воспроизвести поддержку экосистемы. В частности, в C++ есть игровые движки и фреймворки, которых мы не увидим в Rust в течение довольно долгого времени.

Те же проблемы, что решает Rust, современный C++ решил (несколько окольными) способами, поэтому доверие опытным разработчикам C++ - разумно безопасный вариант, если вы не хотите рисковать в Rust.

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

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

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

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

Rust и WebAssembly

Если вы еще не слышали об этом, WebAssembly похож на… сборку для Интернета.

Исторически браузеры могли запускать HTML, CSS и JavaScript, причем HTML отвечал за структуру, CSS - за внешний вид, а JavaScript - за взаимодействия. Если вам не нравится писать простой JavaScript, вы можете перенести его с других языков, которые добавляли типы, код, подобный Haskell или OCaml, и другие вещи.

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

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

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

Чтобы узнать больше о Rust и WebAssembly, посмотрите этот доклад Стива Клабника или посмотрите книгу rustwasm.

Начало работы с Rust

Чтобы начать работу с кодом Rust, вы можете либо скачать rustup здесь, либо использовать Rust Playground, онлайн-инструмент, который позволяет вам запустить некоторый код Rust и увидеть последствия.

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

Чтобы создать новый проект, перейдите в каталог, в котором должен находиться проект, и загрузите новый fizzbuzz. Это даст указание менеджеру сборки Rust создать новый проект. Как только вы это сделаете, перейдите в папку / src и откройте main.rs.

Сначала давайте напишем что-нибудь, что принимает число и возвращает:

В Rust есть очень мощный инструмент для этого:

fn fizzbuzz (number: u32) -> String {
    match (number % 3, number % 5) {
        (0, 0) => "fizzbuzz".to_string(),
        (0, _) => "fizz".to_string(),
        (_, 0) => "buzz".to_string(),
        (_, _) => number.to_string()
    }
}

Поскольку текст в кавычках - это строка в памяти или str в Rust, нам нужно преобразовать ее в String.

Теперь нам нужен способ считать до определенного числа от 1. Мы напишем новую функцию, которая принимает число в качестве аргумента, создает диапазон от 1 до числа, применяет функцию fizzbuzz и печатает результат. В Rust мы можем добиться этого с помощью простого цикла for.

fn count_up_to (number: u32) ->() {
    for i in 1..=number {
        println!("{}", fizzbuzz(i))
    }
}

Чтобы добиться какого-либо результата в терминале, нам нужна функция main. Давайте заменим hello_world следующим:

fn main() {
    count_up_to(100);
}

Теперь мы можем использовать команду cargo run main.rs и, скорее всего, увидим на нашем терминале поток гудков и гудков.

Но эй! Может быть, fizzbuzz - не единственная игра, в которую мы играем? Может быть, новая горячность - вуббалубба? Давайте быстро изменим наш код подсчета, чтобы убедиться, что мы сможем принять участие в любой игре со счетом в городе.

Для этого нам понадобится наша функция Rust, которая принимает другую функцию, которая принимает 32-битное целое число без знака и возвращает String. После добавления к сигнатуре типа того, что называется указателем на функцию, худшее прошло.

fn count_up_to_with (number: u32, function: fn(u32) -> String) ->() {
}

Внутри нам просто нужно заменить fizzbuzz на функциональную переменную.

fn count_up_to_with (number: u32, function: fn(u32) -> String) ->() {
    for i in 1..=number {
        println!("{}", function(i))
    }
}

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

Для удобства вот вуббалубба, вряд ли творческое изобретение:

fn wubbalubba (number: u32) -> String {
    match (number * 2 % 3, number % 4) {
        (0, 0) => "dub dub".to_string(),
        (0, _) => "wubba".to_string(),
        (_, 0) => "lubba".to_string(),
        (_, _) => number.to_string()
    }
}

И необходимая функция для его вызова:

fn main() {
    count_up_to_with(100, wubbalubba);
}

Дальнейшее обучение

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