Как стать автором
Обновить
1845.02

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

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



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!




Теги:
Хабы:
+20
Комментарии 11
Комментарии Комментарии 11

Публикации

Информация

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