Практическое руководство по Rust. 1/4

от автора

Hello world!

Представляю вашему вниманию первую часть практического руководства по Rust.

Другой формат, который может показаться вам более удобным.

Руководство основано на Comprehensive Rust — руководстве по Rust от команды Android в Google и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить ?

В этой части мы рассмотрим следующие темы:

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

Материалы для более глубокого изучения названных тем:

Также см. Большую шпаргалку по 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 и fNN бит

  • 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::fmt
  • format!(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)) }

Это конец первой части руководства.

Материалы для более глубокого изучения рассмотренных тем:

Happy coding!



ссылка на оригинал статьи https://habr.com/ru/articles/790556/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *