Hello world!
Представляю вашему вниманию первую часть практического руководства по Rust.
Другой формат, который может показаться вам более удобным.
Руководство основано на Comprehensive Rust — руководстве по Rust
от команды Android
в Google
и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить 😉
В этой части мы рассмотрим следующие темы:
- базовый синтаксис
Rust
: переменные, скалярные и составные типы, перечисления, структуры, ссылки, функции и методы - типы и выведение типов
- конструкции управления потоком выполнения программы: циклы, условия и т.п.
- пользовательские типы: структуры и перечисления
- сопоставление с образцом: деструктуризация перечислений, структур и массивов
Материалы для более глубокого изучения названных тем:
- Книга/учебник по Rust (на русском языке) — главы 1-3, 5 и 6
- rustlings — упражнения 00-03, 07-09
- Rust на примерах (на русском языке) — примеры 1-5, 7-9
- Rust by practice — упражнения 3, 4, 6-8, 15 и 16
Также см. Большую шпаргалку по Rust.
Hello, World
Что такое Rust?
Rust
— это новый язык программирования, релиз первой версии которого состоялся в 2015 году:
Rust
— это статический компилируемый язык (какC++
)
rustc
(компиляторRust
) использует LLVM в качестве бэкэнда
Rust
поддерживает множество платформ и архитектур
- x86, ARM, WebAssembly...
- Linux, Mac, Windows...
Rust
используется для программирования широкого диапазона устройств
- прошивки (firmware) и загрузчики (boot loaders)
- умные телевизоры
- мобильные телефоны
- настольные компьютеры
- серверы
Некоторые преимущества Rust
:
- высокая гибкость
- высокий уровень контроля
- может использоваться для программирования очень низкоуровневых устройств, таких как микроконтроллеры
- не имеет среды выполнения или сборки мусора
- фокус на надежности и безопасности без ущерба для производительности
Hello, World
Рассмотрим простейшую программу на Rust
:
fn main() {
println!("Привет 🌍!");
}
Вот что мы здесь видим:
- функции определяются с помощью
fn
- блоки кода выделяются фигурными скобками
- функция
main()
— это входная точка программы Rust
имеет гигиенические макросы, такие какprintln!()
- строки в
Rust
кодируются в UTF-8 и могут содержать любой символ Юникода
Ремарки:
Rust
очень похож на такие языки, какC
/C++
/Java
. Он является императивным и не изобретает "велосипеды" без крайней необходимостиRust
является современным: полностью поддерживает такие вещи, как Юникод (Unicode)Rust
использует макросы (macros) для ситуаций, когда функция принимает разное количество параметров (не путать с перегрузкой функции (function overloading))- макросы являются "гигиеническими" — они не перехватывают случайно идентификаторы из области видимости, в которой используются. На самом деле, макросы
Rust
только частично являются гигиеническими Rust
является мультипарадигменным языком. Он имеет мощные возможности ООП и включает перечень функциональных концепций
Преимущества Rust
Некоторые уникальные особенности Rust
:
- безопасность памяти во время компиляции — весь класс проблем с памятью предотвращается во время компиляции
- неинициализированные переменные
- двойное освобождение (double-frees)
- использование после освобождения (use-after-free)
- нулевые указатели (
NULL
pointers) - забытые заблокированные мьютексы (mutexes)
- гонки данных между потоками (threads)
- инвалидация итератора
- отсутствие неопределенного поведения во время выполнения — то, что делает инструкция
Rust
, никогда не остается неопределенным
- проверяются границы доступа (index boundaries) к массиву
- переполнение (overflowing) целых чисел приводит к панике или оборачиванию (wrapping)
- современные возможности — столь же выразительные и эргономичные, как в высокоуровневых языках
- перечисления и сопоставление с образцом (matching)
- дженерики (generics)
- интерфейс внешних функций (foreign function interface, FFI) без накладных расходов
- бесплатные абстракции
- отличные ошибки компилятора
- встроенное управление зависимостями
- встроенная поддержка тестирования
- превосходная поддержка протокола языкового сервера (Language Server Protocol)
Песочница
Песочница Rust
предоставляет легкий способ быстро запускать короткие программы Rust
.
Типы и значения
Переменные
Безопасность типов в Rust
обеспечивается за счет статической типизации. Привязки переменных (variable bindings) выполняются с помощью let
:
fn main() {
let x: i32 = 10;
println!("x: {x}");
// x = 20;
// println!("x: {x}");
}
- Раскомментируйте
x = 20
, чтобы увидеть, что переменные по умолчанию являются иммутабельными (неизменными/неизменяемыми). Добавьте ключевое словоmut
послеlet
, чтобы сделать переменную мутабельной i32
— это тип переменной. Тип переменной должен быть известен во время компиляции, но выведение типов (рассматриваемое позже) позволяет разработчикам опускать типы во многих случаях
Значения
Вот некоторые базовые встроенные типы и синтаксис литеральных значений каждого типа:
Типы | Литералы | |
---|---|---|
Целые числа со знаком | i8, i16, i32, i64, i128, isize | -10, 0, 1_000, 123_i64 |
Целые числа без знака | u8, u16, u32, u64, u128, usize | 0, 123, 10_u16 |
Числа с плавающей точкой | f32, f64 | 3.14, -10.0e20, 2_f32 |
Скалярные значения Юникода | char | 'a', 'α', '∞' |
Логические значения | bool | true,false |
Типы имеют следующие размеры:
iN
,uN
иfN
—N
бит
isize
иusize
— размер указателя
char
— 32 бита
bool
— 8 бит
Нижние подчеркивания предназначены для улучшения читаемости, поэтому их можно не писать, т.е.
1_000
можно записать как1000
(или10_00
), а123_i64
можно записать как123i64
Арифметика
fn interproduct(a: i32, b: i32, c: i32) -> i32 {
return a * b + b * c + c * a;
}
fn main() {
println!("результат: {}", interproduct(120, 100, 248));
}
В арифметике Rust
нет ничего особенного по сравнению с другими языками программирования, за исключением определения поведения при переполнении целых чисел: при сборке для разработки программа запаникует, а при релизной сборке переполнение будет обернуто (wrapped). Кроме переполнения, существует также насыщение (saturating) и каррирование (carrying), которые обеспечиваются соответствующими методами, например, (a * b).saturating_add(b * c).saturating_add(c * a)
.
Строки
В Rust
существует 2 типа для представления строк, оба будут подробно рассмотрены позже. Оба типа всегда хранят закодированные в UTF-8
строки.
String
— модифицируемая, собственная (owned) строка&str
— строка, доступная только для чтения. Строковые литералы имеют этот тип
fn main() {
let greeting: &str = "Привет";
let planet: &str = "🪐";
let mut sentence = String::new();
sentence.push_str(greeting);
sentence.push_str(", ");
sentence.push_str(planet);
println!("итоговое предложение: {}", sentence);
println!("{:?}", &sentence[0..5]);
//println!("{:?}", &sentence[12..13]);
}
Ремарки:
- поведение при наличии в строке невалидных символов
UTF-8
вRust
является неопределенным, поэтому использование таких символов может привести к панике String
— это пользовательский тип с конструктором (::new()
) и методами вродеpush_str()
&
в&str
является индикатором того, что это ссылка. Мы поговорим о ссылках позже, пока думайте о&str
как о строках, доступных только для чтения- закомментированная строка представляет собой индексирование строки по позициям байт.
12..13
не попадают в границы (boundaries) символа, поэтому программа паникует. Измените диапазон на основе сообщения об ошибке - сырые (raw) строки позволяют создавать
&str
с автоматическим экранированием специальных символов:r"\n" == "\\n"
. Двойные кавычки можно вставить, обернув строку в одинаковое количество#
с обеих сторон:
fn main() {
// Сырая строка
println!(r#"<a href="link.html">ссылка</a>"#); // "<a href="link.html">ссылка</a>"
// Экранирование
println!("<a href=\"link.html\">ссылка</a>"); // <a href="link.html">ссылка</a>
}
Выведение типов
Для определения/выведения типа переменной Rust
"смотрит" на то, как она используется:
fn takes_u32(x: u32) {
println!("u32: {x}");
}
fn takes_i8(y: i8) {
println!("i8: {y}");
}
fn main() {
let x = 10;
let y = 20;
takes_u32(x);
takes_i8(y);
// takes_u32(y);
}
Дефолтным целочисленным типом является i32
({integer}
в сообщениях об ошибках), а дефолтным "плавающим" типом — f64
({float}
в сообщениях об ошибках).
fn main() {
let x = 3.14;
let y = 20;
assert_eq!(x, y);
// ERROR: no implementation for `{float} == {integer}`
// Целые числа и числа с плавающей точкой по умолчанию сравнивать между собой нельзя
}
Упражнение: Фибоначчи
Первое и второе числа Фибоначчи — 1
. Для n > 2
nth
(итое) число Фибоначчи вычисляется рекурсивно как сумма n - 1
и n - 2
чисел Фибоначчи.
Напишите функцию fib(n)
, которая вычисляет nth-число
Фибоначчи.
fn fib(n: u32) -> u32 {
if n <= 2 {
// Базовый случай
todo!("реализуй меня")
} else {
// Рекурсия
todo!("реализуй меня")
}
}
fn main() {
let n = 20;
println!("fib(n) = {}", fib(n));
// Макрос для проверки двух выражений на равенство.
// Неравенство вызывает панику
assert_eq!(fib(n), 6765);
}
fn fib(n: u32) -> u32 {
if n <= 2 {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
fn main() {
let n = 20;
println!("fib(n) = {}", fib(n));
assert_eq!(fib(n), 6765);
}
Поток управления
Условия
Большая часть синтаксиса потока управления Rust
похожа на C
, C++
или Java
:
- блоки разделяются фигурными скобками
- строчные комментарии начинаются с
//
, блочные — разделяются/* ... */
- ключевые слова
if
иwhile
работают, как ожидается - значения переменным присваиваются с помощью
=
, сравнения выполняются с помощью==
Выражения if
Выражения if используются в точности, как в других языках:
fn main() {
let x = 10;
if x < 20 {
println!("маленькое");
} else if x < 100 {
println!("больше");
} else {
println!("огромное");
}
}
Кроме того, if
можно использовать как выражение, возвращающее значение. Последнее выражение каждого блока становится значением выражения if
:
fn main() {
let x = 10;
let size = if x < 20 { "маленькое" } else { "большое" };
println!("размер числа: {}", size);
}
Поскольку if
является выражением и должно иметь определенный тип, значения обоих блоков должны быть одного типа. Попробуйте добавить ;
после маленькое
во втором примере.
При использовании if
в качестве выражения, оно должно заканчиваться ;
для его отделения от следующей инструкции. Попробуйте удалить ;
перед println!()
.
Циклы
Rust
предоставляет 3 ключевых слова для создания циклов: while
, loop
и for
.
while
Ключевое слово while работает, как в других языках — тело цикла выполняется, пока условие является истинным:
fn main() {
let mut x = 200;
while x >= 10 {
x = x / 2;
}
println!("итоговое значение x: {x}");
}
for
Цикл for перебирает диапазон значений:
fn main() {
for x in 1..5 {
println!("x: {x}");
}
}
loop
Цикл loop продолжается до прерывания с помощью break
:
fn main() {
let mut i = 0;
loop {
i += 1;
println!("{i}");
if i > 100 {
break;
}
}
}
- Мы подробно обсудим итераторы позже
- обратите внимание, что цикл
for
итерируется до 4. Для "включающего" диапазона используется синтаксис1..=5
break и continue
Ключевое слово break используется для раннего выхода (early exit) из цикла. Для loop
break
может принимать опциональное выражение, которое становится значением выражения loop
.
Для незамедлительного перехода к следующей итерации используется ключевое слово continue.
fn main() {
let (mut a, mut b) = (100, 52);
let result = loop {
if a == b {
break a;
}
if a < b {
b -= a;
} else {
a -= b;
}
};
println!("{result}");
}
continue
и break
могут помечаться метками (labels):
fn main() {
'outer: for x in 1..5 {
println!("x: {x}");
let mut i = 0;
while i < x {
println!("x: {x}, i: {i}");
i += 1;
if i == 3 {
break 'outer;
}
}
}
}
В примере мы прерываем внешний цикл после 3 итераций внутреннего цикла.
Обратите внимание, что только loop
может возвращать значения. Это связано с тем, что цикл loop
гарантировано выполняется хотя бы раз (в отличие от циклов while
и for
).
Блоки и области видимости
Блоки
Блок в Rust
содержит последовательность выражений. У каждого блока есть значение и тип, соответствующие последнему выражению блока:
fn main() {
let z = 13;
let x = {
let y = 10;
println!("y: {y}");
z - y
};
println!("x: {x}");
}
Если последнее выражение заканчивается ;
, результирующим значением и типом является ()
(пустой тип/кортеж — unit type).
Области видимости и затенение
Областью видимости (scope) переменной является ближайший к ней блок.
Переменные можно затенять/переопределять (shadow), как внешние, так и из той же области видимости:
fn main() {
let a = 10;
println!("перед: {a}");
{
let a = "привет";
println!("внутренняя область видимости: {a}");
let a = true;
println!("затенение во внутренней области видимости: {a}");
}
println!("после: {a}");
}
- Для того, чтобы убедиться в том, что область видимости переменной ограничена фигурными скобками, добавьте переменную
b
во внутреннюю область видимости и попробуйте получить к ней доступ во внешней области видимости - затенение отличается от мутации, поскольку после затенения обе локации памяти переменной существуют в одно время. Обе доступны под одним названием в зависимости от использования в коде
- затеняемая переменная может иметь другой тип
- поначалу затенение выглядит неясным, но оно удобно для сохранения значений после
unwrap()
(распаковки)
Функции
fn gcd(a: u32, b: u32) -> u32 {
if b > 0 {
gcd(b, a % b)
} else {
a
}
}
fn main() {
println!("наибольший общий делитель: {}", gcd(143, 52));
}
- Типы определяются как для параметров, так и для возвращаемого значения
- последнее выражение в теле функции становится возвращаемым значением (после него не должно быть
;
). Для раннего возврата может использоваться ключевое словоreturn
- дефолтным типом, возвращаемым функцией, является
()
(это справедливо также для функций, которые ничего не возвращают явно) - перегрузка функций в
Rust
не поддерживается
- число параметров всегда является фиксированным. Параметры по умолчанию не поддерживаются. Для создания функций с переменным количеством параметров используются макросы (macros)
- параметры имеют типы. Эти типы могут быть общими (дженериками — generics). Мы обсудим это позже
Макросы
Макросы раскрываются (expanded) в коде в процессе компиляции и могут принимать переменное количество параметров. Они обозначаются с помощью !
в конце. Стандартная библиотека Rust
включает несколько полезных макросов:
println!(format, ..)
— печатает строку в стандартный вывод, применяя форматирование, описанное в std::fmtformat!(format, ..)
— работает какprintln!()
, но возвращает строкуdbg!(expression)
— выводит значение выражения в терминал и возвращает егоtodo!()
— помечает код как еще не реализованный. Выполнение этого кода приводит к панике программыunreachable!()
— помечает код как недостижимый. Выполнение этого кода приводит к панике программы
fn factorial(n: u32) -> u32 {
let mut product = 1;
for i in 1..=n {
product *= dbg!(i);
}
product
}
fn fizzbuzz(n: u32) -> u32 {
todo!("реализуй меня")
}
fn main() {
let n = 13;
println!("{n}! = {}", factorial(4));
}
Упражнение: гипотеза Коллатца
Для объяснения сути гипотезы Коллатца рассмотрим следующую последовательность чисел, называемую сиракузской последовательностью. Берем любое натуральное число n
. Если оно четное, то делим его на 2, а если нечетное, то умножаем на 3 и прибавляем 1 (получаем 3n + 1
). Над полученным числом выполняем те же самые действия, и так далее. Последовательность прерывается на ni
, если ni
равняется 1.
Например, для числа 3 получаем:
- 3 — нечетное, 3*3 + 1 = 10
- 10 — четное, 10:2 = 5
- 5 — нечетное, 5*3 + 1 = 16
- 16 — четное, 16/2 = 8
- 8 — четное, 8/2 = 4
- 4 — четное, 4/2 = 2
- 2 — четное, 2/2 = 1
- 1 — нечетное (последовательность прерывается,
n
равняется 8)
Напишите функцию для вычисления сиракузской последовательности для указанного числа n
.
fn collatz_length(mut n: i32) -> u32 {
todo!("реализуй меня")
}
fn main() {
println!("длина последовательности: {}", collatz_length(11));
assert_eq!(collatz_length(11), 15);
}
fn collatz_length(mut n: i32) -> u32 {
let mut len = 1;
while n > 1 {
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
len += 1;
}
len
}
fn main() {
println!("длина последовательности: {}", collatz_length(11));
assert_eq!(collatz_length(11), 15);
}
Кортежи и массивы
Кортежи и массивы
Кортежи (tuples) и массивы (arrays) — первые "составные" (compound) типы, которые мы изучим. Все элементы массива должны быть одного типа, элементы кортежа могут быть разных типов. И массивы, и кортежи имеют фиксированный размер.
Типы | Литералы | |
---|---|---|
Массивы | [T; N] | [20, 30, 40], [0; 3] |
Кортежи | (), (T,), (T1, T2) | (), ('x',), ('x', 1.2) |
Определение массива и доступ к его элементам:
fn main() {
let mut a: [i8; 10] = [42; 10];
a[5] = 0;
println!("a: {a:?}");
}
Определение кортежа и доступ к его элементам:
fn main() {
let t: (i8, bool) = (7, true);
println!("t.0: {}", t.0);
println!("t.1: {}", t.1);
}
Массивы:
- значением массива типа
[T; N]
являетсяN
(константа времени компиляции) элементов типаT
. Обратите внимание, что длина массива является частью его типа, поэтому[u8; 3]
и[u8; 4]
считаются двумя разными типами. Срезы (slices), длина которых определяется во время выполнения, мы рассмотрим позже - попробуйте получить доступ к элементу за пределами границ массива. Доступ к элементам массива проверяется во время выполнения.
Rust
обычно выполняет различные оптимизации такой проверки, а в небезопасномRust
ее можно отключить - для присвоения значения массиву можно использовать литералы
- поскольку массивы имеют реализацию только отладочного вывода, они форматируются с помощью
{:?}
или{:#?}
Кортежи:
- как и массивы, кортежи имеют фиксированный размер
- кортежи группируют значения разных типов в один составной тип
- доступ к полям кортежа можно получить с помощью точки и индекса, например,
t.0
,t.1
- пустой кортеж
()
также называется "единичным/пустым типом" (unit type). Это и тип, и его единственное валидное значение. Пустой тип является индикатором того, что функция или выражение ничего не возвращают (в этом смысле пустой тип похож наvoid
в других языках)
Перебор массива
Для перебора массива (но не кортежа) может использоваться цикл for
:
fn main() {
let primes = [2, 3, 5, 7, 11, 13, 17, 19];
for prime in primes {
for i in 2..prime {
assert_ne!(prime % i, 0);
}
}
}
Возможность перебора массива в цикле for
обеспечивается трейтом IntoIterator
, о котором мы поговорим позже.
В примере мы видим новый макрос assert_ne!
. Существуют также макросы assert_eq!
и assert!
. Эти макросы проверяются всегда, в отличие от их аналогов для отладки debug_assert!
и др., которые удаляются из производственной сборки.
Сопоставление с образцом
Ключевое слово match
позволяет сопоставлять значение с одним или более паттернами/шаблонами. Сравнение выполняется сверху вниз, побеждает первое совпадение.
match
похож на switch
из других языков:
#[rustfmt::skip]
fn main() {
let input = 'x';
match input {
'q' => println!("выход"),
'a' | 's' | 'w' | 'd' => println!("движение"),
'0'..='9' => println!("число"),
key if key.is_lowercase() => println!("буква в нижнем регистре: {key}"),
_ => println!("другое"),
}
}
Паттерн _
— это шаблон подстановочного знака (wildcard pattern), который соответствует любому значению. Сопоставления должны быть исчерпывающими, т.е. охватывать все возможные случаи, поэтому _
часто используется как финальный перехватчик.
Сопоставление может использоваться как выражение. Как и в случае с if
, блоки match
должны иметь одинаковый тип. Типом является последнее выражение в блоке, если таковое имеется. В примере типом является ()
.
Переменная в паттерне (key
в примере) создает привязку, которая может использоваться в блоке.
Защитник сопоставления (match guard — if ...
) допускает совпадение только при удовлетворении условия.
Ремарки:
- вы могли заметить некоторые специальные символы, которые используются в шаблонах:
|
— этоor
(или)..
— распаковка значения1..=5
— включающий диапазон_
— подстановочный знак
- защита сопоставления важна и необходима, когда мы хотим кратко выразить более сложные идеи, чем позволяют одни только шаблоны
- защита сопоставление и использование
if
внутри блокаmatch
— разные вещи - условие, определенное в защитнике сопоставления, применяется ко всем выражениям паттерна, определенного с помощью
|
Деструктуризация
Деструктуризация — это способ извлечения данных из структуры данных с помощью шаблона, совпадающего со структурой данных. Это способ привязки к субкомпонентам (subcomponents) структуры данных.
Кортежи
fn main() {
describe_point((1, 0));
}
fn describe_point(point: (i32, i32)) {
match point {
(0, _) => println!("на оси Y"),
(_, 0) => println!("на оси X"),
(x, _) if x < 0 => println!("слева от оси Y"),
(_, y) if y < 0 => println!("ниже оси X"),
_ => println!("первый квадрант"),
}
}
Массивы
#[rustfmt::skip]
fn main() {
let triple = [0, -2, 3];
println!("расскажи мне о {triple:?}");
match triple {
[0, y, z] => println!("первый элемент - это 0, y = {y} и z = {z}"),
[1, ..] => println!("первый элемент - это 1, остальные элементы игнорируются"),
_ => println!("все элементы игнорируются"),
}
}
- Создайте новый шаблон массива, используя
_
для представления элемента - добавьте в массив больше значений
- обратите внимание, как
..
расширяется (expand) до разного количества элементов - покажите сопоставление с хвостом (tail) с помощью шаблонов
[.., b]
и[a@.., b]
Упражнение: вложенные массивы
Массивы могут содержать другие массивы:
let matrix3x3 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
Каков тип этой переменной?
Напишите функцию transpose()
, которая транспонирует матрицу 3х3 (превращает строки в колонки).
fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
todo!("реализуй меня")
}
fn main() {
let matrix = [
[101, 102, 103], // <-- комментарий не дает `rustfmt` форматировать `matrix` в одну строку
[201, 202, 203],
[301, 302, 303],
];
let transposed = transpose(matrix);
println!("транспонированная матрица: {:#?}", transposed);
assert_eq!(
transposed,
[
[101, 201, 301], //
[102, 202, 302],
[103, 203, 303],
]
);
}
fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
let mut result = [[0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
result[j][i] = matrix[i][j];
}
}
result
}
Ссылки
Общие ссылки
Ссылка (reference) — это способ получить доступ к значению без принятия его во владение, т.е. без заимствования (borrowing) этого значения. Общие/распределенные (shared) ссылки доступны только для чтения: ссылочные данные не могут модифицироваться.
fn main() {
let a = 'A';
let b = 'B';
let mut r: &char = &a;
println!("r: {}", *r);
r = &b;
println!("r: {}", *r);
}
Общая ссылка на тип T
имеет тип &T
. Оператор &
указывает на то, что это ссылка. Оператор *
используется для разыменования (dereferencing) ссылки — получения ссылочного значения.
Rust
запрещает висящие ссылки (dangling references):
fn x_axis(x: i32) -> &(i32, i32) {
let point = (x, 0);
return &point;
}
Ремарки:
- ссылка "заимствует" значение, на которое она ссылается. Код может использовать ссылку для доступа к значению, но его "владельцем" (owner) будет оригинальная переменная. Мы подробно поговорим о владении в 3 части
- ссылки реализованы как указатели (pointers) в
C
илиC++
, ключевым преимуществом которых является то, что они могут быть намного меньше, чем вещи, на которые они указывают. Позже мы будем говорить о том, какRust
обеспечивает безопасную работу с памятью, предотвращая баги, связанные с сырыми (raw) указателями Rust
не создает ссылки автоматически- в некоторых случаях
Rust
выполняет разыменование автоматически, например, при вызове методов (r.count_ones()
) - в первом примере переменная
r
является мутабельной, поэтому ее значение можно менять (r = &b
). Это повторно привязываетr
, теперь она указывает на что-то другое. Это отличается отC++
, где присвоение значения ссылке меняет ссылочное значение - общая ссылка не позволяет модифицировать значение, на которое она ссылается, даже если это значение является мутабельным (попробуйте
*r = 'X'
) Rust
отслеживает времена жизни (lifetimes) всех ссылок, чтобы убедиться, что они живут достаточно долго. В безопасномRust
не может быть висящих ссылок (dangling pointers).x_axis()
возвращает ссылку наpoint
, ноpoint
уничтожается (выделенная память освобождается — deallocate) после выполнения кода функции, и код не компилируется
Эксклюзивные ссылки
Эксклюзивные ссылки (exclusive references), также известные как мутабельные ссылки (mutable references), позволяют менять значение, на которое они ссылаются. Они имеют тип &mut T
:
fn main() {
let mut point = (1, 2);
let x_coord = &mut point.0;
*x_coord = 20;
println!("point: {point:?}");
}
Ремарки:
- "эксклюзивный" означает, что только эта ссылка может использоваться для доступа к значению. Других ссылок (общих или эксклюзивных) существовать не должно. Ссылочное значение недоступно, пока существует эксклюзивная ссылка. Попробуйте получить доступ к
&point.0
или изменитьpoint.0
, пока живаx_coord
- убедитесь в том, что понимаете разницу между
let mut x_coord: &i32
иlet x_coord: &mut i32
. Первая переменная — это общая ссылка, которая может быть привязана к разным значениям, вторая — эксклюзивная ссылка на мутабельную переменную
Упражнение: геометрия
Ваша задача — создать несколько вспомогательных функций для трехмерной геометрии, представляющей точку как [f64; 3]
.
// Функция для вычисления магнитуды вектора: суммируем квадраты координат вектора
// и извлекаем из этой суммы квадратный корень.
// Метод для извлечения квадратного корня - `sqrt()` (`v.sqrt()`)
fn magnitude(...) -> f64 {
todo!("реализуй меня")
}
// Функция нормализации вектора: вычисляем магнитуду вектора
// и делим на нее все координаты вектора
fn normalize(...) {
todo!("реализуй меня")
}
fn main() {
println!("магнитуда единичного вектора: {}", magnitude(&[0.0, 1.0, 0.0]));
let mut v = [1.0, 2.0, 9.0];
println!("магнитуда {v:?}: {}", magnitude(&v));
normalize(&mut v);
println!("магнитуда {v:?} после нормализации: {}", magnitude(&v));
}
fn magnitude(vector: &[f64; 3]) -> f64 {
let mut mag_squared = 0.0;
for coord in vector {
mag_squared += coord * coord;
}
mag_squared.sqrt()
}
fn normalize(vector: &mut [f64; 3]) {
let mag = magnitude(vector);
vector[0] /= mag;
vector[1] /= mag;
vector[2] /= mag;
}
Пользовательские типы
Именованные структуры
Rust
поддерживает кастомные структуры:
struct Person {
name: String,
age: u8,
}
fn describe(person: &Person) {
println!("{} is {} years old", person.name, person.age);
}
fn main() {
let mut peter = Person { name: String::from("Peter"), age: 27 };
describe(&peter);
peter.age = 28;
describe(&peter);
let name = String::from("Avery");
let age = 39;
let avery = Person { name, age };
describe(&avery);
let jackie = Person { name: String::from("Jackie"), ..avery };
describe(&jackie);
}
Ремарки:
- тип структуры отдельно определять не нужно
- структуры не могут наследовать друг другу
- для реализации трейта на типе, в котором не нужно хранить никаких значений, можно использовать структуру нулевого размера (zero-sized), например,
struct Foo;
- если название переменной совпадает с названием поля, то, например,
name: name
можно сократить доname
- синтаксис
..avery
позволяет копировать большую часть полей старой структуры в новую структуру. Он должен быть последним элементом
Кортежные структуры
Если названия полей неважны, можно использовать кортежную структуру:
struct Point(i32, i32);
fn main() {
let p = Point(17, 23);
println!("({}, {})", p.0, p.1);
}
Это часто используется для оберток единичных полей (single-field wrappers), которые называются newtypes
(новыми типами):
struct PoundsOfForce(f64);
struct Newtons(f64);
fn compute_thruster_force() -> PoundsOfForce {
todo!("Ask a rocket scientist at NASA")
}
fn set_thruster_force(force: Newtons) {
// ...
}
fn main() {
let force = compute_thruster_force();
set_thruster_force(force);
}
Ремарки:
newtype
— отличный способ закодировать дополнительную информацию о значении в примитивном типе, например:
- число измеряется в определенных единицах (
Newtons
) - при создании значение проходит определенную валидацию, которую не нужно каждый раз выполнять вручную:
PhoneNumber(String)
илиOddNumber(u32)
- число измеряется в определенных единицах (
- пример является тонкой отсылкой к провалу Mars Climate Orbiter
Перечисления
Ключевое слово enum
позволяет создать тип, который имеет несколько вариантов:
#[derive(Debug)]
enum Direction {
Left,
Right,
}
#[derive(Debug)]
enum PlayerMove {
Pass, // простой вариант
Run(Direction), // кортежный вариант
Teleport { x: u32, y: u32 }, // структурный вариант
}
fn main() {
let m: PlayerMove = PlayerMove::Run(Direction::Left);
println!("On this turn: {:?}", m);
}
Ремарки:
- перечисление позволяет собрать набор значений в один тип
Direction
— это тип с двумя вариантами:Direction::Left
иDirection::Right
PlayerMove
— это тип с тремя вариантами. В дополнение к полезным нагрузкам (payloads)Rust
будет хранить дискриминант, чтобы во время выполнения знать, какой вариант находится в значенииPlayerMove
Rust
использует минимальное пространство для хранения дискриминанта
- при необходимости сохраняется целое число наименьшего требуемого размера
- если разрешенные значения варианта не охватывают все битовые комбинации, для кодирования дискриминанта будут использоваться недопустимые битовые комбинации ("нишевые оптимизации" (niche optimization)). Например,
Option<&u8>
хранит либо указатель на целое число, либоNULL
для вариантаNone
- при необходимости дискриминантом можно управлять (например, для обеспечения совместимости с
C
):
#[repr(u32)]
enum Bar {
A, // 0
B = 10000,
C, // 10001
}
fn main() {
println!("A: {}", Bar::A as u32);
println!("B: {}", Bar::B as u32);
println!("C: {}", Bar::C as u32);
}
Без repr
тип дискриминанта занимает 2 байта, поскольку 10001
соответствует двум байтам.
Статики и константы
Статичные (static) и константные (constant) переменные — это 2 способа создания значений с глобальной областью видимости, которые не могут быть перемещены или перераспределены при выполнении программы.
const
Константные значения оцениваются во время компиляции и их значения встраиваются при использовании (inlined upon use):
const DIGEST_SIZE: usize = 3;
const ZERO: Option<u8> = Some(42);
fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {
let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE];
for (idx, &b) in text.as_bytes().iter().enumerate() {
digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);
}
digest
}
fn main() {
let digest = compute_digest("hello");
println!("digest: {digest:?}");
}
Только функции, помеченные с помощью const
, могут вызываться во время компиляции для генерации значений const
. Но такие функции могут вызываться и во время выполнения.
static
Статичные переменные живут на протяжении всего жизненного цикла программы и не могут перемещаться:
static BANNER: &str = "welcome";
fn main() {
println!("{BANNER}");
}
Значения статичных переменных не встраиваются при использовании и имеют фиксированные локации в памяти. Это может быть полезным для небезопасного и встроенного кода (FFI), но для создания глобальных переменных рекомендуется использовать const
.
Ремарки:
static
обеспечивает идентичность объекта (object identity): адрес в памяти и состояние, как того требуют типы с внутренней изменчивостью, такие какMutex<T>
- константы, которые оцениваются во время выполнения, требуются нечасто, но иногда они могут оказаться полезными, и их использование безопаснее, чем использование статик
Синонимы типов
Синоним типа (type alias) создает название для другого типа. Два типа могут использоваться взаимозаменяемо:
enum CarryableConcreteItem {
Left,
Right,
}
type Item = CarryableConcreteItem;
// Синонимы особенно полезны для длинных, сложных типов
use std::cell::RefCell;
use std::sync::{Arc, RwLock};
type PlayerInventory = RwLock<Vec<Arc<RefCell<Item>>>>;
Упражнение: события в лифте
Ваша задача состоит в том, чтобы создать структуру данных для представления событий в системе управления лифтом. Вам необходимо определить типы и функции для создания различных событий. Используйте #[derive(Debug)]
, чтобы разрешить форматирование типов с помощью {:?}
.
Это упражнение требует только создания и заполнения структур данных, чтобы функция main()
работала без ошибок.
#[derive(Debug)]
/// Событие, на которое должен реагировать контроллер
enum Event {
todo!("Добавить необходимые варианты")
}
/// Направление движения
#[derive(Debug)]
enum Direction {
Up,
Down,
}
/// Лифт прибыл на определенный этаж
fn car_arrived(floor: i32) -> Event {
todo!("реализуй меня")
}
/// Двери лифта открылись
fn car_door_opened() -> Event {
todo!("реализуй меня")
}
/// Двери лифта закрылись
fn car_door_closed() -> Event {
todo!("реализуй меня")
}
/// В вестибюле лифта на определенном этаже была нажата кнопка направления
fn lobby_call_button_pressed(floor: i32, dir: Direction) -> Event {
todo!("реализуй меня")
}
/// В кабине лифта была нажата кнопка этажа
fn car_floor_button_pressed(floor: i32) -> Event {
todo!("реализуй меня")
}
fn main() {
println!(
"Пассажир первого этажа нажал кнопку вверх: {:?}",
lobby_call_button_pressed(0, Direction::Up)
);
println!("Лифт прибыл на первый этаж: {:?}", car_arrived(0));
println!("Двери лифта открылись: {:?}", car_door_opened());
println!(
"Пассажир нажал на кнопку третьего этажа: {:?}",
car_floor_button_pressed(3)
);
println!("Двери лифта закрылись: {:?}", car_door_closed());
println!("Лифт прибыл на третий этаж: {:?}", car_arrived(3));
}
#[derive(Debug)]
enum Event {
/// Была нажата кнопка
ButtonPressed(Button),
/// Лифт прибыл на определенный этаж
CarArrived(Floor),
/// Двери лифта открылись
CarDoorOpened,
/// Двери лифта закрылись
CarDoorClosed,
}
/// Этаж представлен целым числом
type Floor = i32;
#[derive(Debug)]
enum Direction {
Up,
Down,
}
/// Доступная пользователю кнопка
#[derive(Debug)]
enum Button {
/// Кнопка вызова/направления в вестибюле лифта на определенном этаже
LobbyCall(Direction, Floor),
/// Кнопка этажа в кабине лифта
CarFloor(Floor),
}
fn car_arrived(floor: i32) -> Event {
Event::CarArrived(floor)
}
fn car_door_opened() -> Event {
Event::CarDoorOpened
}
fn car_door_closed() -> Event {
Event::CarDoorClosed
}
fn lobby_call_button_pressed(floor: i32, dir: Direction) -> Event {
Event::ButtonPressed(Button::LobbyCall(dir, floor))
}
fn car_floor_button_pressed(floor: i32) -> Event {
Event::ButtonPressed(Button::CarFloor(floor))
}
Это конец первой части руководства.
Материалы для более глубокого изучения рассмотренных тем:
- Книга/учебник по Rust (на русском языке) — главы 1-3, 5 и 6
- rustlings — упражнения 00-03, 07-09
- Rust на примерах (на русском языке) — примеры 1-5, 7-9
- Rust by practice — упражнения 3, 4, 6-8, 15 и 16
Happy coding!