Почему вам стоит изучить язык программирования Rust

Перевод | Автор оригинала: M. Tim Jones

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

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

Rust и ее генеалогия

Во-первых, давайте начнем с небольшого урока истории. Rust - новый язык по сравнению со своими предшественниками (в первую очередь C, который предшествовал ему на 38 лет), но его генеалогия создает его мультипарадигмальный подход. Rust считается C-подобным языком, но другие функции, которые он включает, создают преимущества по сравнению с его предшественниками (см. Рисунок 1).

Во-первых, Rust находится под сильным влиянием Cyclone (безопасный диалект C и императивный язык) с некоторыми аспектами объектно-ориентированных функций C++. Но он также включает в себя функциональные возможности таких языков, как Haskell и OCaml. Результатом стал C-подобный язык, поддерживающий многопарадигмальное программирование (императивное, функциональное и объектно-ориентированное).

Rust и его генеалогическое древо

Хронология исходных языков, ведущих к языку программирования Rust

Ключевые концепции в Rust

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

Во-первых, чтобы почувствовать код, давайте посмотрим на каноническую программу «Hello World», которая просто отправляет это сообщение пользователю (см. Листинг 1).

Листинг 1. «Hello World» в Rust

fn main()
{
   println!( "Hello World.");
}

Эта простая программа, похожая на C, определяет основную функцию, которая является назначенной точкой входа для программы (а она есть в каждой программе). Функция определяется ключевым словом fn, за которым следует необязательный набор параметров в круглых скобках (()). Фигурные скобки ({}) очерчивают функцию; эта функция состоит из вызова функции println! макрос, который выводит на консоль форматированный текст (stdout), как определено строковым параметром.

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

Код многократного использования через модули

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

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

В листинге 2 представлен простой пример. Он начинается с создания нового модуля, называемого битами, который содержит три функции. Первая функция, называемая pos, является частной функцией, которая принимает аргумент u32 и возвращает u32 (как указано стрелкой ->), которое представляет собой значение 1, сдвинутое влево бит раз. Обратите внимание, что ключевое слово возврата здесь не требуется. Это значение вызывается двумя общедоступными функциями (обратите внимание на ключевое слово pub): десятичной и шестнадцатеричной. Эти функции вызывают функцию private pos и выводят значение позиции бита в десятичном или шестнадцатеричном формате (обратите внимание на использование: x для обозначения шестнадцатеричного формата). Наконец, он объявляет основную функцию, которая вызывает две общедоступные функции модуля бит, с выводом, показанным в конце листинга 2 в виде комментариев.

Листинг 2. Пример простого модуля в Rust

mod bits {
   fn pos(bit: u32) ‑> u32 {
      1 << bit
   }

   pub fn decimal(bit: u32) {
      println!("Bits decimal {}", pos(bit));
   }

   pub fn hex(bit: u32) {
      println!("Bits decimal 0x{:x}", pos(bit));
   }
}

fn main( ) {
   bits::decimal(8);
   bits::hex(8);
}

// Bits decimal 256
// Bits decimal 0x100

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

Проверки безопасности для кода очистки

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

Rust включает ключевое слово unsafe, с помощью которого вы можете отключить проверки, которые обычно приводят к ошибке компиляции. Как показано в листинге 3, ключевое слово unsafe позволяет объявить небезопасный блок. В этом примере я объявляю неизменяемую переменную x, а затем указатель на эту переменную с именем raw. Затем, чтобы отменить ссылку на необработанный файл (который в этом случае будет выводить на консоль 1), я использую ключевое слово unsafe, чтобы разрешить эту операцию, которая в противном случае была бы помечена при компиляции.

Листинг 3. Небезопасные операции в Rust

fn main() {
   let a = 1;
   let rawp = &a as const i32;

   unsafe {
      println!("rawp is {}", rawp);
   }
}

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

Лучшая обработка ошибок

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

Неустранимые ошибки

Rust паника! Функция аналогична макросу assert C. Он генерирует выходные данные, чтобы помочь пользователю отладить проблему (а также остановить выполнение до того, как произойдут более катастрофические события). Паника! Функция показана в листинге 4 с ее исполняемым выводом в комментариях.

Листинг 04. Паническая обработка неисправимых ошибок в Rust!

fn main() {
   panic!("Bad things happening.");
}

// thread 'main' panicked at 'Bad things happening.', panic.rs:2:4
// note: Run with RUST_BACKTRACE=1 for a backtrace.

Из выходных данных вы можете видеть, что среда выполнения Rust указывает, где именно возникла проблема (строка 2), и отправила предоставленное сообщение (которое может содержать более описательную информацию). Как указано в выходном сообщении, вы можете сгенерировать трассировку стека, запустив специальную переменную окружения RUST_BACKTRACE. Вы также можете вызвать панику! внутренне на основе обнаруживаемых ошибок (таких как доступ к недопустимому индексу вектора).

Исправимые ошибки

Обработка исправимых ошибок - стандартная часть программирования, и Rust включает удобную функцию для проверки ошибок (см. Листинг 5). Взгляните на эту функцию в контексте файловой операции. Функция File::open возвращает тип Result<T, E>, где T и E представляют параметры универсального типа (в этом контексте они представляют std::fs::File и std::io::Error). Итак, когда вы вызываете File::open и никаких ошибок не произошло (E - это нормально), T будет представлять тип возвращаемого значения (std::fs::File). Если произошла ошибка, E будет представлять тип возникшей ошибки (с использованием типа std::io::Error). (Обратите внимание, что в моей файловой переменной f используется символ подчеркивания [], чтобы опустить предупреждение о неиспользуемой переменной, сгенерированное компилятором.)

Затем я использую специальную функцию в Rust, называемую match, которая похожа на оператор switch в C, но более эффективна. В этом контексте я сравниваю _f с возможными значениями ошибок (Ok и Err). Для Ok я возвращаю файл на переуступку; вместо Err я использую panic !.

Листинг 5. Обработка исправимых ошибок в Rust с помощью Result<t, e>

use std::fs::File;

fn main() {
   let _f = File::open("file.txt");

   let _f = match _f {
      Ok(file) => file,
      Err(why) => panic!("Error opening the file {:?}", why),
   };
}

// thread 'main' panicked at 'Error opening the file Error { repr: Os 
// { code: 2, message: "No such file or directory" } }', recover.rs:8:23
// note: Run with RUST_BACKTRACE=1 for a backtrace.

Исправимые ошибки упрощаются в Rust, когда вы используете перечисление Result; они еще более упрощены за счет использования match. Также обратите внимание на отсутствие в этом примере операции File::close: файл автоматически закрывается, когда область действия _f заканчивается.

Поддержка параллелизма и потоков

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

Рассмотрим простой пример многопоточности в Rust, который вводит некоторые новые элементы (векторные операции) и возвращает некоторые ранее обсуждавшиеся концепции (сопоставление с образцом). В листинге 6 я начинаю с импорта пространств имен thread и Duration в свою программу. Затем я объявляю новую функцию my_thread, которая представляет поток, который я создам позже. В этом потоке я просто передаю идентификатор потока, а затем засыпаю на короткое время, чтобы планировщик разрешил запуск другого потока.

Моя основная функция - это суть этого примера. Я начинаю с создания пустого изменяемого вектора, который я могу использовать для хранения значений одного и того же типа. Затем я создаю 10 потоков с помощью функции spawn и помещаю полученный дескриптор соединения в вектор (подробнее об этом позже). Этот пример порождения отсоединен от текущего потока, что позволяет потоку жить после выхода из родительского потока. После отправки короткого сообщения из родительского потока я, наконец, перебираю вектор типов JoinHandle и жду завершения каждого дочернего потока. Для каждого JoinHandle в векторе я вызываю функцию соединения, которая ожидает выхода этого потока перед продолжением. Если функция соединения возвращает ошибку, я обнаружу эту ошибку с помощью вызова сопоставления.

Листинг 6. Потоки в Rust

use std::thread;
use std::time::Duration;

fn mythread() {
   println!("Thread {:?} is running", std::thread::current().id());
   thread::sleep(Duration::from_millis(1));
}

fn main() {
   let mut v = vec![];

   for _i in 1..10 {
      v.push( thread::spawn(|| { my_thread(); } ) );
   }

   println!("main() waiting.");

   for child in v {
      match child.join() {
         Ok() =>(),
         Err(why) => println!("Join failure {:?}", why),
      };
   }
}

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

Листинг 7. Вывод потока из примера кода в листинге 6.

main() waiting.
Thread ThreadId(7) is running
Thread ThreadId(9) is running
Thread ThreadId(8) is running
Thread ThreadId(6) is running
Thread ThreadId(5) is running
Thread ThreadId(4) is running
Thread ThreadId(3) is running
Thread ThreadId(2) is running
Thread ThreadId(1) is running

Поддержка сложных типов данных (коллекций)

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

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

Для карт у вас есть структуры HashMap и BTreeMap. Вы используете структуру HashMap для создания пар ключ-значение, и вы можете ссылаться на элементы по их ключу (для получения значения). BTreeMap похож на HashMap, но может сортировать ключи, и вы можете легко перебирать все записи.

Для наборов у вас есть структуры HashSet и BTreeSet (которые, как вы заметите, следуют за структурами карт). Эти структуры полезны, когда у вас нет значений (только ключи) и вы легко можете вспомнить вставленные ключи.

Наконец, разная структура в настоящее время - это BinaryHeap. Эта структура реализует приоритетную очередь с двоичной кучей.

Установка Rust и его инструментов

Один из самых простых способов установить Rust - использовать curl через установочный скрипт. Просто выполните следующую строку из командной строки Linux®:

curl -sSf https://static.rust-lang.org/rustup.sh | sh

Эта строка передает сценарий оболочки rustup с rust-lang.org, а затем передает сценарий оболочке для выполнения. По завершении вы можете выполнить rustc -v, чтобы показать установленную вами версию Rust. Установив Rust, вы можете поддерживать его с помощью утилиты rustup, которую вы также можете использовать для обновления вашей установки Rust.

Компилятор Rust называется rustc. В показанных здесь примерах процесс сборки просто определяется как:

rustc threads.rs

… Где компилятор Rust создает собственный исполняемый файл, называемый потоками. Вы можете символически отлаживать программы на Rust, используя rust-lldb или rust-gdb.

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

Наконец, хотя Rust довольно строг в том, что он принимает в качестве исходного кода, вы можете использовать программу rust-clippy, чтобы глубже погрузиться в исходный код и выявить элементы плохой практики. Думайте о rust-clippy как о служебной программе C. lint.

Особенности Windows

В Windows Rust дополнительно требует инструментов сборки C++ для Visual Studio 2013 или новее. Самый простой способ получить инструменты сборки - это установить Microsoft Visual C++ Build Tools 2017, который предоставляет только инструменты сборки Visual C++. Кроме того, вы можете установить Visual Studio 2017, Visual Studio 2015 или Visual Studio 2013 и во время установки выбрать инструменты C++.

Для получения дополнительной информации о настройке Rust в Windows см. Документацию по rustup для Windows.

Что дальше

В середине февраля 2018 года команда Rust выпустила версию 1.24. Эта версия включает инкрементную компиляцию, автоматическое форматирование исходного кода с помощью rustfmt, новые оптимизации и стабилизацию библиотеки. Вы можете узнать больше о Rust и его развитии в блоге Rust и загрузить Rust с веб-сайта Rust Language. Там вы можете прочитать о многих других функциях, которые предлагает Rust, включая сопоставление с образцом, итераторы, замыкания и интеллектуальные указатели.