From Wikipedia (Ru) - Reading time: 6 min
| 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[9], Linux[9], macOS[9], FreeBSD[9], iOS[9], Android[9], кроссплатформенность[9] и WebAssembly[9] |
Rust (Раст, [rʌst]; rust с англ. — «ржавчина») — мультипарадигменный компилируемый язык программирования общего назначения, сочетающий парадигмы функционального и процедурного программирования с объектной системой, основанной на типажах. Управление памятью осуществляется через механизм «владения» с использованием аффинных типов[англ.][10], что позволяет обходиться без системы сборки мусора во время исполнения программы. Rust гарантирует безопасную работу с памятью благодаря встроенной в компилятор системе статической проверки ссылок (borrow checker). Имеются средства, позволяющие использовать приёмы объектно-ориентированного программирования[11].
Ключевые приоритеты языка: безопасность, скорость и параллелизм. Rust пригоден для системного программирования, в частности, он рассматривается как перспективный язык для разработки ядер операционных систем[10]. Rust сопоставим по скорости и возможностям с Си/C++, однако даёт большую безопасность при работе с памятью, что обеспечивается встроенными в язык механизмами контроля ссылок. Производительности программ на Rust способствует использование «абстракций с нулевой стоимостью»[12].
После нескольких лет активной разработки первая стабильная версия (1.0) вышла 15 мая 2015 года, после чего новые версии выходят раз в 6 недель[13]. Для версий языка, вышедших после 1.0, заявлена обратная совместимость[14].
Разрабатывается с 2010-х годов сообществом Mozilla Research и финансировался фондом Mozilla Foundation. С 2020 года планировалась передача интеллектуальной собственности и процессов развития и финансирования языка в организацию Rust Foundation[15]. 8 февраля 2021 года пять компаний-учредителей (AWS, Huawei, Google, Microsoft и Mozilla) официально объявили о создании Rust Foundation[16][17].
Девять лет подряд с 2016 по 2024 год Rust занимает первое место в списке самых любимых языков программирования («Most loved programming languages» / «Most admired programming languages») по версии ежегодного опроса разработчиков Stack Overflow Developer Survey[18][19][20][21][22][23].
Работа над языком была начата сотрудником Mozilla Грэйдоном Хором в 2006 году. Автор дал проекту название Rust, по его словам, связанное с грибами семейства ржавчинные (англ. rust fungi)[24].
В 2009 году[25] компания Mozilla начала отдельно спонсировать разработку Rust. Спустя год язык был официально представлен на Mozilla Summit 2010[26]. Изначальный компилятор, реализованный на OCaml, был заменён на новый, написанный на Rust и использовавший LLVM для генерации машинного кода[27]; в следующем году новый компилятор впервые успешно скомпилировал сам себя[28].
Первая официальная альфа-версия Rust (0.1) была выпущена в январе 2012 года[29].
В апреле 2013 года был запущен Servo — экспериментальный проект компании Mozilla по разработке браузерного движка на Rust.[30]
Первая стабильная версия Rust (1.0) вышла в мае 2015 года. Программные интерфейсы и возможности языка подверглись значительной ревизии, после которой по умолчанию оставлены только полностью готовые к применению возможности, реализация которых не будет изменяться в дальнейшем. Все остальные функции переведены в разряд экспериментальных и вынесены из поставки по умолчанию[31].
В декабре 2022 года Rust стал первым языком, кроме C и ассемблера, который поддерживается при разработке ядра Linux[32].
Используется сильная статическая типизация. Поддерживается обобщённое программирование с поддержкой параметрического полиморфизма, обеспечивается автоматический вывод типов для локальных переменных (но не для параметров функций).
Реализована поддержка единичных типов[англ.] данных — типов, которые имеют ровно один экземпляр и не занимают места в памяти, примеры:
();[u8; 0]);struct Foo);struct Foo([u8; 0], ())).Реализованы пустые типы данных — типы, экземпляры которых не могут быть созданы; реализованы в виде перечисляемых типов, не имеющих вариантов: enum Void {}.
Все типы данных в языке делятся на две основные группы: простые и типы стандартной библиотеки.
Простые типы (типы постоянной длины, встроенные в сам язык) — числовой, булев, символьный, массив, срез, строковый срез, кортеж, ссылка, указатель на функцию. Часть простых типов является «машинной», то есть реализуются непосредственно в современных процессорах, таковы числовой, булев и символьный. Типы, предоставляемые стандартной библиотекой std (переменной длины): вектор, строка, хеш-таблица и им подобные.
Числовые типы:
-5i8, 0x400_u16, 0o100i16, 20_922_789_888u64, b'*' (байтовый литерал), b'\x1b', 42 (тип этого значения будет выведен автоматически), 0xfff_fc00usize3.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)[33].
Синтаксис языка похож на Си и C++; язык регистрозависимый, блоки кода ограничиваются фигурными скобками; используются стандартные наименования управляющих конструкций if, else, while, и for; комментарии также пишутся в С-формате; имена модулей разделяются двумя символами двоеточия (::). Идентификаторы могут содержать латинские буквы, цифры и знак подчёркивания. В строковых литералах допускается использование любых символов unicode в кодировке UTF-8.
Набор операторов в Rust: арифметические (* — умножение, / — деление, % — взятие остатка от деления, + — сложение, - — вычитание и унарный префиксный оператор - для смены знака числа), битовые (>>, <<, &, | и ^), операторы сравнения (==, !=, <, >, <=, >=), логические (&& и ||). Для приведения типов в Rust используется бинарный оператор as. Неявное приведение типов происходит в очень небольшом наборе ситуаций[34].
Rust поддерживает макроопределения — средства подстановки с использованием регулярных выражений, выполняющиеся во время этапа подготовки к компиляции, более развитые и безопасные, чем в Си. Макроопределения (макрокоманды) — это определяемые пользователем простые расширения синтаксиса, выполняемые с помощью команды macro_rules! Макрокоманды определяются в том же стиле, что и конструкция сопоставления с образцом. Признак макроса — восклицательный знак в конце имени. Также поддерживаются так называемые «процедурные» макроопределения[35], имеющие возможность исполнять произвольный код во время компиляции.
Ключевое слово 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 нет. Вместо наследования типов, аналогия иерархии классов реализуется введением типажей, включением структуры-предка в структуру-потомка или введением перечислений для обобщения разных структур[36].
Язык поддерживает обобщённые типы (generics). Помимо функций, обобщёнными в Rust могут быть комплексные типы данных, структуры и перечисления. Компилятор Rust компилирует обобщённые функции весьма эффективно, применяя к ним мономорфизацию (генерация отдельной копии каждой обобщённой функции непосредственно в каждой точке её вызова). Таким образом, копия может быть адаптирована под конкретные типы аргументов, а следовательно, и оптимизирована для этих типов. В этом отношении обобщённые функции Rust сравнимы по производительности с шаблонами языка C++.
В более ранних версиях языка поддерживались легковесные потоки, но потом от них отказались в пользу нативных потоков операционной системы. При этом рекомендуемым методом обмена данными между потоками является отправка сообщений, а не использование общей памяти. Для достижения высокой производительности возможно отправлять данные не через копирование, а используя собственные указатели (Box<T>). Они гарантируют наличие только одного владельца.
Определение и вызов асинхронных операций поддерживаются на уровне синтаксиса языка: ключевое слово async определяет асинхронную функцию или блок; обычный вызов такой функции возвращает объект с типажом Future — дескриптор ленивой асинхронной операции[37]. Вызов .await позволяет одной асинхронной операции ждать, пока не завершится другая асинхронная операция. При этом реализация среды исполнения асинхронных операций не входит ни в ядро языка, ни в стандартную библиотеку, а предоставляется сторонними библиотеками[38].
Система модулей: единица компиляции («крейт») может состоять из нескольких модулей. Иерархия модулей, как правило, совпадает с иерархией каталогов и файлов проекта. Модуль (как правило) является отдельным файлом, а также является пространством имён и одним из средств управления видимостью идентификаторов: в пределах модуля (и в подмодулях) «видны» все идентификаторы, в вышестоящих модулях видны только публичные (pub) функции, типы, типажи, константы, подмодули, поля структур.
Автоматизированное тестирование: язык даёт возможность реализовать автоматизированные модульные тесты (юнит-тесты) прямо в тестируемом модуле либо подмодуле. Тестовые методы при компиляции игнорируются и вызываются только при тестировании. Интеграционные тесты реализуются как отдельные крейты в каталоге tests.
Автоматизированное документирование: средство rustdoc позволяет генерировать HTML-документацию прямо из исходного кода. Документация в коде маркируется тройным слешем (/// Пример документации) либо двойным с восклицательным знаком, для документации модулей — (//! Пример документации модуля). Поддерживается язык разметки Markdown. В документацию может быть встроен код, который является запускаемым (документационные тесты). Это позволяет, в том числе, проверять актуальность документации при внесении изменений в проект.
Система управления пакетами: менеджер пакетов cargo (являющийся также основным инструментом создания, компиляции и тестирования проектов) с помощью файла манифеста Cargo.toml разрешает зависимости проекта (импортируемые крейты), загружая их из репозитория crates.io.
Требования к идентификаторам: компилятор контролирует выполнение соглашений об именовании переменных, типов, функций и так далее (snake_case, UpperCamelCase, SCREAMING_SNAKE_CASE), а также неиспользуемые идентификаторы; неиспользуемые идентификаторы рекомендуется начинать со знака подчёркивания; есть определённые рекомендации по именованию конструкторов, методов преобразования типов и др.[39]
fn main() {
println!("Hello, world!");
}
fn declension_of_noun(count: u8) -> &'static str {
// исключения из правил
if count >= 11 && count <= 14 {
return "бутылок";
}
let remainder = count % 10;
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). В области такого небезопасного кода не применяются некоторые ограничения, таким образом можно выполнять операции на более низком уровне, но разработчик должен полностью понимать, что он делает.