Как стать автором
Обновить
2345.88
Timeweb Cloud
То самое облако

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

Уровень сложностиСредний
Время на прочтение21 мин
Количество просмотров25K



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!




Теги:
Хабы:
Всего голосов 24: ↑19 и ↓5+22
Комментарии12

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud

Истории