Rust — молодой и дерзкий язык программирования
Говорят, что это одновременно C++ и Haskell.
Первая версия языка Rust появилась в 2010 году, и он сразу занял третью строчку в списке любимых языков разработчиков на StackOverflow. Год спустя Rust возглавил этот список и держался там несколько лет. Давайте посмотрим, почему этот язык стал таким популярным, в чём его особенности и почему вокруг него много споров.
В чём идея языка Rust
Автору языка нравилась скорость работы и всемогущество языка C++ и надёжность Haskell. Он поставил перед собой задачу совместить оба этих подхода в одном языке, и за несколько лет он собрал первую версию языка Rust.
Rust позиционируется как компилируемый системный мультипарадигмальный язык высокого уровня. Сейчас поясним, что это значит.
👉 Компилируемый язык означает, что готовая программа — это отдельный файл, который можно запустить на любом компьютере с нужной операционной системой. Для запуска не нужно устанавливать среду разработки и компилятор, достаточно, чтобы скомпилированная версия подходила к вашему компьютеру.
👉 Системный — это когда на языке пишут программы для работы системы в целом. Это могут быть операционные системы, драйверы и служебные утилиты. Обычные программы тоже можно писать на Rust — от калькулятора до системы управления базами данных. Системный язык позволяет писать очень быстрые программы, которые используют все возможности железа.
👉 Мультипарадигмальный значит, что в языке сочетаются несколько парадигм программирования. В случае Rust это ООП, процедурное и функциональное программирование. Причём, ООП в Rust пришло из C++, а функциональное — из Haskell. Программист может сам выбирать, в каком стиле он будет писать код, или совмещать разные подходы в разных элементах программы.
Синтаксис и код
За основу синтаксиса в Rust взят синтаксис из C и C++.Например, классический «Привет, мир!» на Rust выглядит так:
fn main() <
println!(«Hello, world!»);
>
Если вы знакомы с подобным синтаксисом, то сможете быстро начать писать и на Rust. Другое дело, что в Rust есть свои особенности:
let x = if new_game() < 4 >
else if reload() < 3 >
else
Последнее разберём подробно. При такой записи переменная x будет равна четырём, если функция new_game() вернёт значение true. Если этого не случится, компилятор вызовет функцию reload() и проверит, что получилось. Если true, то x примет значение 3, а если и это не сработает — то x станет равным 0.
Ещё в Rust есть сравнение переменной с образцом. В зависимости от того, с каким образцом совпало значение переменной, выполнится та или иная функция:
Главная особенность программ на Rust
Несмотря на синтаксис, похожий на C, главную особенность программ на Rust разработчики взяли из Haskell, и звучит она так:
Если программа на Rust скомпилировалась и не упала во время запуска, то она будет работать до тех пор, пока вы сами её не остановите.
Это значит, что программы на Rust почти так же надёжны, как программы на Haskell. Почти — потому что если программист использует «небезопасный» блок unsafe, который даёт ему прямой доступ к памяти, то в теории это иногда может привести к сбоям. Но даже с такими блоками Rust старается справляться сам и падает только в безнадёжных случаях.
Плюсы и минусы языка
Когда язык совмещает в себе несколько разных подходов из других языков, он получает большинство преимуществ каждого из них:
Минусы в основном связаны со скоростью развития языка. Так как Rust развивается очень быстро, то часто бывает так, что код из старой версии не работает в новой версии. Ещё к минусам можно добавить:
Что написано на Rust
Чаще всего Rust используют в тех проектах, где нужна стабильность и надёжность при высокой нагрузке и общее быстродействие программы.
На практике Rust подходит для разработки ОС, веб-серверов, системных программ мониторинга, веб-движков, а также для создания масштабируемых частей фронтенда и бэкенда. Например, вот самые известные проекты, где Rust был основным языком программирования:
Обзор языка программирования Rust
Rust — новый экспериментальный язык программирования, разрабатываемый Mozilla. Язык компилируемый и мультипарадигмальный, позиционируется как альтернатива С/С++, что уже само по себе интересно, так как даже претендентов на конкуренцию не так уж и много. Можно вспомнить D Вальтера Брайта или Go от Google.
В Rust поддерживаются функицональное, параллельное, процедурное и объектно-ориентированное программирование, т.е. почти весь спектр реально используемых в прикладном программировании парадигм.
Я не ставлю целью перевести документацию (к тому же она весьма скудная и постоянно изменяется, т.к. официального релиза языка еще не было), вместо этого хочется осветить наиболее интересные фичи языка. Информация собрана как из официальной документации, так и из крайне немногочисленных упоминаний языка на просторах Интернета.
Первое впечатление
Синтаксис языка строится в традиционном си-подобном стиле (что не может не радовать, так как это уже стандарт де-факто). Естественно, всем известные ошибки дизайна С/С++ учтены.
Традиционный Hello World выглядит так:
Пример чуть посложнее — функция расчета факториала:
Как видно из примера, функции объявляются в «функциональном» стиле (такой стиль имеет некоторые преимущества перед традиционным «int fac(int n)»). Видим автоматический вывод типов (ключевое слово let), отсутствие круглых скобок у аргумента while (аналогично Go). Еще сразу бросается в глаза компактность ключевых слов. Создатели Rust дейтсвительно целенаправленно сделали все ключевые слова как можно более короткими, и, скажу честно, мне это нравится.
Мелкие, но интересные синтаксические особенности
Типы данных
Rust, подобно Go, поддерживает структурную типизацию (хотя, по утверждению авторов, языки развивались независимо, так что это влияние их общих предшественников — Alef, Limbo и т.д.). Что такое структурная типизация? Например, у вас в каком-то файле объявлена структура (или, в терминологии Rust, «запись»)
type point =
Вы можете объявить кучу переменных и функций с типами аргументов «point». Затем, где-нибудь в другом месте, вы можете объявить какую-нибудь другую структуру, например
type MySuperPoint =
и переменные этого типа будут полностью совместимы с переменными типа point.
В противоположность этому, номинативная типизация, принятая в С, С++,C# и Java таких конструкций не допускает. При номинативной типизации каждая структура — это уникальный тип, по умолчанию несовместимый с другими типами.
Структуры в Rust называются «записи» (record). Также имеются кортежи — это те же записи, но с безымянными полями. Элементы кортежа, в отличие от элементов записи, не могут быть изменяемыми.
Имеются вектора — в чем-то подобные обычным массивам, а в чем-то — типу std::vector из stl. При инициализации списком используются квадратные скобки, а не фигурные как в С/С++
Вектор, тем ни менее — динамическая структура данных, в частности, вектора поддерживают конкатенацию.
Есть шаблоны. Их синтаксис вполне логичен, без нагромождений «template» из С++. Поддерживаются шаблоны функций и типов данных.
Язык поддерживает так называемые теги. Это не что иное, как union из Си, с дополнительным полем — кодом используемого варианта (то есть нечто общее между объединением и перечислением). Или, с точки зрения теории — алгебраический тип данных.
В простейшем случае тег идентичен перечислению:
В более сложных случаях каждый элемент «перечисления» — самостоятельная структура, имеющая свой «конструктор».
Еще интересный пример — рекурсивная структура, с помощью которой задается объект типа «список»:
Теги могут участвовать в выражениях сопоставления с образцом, которые могут быть достаточно сложными.
Сопоставление с образцом (pattern matching)
Для начала можно рассматривать паттерн матчинг как улучшенный switch. Используется ключевое слово alt, после которого следует анализируемое выражение, а затем в теле оператора — паттерны и действия в случае совпадения с паттернами.
В качестве «паттеронов» можно использовать не только константы (как в Си), но и более сложные выражения — переменные, кортежи, диапазоны, типы, символы-заполнители (placeholders, ‘_’). Можно прописывать дополнительные условия с помощью оператора when, следующего сразу за паттерном. Существует специальный вариант оператора для матчинга типов. Такое возможно, поскольку в языке присутствует универсальный вариантный тип any, объекты которого могут содержать значения любого типа.
Указатели. Кроме обычных «сишных» указателей, в Rust поддерживаются специальные «умные» указатели со встроенным подсчетом ссылок — разделяемые (Shared boxes) и уникальные (Unique boxes). Они в чем-то подобны shared_ptr и unique_ptr из С++. Они имеют свой синтаксис: @ для разделяемых и
для уникальных. Для уникальных указателей вместо копирования существует специальная операция — перемещение:
после такого перемещения указатель x деинициализируется.
Замыкания, частичное применение, итераторы
С этого места начинается функциональное программирование. В Rust полностью поддерживается концепция функций высшего порядка — то есть функций, которые могут принимать в качестве своих аргументов и возвращать другие функции.
1. Ключевое слово lambda используется для объявления вложенной функции или функционального типа данных.
В этом примере мы имеем функцию make_plus_function, принимающую один аргумент «x» типа int и возвращающую функцию типа «int->int» (здесь lambda — ключевое слово). В теле функции описывается эта самая фунция. Немного сбивает с толку отсутствие оператора «return», впрочем, для ФП это обычное дело.
2. Ключевое слово block используется для объявления функционального типа — аргумента функции, в качестве которого можно подставить нечто, похожее на блок обычного кода.
Здесь мы имеем функцию, на вход которой подается блок — по сути лямбда-функция типа «int->int», и вектор типа int (о синтаксисе векторов далее). Сам «блок» в вызывающем коде записыавется с помощью несколько необычного синтаксиса <|x| x + 1 >. Лично мне больше нравятся лямбды в C#, символ | упорно воспринимается как битовое ИЛИ (которое, кстати, в Rust также есть, как и все старые добные сишные операции).
3. Частичное применение — это создание функции на основе другой функции с большим количеством аргументов путем указания значений некоторых аргументов этой другой функции. Для этого используется ключевое слово bind и символ-заполнитель «_»:
Чтобы было понятнее, скажу сразу, что такое можно сделать на обычном Си путем создания простейшей обертки, как-то так:
const char* daynum (int i) < const char *s =<"mo", "tu", "we", "do", "fr", "sa", "su">; return s[i]; >
Но частичное применение — это функциональный стиль, а не процедурный (кстати, из приведенного примера неясно, как сделать частичное применение, чтобы получить функцию без аргументов)
Еще пример: объявляется функция add с двумя аргументами int, возвращающая int. Далее объявляется функциональный тип single_param_fn, имеющий один аргумент int и возвращающий int. С помощью bind объявляются два функциональных объекта add4 и add5, построенные на основе функции add, у которой частично заданы аргументы.
Функциональные объекты можно вызывать также, как и обычные функции.
4. Чистые функции и предикаты
Чистые (pure) функции — это функции, не имеющие побочных эффектов (в том числе не вызывающие никаких других функций, кроме чистых). Такие функции выдяляются ключевым словом pure.
Предикаты — это чистые (pure) функции, возвращающие тип bool. Такие функции могут использоваться в системе typestate (см. дальше), то есть вызываться на этапе компиляции для различных статических проверок.
Синтаксические макросы
Планируемая фича, но очень полезная. В Rust она пока на стадии начальной разработки.
Выражение, аналогичное сишному printf, но выполняющееся во время компиляции (соответственно, все ошибки аргументов выявляются на стадии компиляции). К сожалению, материалов по синтаксическим макросам крайне мало, да и сами они находятся в стадии разработки, но есть надежда что получится что-то типа макросов Nemerle.
Кстати, в отличие от того же Nemerle, решение выделить макросы синтаксически с помощью символа # считаю очень грамотным: макрос — это сущность, очень сильно отличающаяся от функции, и я считаю важным с первого взгляда видеть, где в коде вызываются функции, а где — макросы.
Атрибуты
Концепция, похожая на атрибуты C# (и даже со схожим синтаксисом). За это разработчикам отдельное спасибо. Как и следовало ожидать, атрибуты добавляют метаинформацию к той сущности, которую они аннотируют,
Придуман еще один вариант синтаксиса атрибутов — та же строка, но с точкой с запятой в конце, аннотирует текущий контекст. То есть то, что соответствует ближайшим фигурным скобкам, охватывающим такой атрибут.
Параллельные вычисления
Пожалуй, одна из наиблее интересных частей языка. При этом в tutorial на данный момент не описана вообще:)
Программа на Rust состоит из «дерева задач». Каждая задача имеет функцию входа, собственный стек, средства взаимодействия с другими задачами — каналы для исходящей информации и порты для входящей, и владеет некоторой частью объектов в динамической куче.
Множество задач Rust могут существовать в рамках одного процесса операционной системы. Задачи Rust «легковесные»: каждая задача потребляет меньше памяти чем процесс ОС, и переключение между ними осуществляется быстрее чем переключение между процессами ОС (тут, вероятно, имеются в виду все-же «потоки»).
Задача состоит как минимум из одной функции без аргументов. Запуск задачи осуществляется с помощью функции spawn. Каждая задача может иметь каналы, с помощью которых она передает инфорацию другим задачам. Канал — это специальный шаблонный тип chan, параметризируемый типом данных канала. Например, chan — канал для передачи беззнаковых байтов.
Для передачи в канал используется функция send, первым аргументом которой является канал, а вторым — значение для передачи. Фактически эта функция помещает значение во внутренний буфер канала.
Для приема данных используются порты. Порт — это шаблонный тип port, параметризируемый типом данных порта: port — порт для приема беззнаковых байтов.
Для чтения из портов используется функция recv, аргументом которой является порт, а возвращаемым значением — данные из порта. Чтение блокирует задачу, т.е. если порт пуст, задача переходит в состояние ожидания до тех пор, пока другая задача не отправит на связанный с портом канал данные.
Связывание каналов с портами происходит очень просто — путем инициализации канала портом с помощью ключевого слова chan:
let reqport = port();
let reqchan = chan(reqport);
Несколько каналов могут быть подключены к одному порту, но не наоборот — один канал не может быть подключен одновременно к нескольким портам.
Typestate
Общепринятого перевода на русский понятия «typestate» я так и не нашел, поэтому буду называть это «состояния типов». Суть этой фичи в том, что кроме обычного контроля типов, принятого в статической типизации, возможны дополнительные контекстные проверки на этапе компиляции.
В том или ином виде состояния типов знакомы всем программистам — по сообщениям компилятора «переменная используется без инициализации». Компилятор определяет места, где переменная, в которую ни разу не было записи, используется для чтения, и выдает предупреждение. В более общем виде эта идея выглядит так: у каждого объекта есть набор состояний, которые он может принимать. В каждом состоянии для этого объекта определены допустимые и недопустимые операции. И компилятор может выполнять проверки — допустима ли конкретная операция над объектом в том или ином месте программы. Важно, что эти проверки выполняются на этапе компиляции.
Например, если у нас есть объект типа «файл», то у него может быть состояние «закрыт» и «открыт». И операция чтения из файла недопустима, если файл закрыт. В современных языках обычно функция чтения или бросает исключение, или возвращает код ошибки. Система состояний типов могла бы выявить такую ошибку на этапе компиляции — подобно тому, как компилятор определяет, что операция чтения переменной происходит до любой возможной операции записи, он мог бы определить, что метод «Read», допустимый в состоянии «файл открыт», вызывается до метода «Open», переводящего объект в это состояние.
В Rust существует понятие «предикаты» — специальные функции, не имеющие побочных эффектов и возвращающие тип bool. Такие функции могут использоваться компилятором для вызова на этапе компиляции с целью статических проверок тех или иных условий.
Ограничения (constraints) — это специальные проверки, которые могут выполняться на этапе компиляции. Для этого используется ключевое слово check.
Предикаты могут «навешиваться» на входные параметры функций таким вот способом:
Информации по typestate крайне мало, так что многие моменты пока непонятны, но концепция в любом случае интересная.
На этом все. Вполне возможно, что я все-же пропустил какие-то интересные моменты, но статья и так раздулась. При желании можно уже сейчас собрать компилятор Rust и попробовать поиграться с различными примерами. Информация по сборке приведена на официальном сайте языка.
Что такое rust язык программирования
Rust невероятно быстр и эффективен по использованию памяти: без рантайма или сборщика мусора он может обеспечить работу критичных для производительности сервисов, запускаться на встраиваемых устройствах и легко интегрироваться с другими языками.
Надёжность
Богатая система типов Rust и модель владения гарантируют потокобезопасность и безопасность памяти, и позволяют устранить множество классов ошибок во время компиляции.
Продуктивность
У Rust отличная документация, дружественный компилятор с полезными сообщениями об ошибках и первоклассный инструментарий — интегрированный пакетный менеджер и инструмент сборки, умная мультиредакторная поддержка с автокомплитом, проверками типов, автоформатированием и многим другим.
Создайте это в Rust
В 2018 году, сообщество Rust приняло решение расширить присутствие языка для нескольких областей (смотрите roadmap на 2018 год). Для этого вы можете найти множество высококачественных пакетов и потрясающие руководства о том, как начать.
Командная строка
Быстро создайте инструмент командной строки с помощью надёжной экосистемы Rust. Rust поможет вам с уверенностью поддерживать ваше приложение и с лёгкостью его распространять.
WebAssembly
Используйте Rust для перезарядки вашего JavaScript, по одному модулю за раз. Опубликуйте в npm, упакуйте с webpack и вы готовы к соревнованиям.
Сетевое программирование
Предсказуемая производительность. Крошечные требования к ресурсам. Потрясающая надёжность. Rust отлично подходит для сетевых сервисов.
Встраиваемые системы
Ориентируетесь на устройства с малой производительностью? Нужен низкоуровневый контроль без отказа от высокоуровневых удобств? Rust предоставит это.
Промышленное использование Rust
Сотни компаний по всему миру используют Rust в реальных проектах для быстрых кросс-платформенных решений с ограниченными ресурсами. Такие проекты, как Firefox, Dropbox и Cloudflare, используют Rust. Rust отлично подходит как для стартапов, так и для больших компаний, как для встраиваемых устройств, так и для масштабируемых web-сервисов.
Мой самый большой комплимент Rust — то, что он скучный, и это потрясающий комплимент.
– Chris Dickinson, инженер npm, Inc
– Antonio Verardi, инженер по инфраструктуре
Примите участие
Читай про Rust
Мы любим документацию! Посмотрите книги, доступные онлайн, а также ключевые блоги и пользовательские руководства.
Смотрите про Rust
У Rust сообщества есть отдельный канал на YouTube, где собрано огромное количество презентаций и учебных пособий.
Сделать вклад
Благодарности
Rust не существовал бы без щедрого вклада времени, работы и ресурсов от отдельных лиц и компаний. Мы очень благодарны за поддержку!
Отдельные участники
Корпоративные спонсоры
Проект Rust получает поддержку от компаний через пожертвования для инфраструктуры.
Почему вы должны попробовать Rust
Это ознакомительная статья о языке программирования Rust и его инструментах, с помощью которой я надеюсь привлечь ваше внимание к этому интересному и уникальному языку, созданному чтобы дать ответ на следующие вопросы разработчиков:
Как убедиться, что в моем приложении нет проблем и уязвимостей, связанных с неправильной работой с памятью?
Как быть уверенным в том, что любой доступ к общим объектам правильно защищен?
Как свести к минимуму любую работу, не связанную напрямую с написанием кода?
Цель данной статьи не рассказать о доселе невиданных возможностях Rust (сразу говорю, что тут ничего нового «Растоманы» не найдут), на Хабре вы итак найдете множество интересных статей о внутренностях языка и интересных случаев использования. Моя цель рассказать о том, что он предлагает в качестве решения обозначенных выше проблем, как это будет выглядеть со стороны программиста, и почему это важно.
История одного языка
Прошло уже больше пяти лет с момента первого стабильного релиза. От изначальной версии Rust, показанной на презентации в 2010 году, уже мало что осталось. Полистав ту презентацию, вы можете легко заметить, что тогда язык был совершенно иным: другой синтаксис, зеленые потоки вместо системных и т.д. Rust сильно изменился во всех аспектах и стал языком программирования общего назначения, который подходит для любых задач. Этот результат был достигнут в основном благодаря открытому и дружелюбному сообществу, создавшему инфраструктуру, статьи, книги, учебные пособия и полезные инструменты. В настоящее время Rust можно применять в любой сфере, независимо от того, является ли проект desktop-приложением, ядром ОС или веб-приложением для облачного сервиса.
Сегодня множество технологических компаний выбирают Rust за его производительность, безопасность и эффективность разработки. Позвольте мне привести несколько примеров:
Apple – технологический гигант, который ставит на первое место производительность и безопасность, решил перевести низкоуровневую часть Apple Cloud Traffic на Rust, а новый функционал, по возможности, в первую очередь писать на нем.
Amazon – выбрал Rust для реализации Firecracker – проекта безопасных и быстрых microVM для бессерверных вычислений.
Microsoft – также активно исследует возможность перехода на Rust для низкоуровневых и чувствительных к производительности компонентов. Они уже попробовали в качестве эксперимента переписать некоторые низкоуровневые системные компоненты Windows.
Google – разрешает использовать Rust (кроме kernel части) для реализации своей новой операционной системы Fuchsia.
Huawei – также исследует возможности использовать язык Rust в своих будущих проектах.
Список можно продолжить и далее: Dropbox, Facebook, Bitbucket, Discord и т.д. – все эти известные компании начали использовать Rust в частях своих серверных платформ. Обратите внимание, что не только крупные международные компании используют Rust – в нем заинтересованы и стартапы. Например, новая компания, основанная Брайаном Кантриллом (изначальный разработчик dtrace), планирует использовать Rust в качестве основного языка для реализации своего проекта. Немаловажно для всех этих компаний то, что разработчики Rust принимают решения, связанные с дизайном языка, руководствуясь обратной совместимостью и стабильностью кодовой базы.
Но, несмотря на все успехи Rust, колыбель языка Mozilla чувствовала себя не очень хорошо и, в связи с финансовыми трудностями, в 2020 году приступила к отделению проекта в независимую организацию Rust Foundation, серьезно уменьшив свое влияние на развитие языка и отпустив его в свободное плавание. Изначальный список членов организации внушает оптимизм за будущее языка:
Как вы можете видеть, в данный момент Rust является одним из самых любимых и перспективных языков программирования. Было бы ошибкой для программистов просто игнорировать все эти факты и ни разу не попробовать данный язык в деле.
Но что делает Rust таким желанным для многих разработчиков? Давайте немного углубимся в детали и изучим некоторые важные и полезные аспекты языка.
Приоритет безопасности
Если мы хотим создать приложения с минимумом уязвимостей, мы должны обратить пристальное внимание на операции управления памятью. Нарушение безопасности памяти приводит к неожиданному сбою программ и может быть использовано для несанкционированного доступа к данным пользователя. В результате пользователь сталкивается с утечкой данных и удаленным выполнением кода. К таким нарушениям относятся:
Использование памяти после освобождения (Use After Free)
Разыменование нулевого указателя (Null Dereference)
Использование неинициализированной памяти (Using uninitialized variables)
Двойное освобождение (Double Free)
Переполнение буфера (Buffer Overflow)
MSRC Security Research 2019 года (Microsoft) показал, что 70% Common Vulnerabilities and Exposures (CVEs) в их продуктах были связаны с проблемами безопасности памяти в С и С++ (думаю весьма известный график):
Кто-то может сказать, что данный отчет лишь показывает проблемы Microsoft, так как их продукты являются проприетарным программным обеспечением, тогда как продукт с открытым исходным кодом благодаря контролю «тысячи глаз» будет иметь значительно меньше проблем. На самом деле не все так радужно. Взгляните на CVEs за 2019 год (последний из доступных на данном ресурсе), найденные в ядре Linux, разрабатываемом и проверяемом ведущими отраслевыми экспертами:
Как мы видим, open-source сообщество допустили достаточно большое количество уязвимостей, связанных с безопасностью работы с памятью (примерно 65%). Я уверен, вы знаете, что такого рода проблемы тяжело найти и зачастую они проходят мимо глаз. Однако в Rust эти проблемы не могут возникнуть, как говорится, by design (если конечно не использовать unsafe, но это тема для отдельного разговора). И все это достигается на этапе компиляции!
Для этих целей Rust имеет такие элементы языка, как Ownership и Borrowing. Система Ownership является основой для всего языка. Rust гарантирует, что у любого заданного объекта на все время его жизни существует ровно один владелец:
Кроме того, передача объекта в качестве аргумента функции также приведет к передаче права собственности на область действия аргумента функции:
Одним из дополнительных преимуществ Rust является диагностика ошибок в базовом компиляторе. Я уверен, что вам понравятся такие дружелюбные и полезные сообщения об ошибках. В Rust каждое сообщение:
Объясняет, почему проблема произошла;
Выделяет все элементы, которые вызвали проблему;
Дает инструкции о том, как получить больше информации об ошибке, и даже предлагает возможный вариант исправления.
Для примера, посмотрите на это подробное сообщение об ошибке:
Хорошо, а когда освободится выделенная память? В Rust нету GC, поэтому волшебный runtime не придет на помощь. В таких языках как C/C++ разработчик обязан самостоятельно определять, когда пора возвратить память ОС. Данная задача не из тривиальных и требует достаточно высокую квалификацию в сложных проектах. Rust решает эту проблему по-своему: он позволяет хранить данные как в стеке, так и в куче, а память автоматически возвращается, как только переменная, которой она принадлежит, уходит за пределы области видимости:
В Rust можно также заимствовать (borrow) объект, используя ссылку, чтобы избежать излишние изменения владельца и копирования. Однако для этого существуют некоторые строгие ограничения, процитирую:
В любой момент времени вы можете иметь либо одну изменяемую ссылку, либо любое количество неизменяемых ссылок.
Ссылки всегда должны быть актуальными.
Для этой цели Rust отслеживает время жизни (lifetime) для каждой ссылки. Чаще всего времена жизни объектов генерируются автоматически, но в некоторых случаях мы должны аннотировать их вручную, используя специальный синтаксис языка. Это позволяет эффективно управлять памятью без оверхеда от использования GC. Пример заимствования:
А вот так мы можем нарушить озвученные выше ограничения. Но тут приходит borrow checker, и выписывает нам ошибку:
В данном примере мы нарушаем одно из правил языка – в один момент времени разрешена только одна изменяемая ссылка. В этом случае мы получаем следующее сообщение:
Разделяет переменные на изменяемые и неизменяемые.
Каждая переменная является неизменяемой по умолчанию и обязательно должна быть проинициализирована актуальным для данного типа значением (например, определенным значением enum).
Не позволяет выполнять неявное приведение типов даже для примитивов.
Проверка границ массивов (небольшая цена производительности для исключения более серьезных проблем).
В debug-режиме компилируется с проверкой integer overflow.
И под конец главы добавлю, что сами разработчики Rust не только о себе думают, но и активно участвуют в улучшении безопасности проекта LLVM. Например в этом году они в тесной коллаборации с LLVM добавили поддержку защиты от Stack Clash в LLVM/Clang.
Многопоточность без гонок
Последние поколения процессоров склонны наращивать количество ядер. Еще несколько лет назад максимальное количество ядер в корпоративных процессорах составляло 28, но не так давно получили распространение процессоры AMD Epyc и Kunpeng, которые подняли планку до 64 ядер! Это огромная мощность, которую очень трудно использовать эффективно. Максимальное использование ресурсов таких CPUs приводит к тому, что алгоритмы и приложения становятся все более сложными. Исторически, программирование в этом контексте всегда было трудоемким и подвержено ошибкам, поэтому разработчикам нужны инструменты, которые помогут им реализовать свои идеи без головной боли.
Если разработчик попытается переместить объект, не являющийся объектом Send, в другой поток, используя захват переменной замыканием, компилятор Rust выведет нечто подобное:
В Rust наличие изменяемого доступа к объекту гарантирует атомарность его обновления, так как ни один другой поток не может иметь параллельный доступ для чтения. На этом этапе компилятор Rust предотвращает все потенциальные состояния гонки данных во время компиляции, однако результатом такого подхода является увеличение количества неудобств при разработки приложений с многопоточной обработкой, особенно если пытаться обойти unsafe операции. Например, исследователи столкнулись со сложностями при безопасном распараллеливании алгоритмов своими руками, и в итоге решили воспользоваться помощью готовой библиотеки.
Подводя итог, в Rust вам не нужно беспокоиться, что вы где-то забыли защитить данные или обратились неправильно к общим объектам, так как компилятор скажет и укажет вам на все небезопасные операции. Это стало возможным благодаря системе Ownership и грамотной архитектуре ограничения передачи объектов между потоками.
One Tool to Rule Them All
Разработчики Rust знают об этой ситуации и понимают, что они должны обязательно предложить нечто лучшее, чтобы обеспечить все необходимые функции и уменьшить трату времени на задачи, которые не связаны непосредственно с разработкой продукта. Результат их работы они назвали Cargo. Cargo поставляется вместе с Rust и используется почти в каждом проекте (хотя без него вы все еще можете использовать компилятор rustc, как любой другой компилятор C/C++). Этот менеджер берет все процедуры обслуживания на себя и помогает вам создавать проекты, управлять зависимостями, собирать приложение, создавать документацию и тестировать проект.
Итак, давайте посмотрим, как это выглядит. Во-первых, нам нужно создать наш новый замечательный проект, просто введя следующую команду:
Cargo создаст каталог со следующими файлами:
Как мы видим, Cargo создал git репозиторий, каталог с исходниками, TOML-файл, содержащий конфигурацию проекта, и некоторые служебные файлы. Самый интересный файл TOML. Здесь хранятся все настройки проекта, включая имя проекта, версию, список зависимостей, флаги для компилятора и другие классные вещи. Его легко читать и редактировать, а так же централизовано управлять релизами. Пример содержания файла после инициализации:
Далее, давайте напишем простенькую программу:
Это все, что требуется от разработчика, чтобы собрать проект со всеми зависимостями. И, самое главное, при росте проекта сложность управления не сильно изменится благодаря продуманной структуре проекта и инструментов. Cargo проверяет все изменения, сканирует зависимости и обновляет их при необходимости, используя для конфигурации TOML файл.
Тестирование
Готово! Теперь запускаем:
С помощью атрибута #[cfg(test)] можно создать отдельный подмодуль со всеми нужными вспомогательными функциями для тестов который будут использоваться только во время тестирования.
Документация
И собираем документацию одной простой командой:
В результате мы получаем HTML-страницы с гиперссылками, подсветкой синтаксиса и поисковой системой:
Внешние зависимости
В Rust единица компиляции называется «crate» и может быть либо библиотекой, либо исполняемой программой.
И вызываем команду сборки проекта:
