Rust | |
---|---|
Класс языка | процедурный язык программирования, язык функционального программирования, мультипарадигмальный, императивный, язык системного программирования[вд], свободное и открытое программное обеспечение, компилируемый язык программирования и язык программирования |
Появился в | 2006[1][5] |
Автор | Грэйдон Хор[вд] |
Разработчик | Mozilla[1], Грэйдон Хор[вд][1][2] и Rust Foundation[вд][3] |
Расширение файлов |
.rs |
Выпуск |
|
Испытал влияние | Alef[вд][6], C++[6], C#[6], Cyclone[6], Erlang[6], Haskell[6], Limbo[6], Newsqueak[вд][6], OCaml[6], Ruby[6], Scheme[6], SML[6] и Swift[6] |
Лицензия | Apache License 2.0[7][8] и лицензия MIT[7][8] |
Сайт | rust-lang.org (англ.) |
Платформа | Windows, Linux, macOS, FreeBSD, iOS, Android, кроссплатформенность и WebAssembly |
Медиафайлы на Викискладе |
Rust (Раст, [rʌst]; rust с англ. — «ржавчина») — мультипарадигменный компилируемый язык программирования общего назначения, сочетающий парадигмы функционального и процедурного программирования с объектной системой, основанной на типажах. Управление памятью осуществляется через механизм «владения» с использованием аффинных типов[англ.][9], что позволяет обходиться без системы сборки мусора во время исполнения программы. Rust гарантирует безопасную работу с памятью благодаря встроенной в компилятор системе статической проверки ссылок (borrow checker). Имеются средства, позволяющие использовать приёмы объектно-ориентированного программирования[10].
Ключевые приоритеты языка: безопасность, скорость и параллелизм. Rust пригоден для системного программирования, в частности, он рассматривается как перспективный язык для разработки ядер операционных систем[9]. Rust сопоставим по скорости и возможностям с C++/Си, однако даёт большую безопасность при работе с памятью, что обеспечивается встроенными в язык механизмами контроля ссылок. Производительности программ на Rust способствует использование «абстракций с нулевой стоимостью»[11].
После нескольких лет активной разработки первая стабильная версия (1.0) вышла 15 мая 2015 года, после чего новые версии выходят раз в 6 недель[12]. Для версий языка, вышедших после 1.0, заявлена обратная совместимость[13].
Разрабатывается с 2010-х годов сообществом Mozilla Research и финансировался фондом Mozilla Foundation. С 2020 года планировалась передача интеллектуальной собственности и процессов развития и финансирования языка в организацию Rust Foundation[14]. 8 февраля 2021 года пять компаний-учредителей (AWS, Huawei, Google, Microsoft и Mozilla) официально объявили о создании Rust Foundation[15][16].
Восемь лет подряд с 2016 по 2023 год Rust занимает первое место в списке самых любимых языков программирования («Most loved programming languages» / «Most admired programming languages») по версии ежегодного опроса разработчиков Stack Overflow Developer Survey[17][18][19][20][21].
Работа над языком была начата сотрудником Mozilla Грэйдоном Хором в 2006 году. Автор дал проекту название Rust, по его словам, связанное с грибами семейства ржавчинные (англ. rust fungi)[22].
В 2009 году[23] компания Mozilla начала отдельно спонсировать разработку Rust. Спустя год язык был официально представлен на Mozilla Summit 2010[24]. Изначальный компилятор, реализованный на OCaml, был заменён на новый, написанный на Rust и использовавший LLVM для генерации машинного кода[25]; в следующем году новый компилятор впервые успешно скомпилировал сам себя[26].
Первая официальная альфа-версия Rust (0.1) была выпущена в январе 2012 года[27].
В апреле 2013 года был запущен Servo — экспериментальный проект компании Mozilla по разработке браузерного движка на Rust.[28]
Первая стабильная версия Rust (1.0) вышла в мае 2015 года. Программные интерфейсы и возможности языка подверглись значительной ревизии, после которой по умолчанию оставлены только полностью готовые к применению возможности, реализация которых не будет изменяться в дальнейшем. Все остальные функции переведены в разряд экспериментальных и вынесены из поставки по умолчанию[29].
В декабре 2022 года Rust стал первым языком, кроме C и ассемблера, который поддерживается при разработке ядра Linux[30].
Microsoft переписывает отдельные элементы собственной экосистемы программных продуктов на языке программирования Rust; так, в 2024 г. фундаментальный серверный компонент, обеспечивающий работу набора облачных сервисов Microsoft 365 будет переписан на этом языке[31].
Используется сильная статическая типизация. Поддерживается обобщённое программирование с поддержкой параметрического полиморфизма, обеспечивается автоматический вывод типов для локальных переменных (но не для параметров функций).
Реализована поддержка единичных типов[англ.] данных — типов, которые имеют ровно один экземпляр и не занимают места в памяти, примеры:
()
;[u8; 0]
);struct Foo
);struct Foo([u8; 0], ())
).Реализованы пустые типы[англ.] данных — типы, экземпляры которых не могут быть созданы; реализованы в виде перечисляемых типов, не имеющих вариантов: enum Void {}
.
Все типы данных в языке делятся на две основные группы: простые и типы стандартной библиотеки.
Простые типы (типы постоянной длины, встроенные в сам язык) — числовой, булев, символьный, массив, срез, строковый срез, кортеж, ссылка, указатель на функцию. Часть простых типов является «машинной», то есть реализуются непосредственно в современных процессорах, таковы числовой, булев и символьный. Типы, предоставляемые стандартной библиотекой std
(переменной длины): вектор, строка, хеш-таблица и им подобные.
Числовые типы:
-5i8
, 0x400_u16
, 0o100i16
, 20_922_789_888u64
, b'*'
(байтовый литерал), b'\x1b'
, 42
(тип этого значения будет выведен автоматически), 0xfff_fc00usize
3.14f32
, 6.0221e23f64
, 2.
,Булев (bool): true
, false
.
Символьный (char): тип, представляющий символ Unicode (внутреннее представление данных как u32). Примеры значений: '₽'
, '\n'
, '\x7f'
, '\u{CA0}'
,
Указатель на функцию (function pointer): объекты-функции имеют тип, определяемый их сигнатурой, то есть параметрами и возвращаемым значением. Пример: let f: fn(i32) -> i32 = plus_one;
Ссылка (разделяемое заимствование — shared borrow) &T
(разделяемая, не изменяемая, не владеющая ресурсом), вместо того, чтобы забирать владение ресурсом, она его заимствует. Имена, которые заимствуют что-то, не освобождают ресурс, когда они выходят из области видимости. Кроме того, имена-владельцы переходят в заимствованное состояние.
Ссылка изменяемая (изменяемое заимствование — mutable borrow) &mut T
(не владеющая ресурсом). Позволяет изменять ресурс, который заимствуется.
Структуры (struct):
struct Color {red: u8, green: u8, blue: u8}
struct Color (u8, u8, u8);
struct Electron;
[1, 2, 3]
, [true; 10000]
.vec![0; 10];
, Vec::with_capacity(10)
&a[1..4]
,("Age", 22)
, ("Europe",)
,HashMap::new();
Vec<u8>
) — тип, владеющий содержимым. String представляет собой строку, размещённую в куче. Эта строка расширяема, и она гарантированно является корректной последовательностью байтов с точки зрения UTF-8. String обычно создаётся путём преобразования из строкового среза с использованием метода to_string. Примеры: "строковый срез".to_string()
, String::new()
."строковый срез"
. &'static str — строка, введённая символами в коде самой программы, — тот же строковый срез, только статически размещённый (сохраняемый в скомпилированной программе).
r"\d{0,5}.*"
.b"white"
.Перечисление (enum): каждый вариант в перечислении в Rust может быть также связан с другими данными, благодаря чему перечисление называют также tagged union или типом-суммой. Синтаксис для объявления вариантов схож с синтаксисом для объявления структур: могут быть варианты без данных, варианты с именованными данными и варианты с безымянными данными:
enum Day {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}
enum Foo {Bar = 123,}
.enum Message {Quit, Move { x: i32, y: i32 }}
.enum Message {Quit, Size(i32)}
.const
— постоянные. Живут в течение всего времени работы программы. А именно, у них вообще нет определённого адреса в памяти. Это потому, что они встраиваются (inline) в каждое место, где есть их использование,static
— значение с возможностью изменения, имеющее время жизни 'static
. Похожи на постоянные, но статические значения не встраиваются в место их использования. Это значит, что каждое значение существует в единственном экземпляре, и у него есть определённый адрес. Также может быть изменяемым, при помощи ключевого слова mut
. Изменения возможны только в unsafe
блоках.При выборе следует отдавать предпочтение const
, так как зачастую для константы не нужен конкретный адрес в памяти и const
позволяет делать оптимизации вроде свёртки констант.
В языке реализована модель управления памятью, ориентированная на безопасные шаблоны параллельного выполнения, препятствующая некорректному доступу к памяти, который обычно является источником критических ошибок сегментации в других языках программирования. Обеспечивается контроль над использованием неинициализированных и деинициализированных переменных; невозможно совместное использование разделяемых состояний несколькими задачами; обеспечивается статический анализ времени жизни указателей и проверка на выход за пределы массива (автоматически и всегда, но доступно отключение проверки в unsafe
-блоках с помощью метода get_unchecked
).
Реализована так называемая Move-семантика: по умолчанию Rust «переносит» (move) указатель на объект в куче новому владельцу при присваивании, делая старую переменную недействительной. Этого не происходит, если тип реализует типаж Copy, поскольку данные в стеке копируются.
let a = "объект с данными в куче".to_string();
// объект передан переменной b
// переменная a становится неинициализированной
let b = a;
// ошибка!
let c = a;
// данные объекта на стеке
let a = 55;
// копия объекта передана переменной b
let b = a;
// c = 55
let c = a;
Ещё одна особенность модели памяти — поддержка заимствований (borrow) с возможностью изменения заимствованного объекта (&mut
) и без таковой (&
): Лексически и семантически очень схожи со ссылками, но имеют специфику: заимствование объекта сходно семантике «Либо много читателей, либо один писатель» — объект можно передать в заимствование либо однократно с возможностью изменения объекта, либо многократно без таковой; заимствования можно перезаимствовать другому заёмщику. В отличие от обычной семантики «Либо много читателей, либо один писатель», применяется не в контексте синхронизации потоков, а универсально. Контроль корректности заимствований происходит во время компиляции и не порождает дополнительного исполнимого кода (принцип абстракций с нулевой стоимостью). Компилятором контролируется также соотношение времён жизни заимствований и самого объекта — заимствования не могут жить дольше (выходить за пределы области видимости) заимствованного объекта. Заимствования работают с любыми данными независимо от их размещения (стек, локальная или обменная куча, другие специальные расположения). Следует различать независимые понятия — изменяемость собственно заимствования (let mut b = &c
) и изменяемость заимствованного объекта (let b = &mut c
).
Упаковка (Box) — «умный» указатель, владеющий объектом в куче, уничтожает объект и освобождает память при выходе из области видимости.
Ячейка (Cell, RefCell) реализует изменяемость содержимого при неизменяемости самой ячейки.
Указатели со счётчиком ссылок (Rc<T>
) и с атомарным счётчиком ссылок (Arc<T>
): Умные указатели с подсчётом ссылок, уничтожающие объект и освобождающие память при обнулении счётчика. Arc реализует потокобезопасность для счётчика ссылок (но не для самого объекта). Rc и Arc контролируют неизменяемый объект, поэтому типичное их использование выглядит как Rc<Cell<T>>
в однопоточной программе и Arc<Mutex<T>>
в многопоточной.
«Сырые» указатели неизменяемые (*const T
) и изменяемые (*mut T
): Указатели без гарантии безопасности. Настоятельно не рекомендуется их использовать.
Связывания неизменяемы по умолчанию, а чтобы объявить переменную изменяемой, необходимо ключевое слово mut.
Примеры:
let x = 80; // связывание владельца x со значением 80
let mut y = 50; // изменяемое связывание
let z = &x; // неизменяемая ссылка на неизменяемое связывание
let w = &mut y; // неизменяемая ссылка на изменяемое связывание
let r = &mut y; // ошибка: нельзя создавать вторую ссылку на изменяемое связывание
*w = 90 // y = 90
*z = 30 // ошибка: попытка изменения через ссылку на неизменяемое связывание
let n = Box::new(42); // упаковка
let m = Rc::new(55); // счётчик ссылок
let data = Arc::new("test_string") // атомарный счётчик
В своей докторской диссертации Ральф Юнг (англ. Ralph Jung) формально доказал потокобезопасность и безопасность управления памятью, использовав логику разделения в созданной им модели RustBelt и инструменте Iris (основанном на Coq)[32].
Синтаксис языка похож на Си и C++; язык регистрозависимый, блоки кода ограничиваются фигурными скобками; используются стандартные наименования управляющих конструкций if, else, while, и for; комментарии также пишутся в С-формате; имена модулей разделяются двумя символами двоеточия (::
). Идентификаторы могут содержать латинские буквы, цифры и знак подчёркивания. В строковых литералах допускается использование любых символов unicode в кодировке UTF-8.
Набор операторов в Rust: арифметические (*
— умножение, /
— деление, %
— взятие остатка от деления, +
— сложение, -
— вычитание и унарный префиксный оператор -
для смены знака числа), битовые (>>
, <<
, &
, |
и ^
), операторы сравнения (==
, !=
, <
, >
, <=
, >=
), логические (&&
и ||
). Для приведения типов в Rust используется бинарный оператор as
. Неявное приведение типов происходит в очень небольшом наборе ситуаций[33].
Rust поддерживает макроопределения — средства подстановки с использованием регулярных выражений, выполняющиеся во время этапа подготовки к компиляции, более развитые и безопасные, чем в Си. Макроопределения (макрокоманды) — это определяемые пользователем простые расширения синтаксиса, выполняемые с помощью команды macro_rules!
Макрокоманды определяются в том же стиле, что и конструкция сопоставления с образцом. Признак макроса — восклицательный знак в конце имени. Также поддерживаются так называемые «процедурные» макроопределения[34], имеющие возможность исполнять произвольный код во время компиляции.
Ключевое слово let
определяет связывание (локальную переменную).
let x: i32 = 5;
Данная запись обозначает: «x
— это связывание типа i32
(32-битное целое) со значением пять».
В языке конструкция match представляет собой обобщённую и усовершенствованную версию конструкции switch языка C. Более того, match является самым мощным, универсальным и, можно даже сказать, ключевым элементом управления не только потоком выполнения, но и структурами данных в языке. В выражениях match можно сопоставлять несколько шаблонов, используя синтаксис |
, что означает логическое или.
let x = 10;
match x {
1 | 2 => println!("один или два"),
3 => println!("три"),
4..=10 => println!("от четырёх до десяти"), // Отработает эта ветка, ведь 10 принадлежит данному диапазону.
_ => println!("что угодно, не соответствующее условиям выше"), // "_" соответствует любому значению
}
При работе с составными типами данных (структура, перечисление, кортеж, массив) можно разобрать их на части («деструктурировать») внутри шаблона. Деструктуризация структуры:
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 0, y: 0 };
match point {
Point { x: 0, y } => println!("x - ноль, y равен {}", y), // так как "x" равен нулю, отработает эта ветка.
Point { x, y: 0 } => println!("x равен {}, y - ноль", x),
Point { x, y } => println!("x = {}, y = {}", x, y),
}
Деструктуризация перечисления:
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
let color = Color::Hsv(0, 0, 100);
match color {
Color::Rgb(0, 0, 0) | Color::Hsv(0, 0, 0) => println!("чёрный"),
Color::Rgb(255, 255, 255) | Color::Hsv(0, 0, 100) => println!("белый"), // отработает эта ветка.
Color::Rgb(red, green, blue) => {
println!("красный: {}, зелёный: {}, синий: {}", red, green, blue)
} // отработает при любых значениях Rgb, которые не соответствуют условиям выше.
Color::Hsv(hue, saturation, brightness) => println!(
"тон: {}, насыщенность: {}, яркость: {}",
hue, saturation, brightness
), // то же самое, только с Hsv.
}
Деструктуризация кортежа:
let (a, b) = (1, 2);
println!("{}", a); // 1
println!("{}", b); // 2
Синтаксис if let
позволяет скомбинировать if
и let
в менее многословную конструкцию, и затем обработать значения соответствующе только одному шаблону, одновременно игнорируя все остальные. Данный синтаксис уместно использовать, когда нужно сопоставить только один шаблон.
let x = Some(10);
if let Some(value) = x {
// здесь мы деструктурируем x, переменная value хранит значение 10.
// выполнится эта ветка, так как "x" хранит внутри значение.
println!("значение = {}", value);
} else {
// оператор "else" здесь выступает заменой "_" в выражениях match.
println!("x - пуст");
}
В блоках и функциях, помеченных unsafe
(unsafe с англ. — «небезопасный»), компилятор разрешает делать лишь пять дополнительных вещей:
static mut
) переменные;unsafe
) функции;union
.К unsafe
приходится прибегать при создании низкоуровневых абстракций, в частности — при разработке стандартной библиотеки Rust; обычный код рекомендуется писать без unsafe
.
В Rust объектная система основана на типажах (traits) и структурах (structs). Типажи определяют сигнатуры методов, которые должны быть реализованы для каждого типа (чаще всего — структуры), реализующего типаж. Типаж может содержать и реализации методов, принимаемые по умолчанию. Реализация типажей для данной структуры, а также реализация собственных методов структуры обозначается ключевым словом impl
. Язык содержит несколько десятков встроенных типажей, большая часть которых используется для перегрузки операторов, а некоторые имеют специальное значение.
Rust поддерживает аналогию наследования типажей — типаж может требовать от реализующего типа реализацию других типажей. Однако языковой поддержки наследования самих типов, и следовательно, классического ООП, в Rust нет. Вместо наследования типов, аналогия иерархии классов реализуется введением типажей, включением структуры-предка в структуру-потомка или введением перечислений для обобщения разных структур[35].
Язык поддерживает обобщённые типы (generics). Помимо функций, обобщёнными в Rust могут быть комплексные типы данных, структуры и перечисления. Компилятор Rust компилирует обобщённые функции весьма эффективно, применяя к ним мономорфизацию (генерация отдельной копии каждой обобщённой функции непосредственно в каждой точке её вызова). Таким образом, копия может быть адаптирована под конкретные типы аргументов, а следовательно, и оптимизирована для этих типов. В этом отношении обобщённые функции Rust сравнимы по производительности с шаблонами языка C++.
В более ранних версиях языка поддерживались легковесные потоки, но потом от них отказались в пользу нативных потоков операционной системы. При этом рекомендуемым методом обмена данными между потоками является отправка сообщений, а не использование общей памяти. Для достижения высокой производительности возможно отправлять данные не через копирование, а используя собственные указатели (Box<T>
). Они гарантируют наличие только одного владельца.
Определение и вызов асинхронных операций поддерживаются на уровне синтаксиса языка: ключевое слово async
определяет асинхронную функцию или блок; обычный вызов такой функции возвращает объект с типажом Future
— дескриптор ленивой асинхронной операции[36]. Вызов .await
позволяет одной асинхронной операции ждать, пока не завершится другая асинхронная операция. При этом реализация среды исполнения асинхронных операций не входит ни в ядро языка, ни в стандартную библиотеку, а предоставляется сторонними библиотеками[37].
Система модулей: единица компиляции («крейт») может состоять из нескольких модулей. Иерархия модулей, как правило, совпадает с иерархией каталогов и файлов проекта. Модуль (как правило) является отдельным файлом, а также является пространством имён и одним из средств управления видимостью идентификаторов: в пределах модуля (и в подмодулях) «видны» все идентификаторы, в вышестоящих модулях видны только публичные (pub
) функции, типы, типажи, константы, подмодули, поля структур.
Автоматизированное тестирование: язык даёт возможность реализовать автоматизированные модульные тесты (юнит-тесты) прямо в тестируемом модуле либо подмодуле. Тестовые методы при компиляции игнорируются и вызываются только при тестировании. Интеграционные тесты реализуются как отдельные крейты в каталоге tests
.
Автоматизированное документирование: средство rustdoc позволяет генерировать HTML-документацию прямо из исходного кода. Документация в коде маркируется тройным слешем (/// Пример документации
) либо двойным с восклицательным знаком, для документации модулей — (//! Пример документации модуля
). Поддерживается язык разметки Markdown. В документацию может быть встроен код, который является запускаемым (документационные тесты). Это позволяет, в том числе, проверять актуальность документации при внесении изменений в проект.
Система управления пакетами: менеджер пакетов cargo (являющийся также основным инструментом создания, компиляции и тестирования проектов) с помощью файла манифеста Cargo.toml разрешает зависимости проекта (импортируемые крейты), загружая их из репозитория crates.io.
Требования к идентификаторам: компилятор контролирует выполнение соглашений об именовании переменных, типов, функций и так далее (snake_case, UpperCamelCase, SCREAMING_SNAKE_CASE), а также неиспользуемые идентификаторы; неиспользуемые идентификаторы рекомендуется начинать со знака подчёркивания; есть определённые рекомендации по именованию конструкторов, методов преобразования типов и др.[38]
fn main() {
println!("Hello, world!");
}
fn declension_of_noun(count: u8) -> &'static str {
let remainder = count % 10;
// исключения из правил
if count >= 11 && count <= 14 {
return "бутылок";
}
match remainder {
1 => return "бутылка",
2..=4 => return "бутылки",
_ => return "бутылок",
}
}
fn main() {
let mut word = declension_of_noun(99);
for i in (2..=99).rev() {
println!("{} {} пива на стене", i, word);
println!("{} {} пива!", i, word);
println!("Возьми одну, пусти по кругу");
word = declension_of_noun(i - 1);
println!("{} {} пива на стене!\n", i - 1, word);
}
println!("1 бутылка пива на стене");
println!("1 бутылка пива!");
println!("Возьми одну, пусти по кругу");
println!("Нет больше бутылок пива на стене!\n");
println!("Нет бутылок пива на стене!");
println!("Нет бутылок пива!");
println!("Пойди в магазин и купи ещё");
println!("99 бутылок пива на стене!");
}
Принципы работы с памятью Rust ощутимо отличаются как от языков с полным доступом к памяти, так и от языков с полным контролем за памятью со стороны сборщика мусора. Модель памяти Rust построена таким образом, что, с одной стороны, предоставляет разработчику возможность контролировать, где размещать данные, вводя разделение по типам указателей и обеспечивая контроль за их использованием на этапе компиляции. C другой стороны, механизм подсчёта ссылок Rust старается выдавать ошибки компиляции в тех случаях, в которых использование прочих языков приводит к ошибкам времени выполнения или аварийному завершению программ.
Язык позволяет объявлять функции и блоки кода как «небезопасные» (unsafe
). В области такого небезопасного кода не применяются некоторые ограничения, таким образом можно выполнять операции на более низком уровне, но разработчик должен полностью понимать, что он делает.