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

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

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



Hello world!


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



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


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


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


  • итераторы (iterators): глубокое погружение в трейт Iterator
  • модули и видимость
  • тестирование
  • обработка ошибок: паника, Result и оператор ?
  • небезопасный Rust: случаи, когда безопасного Rust оказывается недостаточно

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



Также см. Большую шпаргалку по Rust.


Итераторы


Iterator


Трейт Iterator позволяет перебирать значения коллекции. Он требует реализации метода next и предоставляет большое количество полезных методов. Многие типы стандартной библиотеки реализуют Iterator, и мы также можем его реализовывать на собственных типах:


struct Fibonacci {
    curr: u32,
    next: u32,
}

impl Iterator for Fibonacci {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        let new_next = self.curr + self.next;
        self.curr = self.next;
        self.next = new_next;
        Some(self.curr)
    }
}

fn main() {
    let fib = Fibonacci { curr: 0, next: 1 };
    for (i, n) in fib.enumerate().take(5) {
        println!("fib({i}): {n}");
    }
}

Ремарки:


  • трейт Iterator реализует много популярных операций функционального программирования над коллекциями (map, filter, reduce и т.д.). В Rust эти функции должны создавать код, столь же эффективный, как и эквивалентные императивные реализации
  • IntoIterator — это трейт, обеспечивающий работу цикла for. Он реализуется типами коллекций, такими как Vec<T>, и ссылками на них, такими как &Vec<T> и &[T]. Диапазоны (ranges) также реализуют этот трейт. Вот почему мы можем перебирать элементы вектора с помощью for i in some_vec { .. }, но some_vec.next() отсутствует

IntoIterator


Трейт Iterator сообщает, как выполнять итерацию после создания итератора. Трейт IntoIterator определяет, как создать итератор для типа. Он автоматически используется циклом for.


struct Grid {
    x_coords: Vec<u32>,
    y_coords: Vec<u32>,
}

impl IntoIterator for Grid {
    type Item = (u32, u32);
    type IntoIter = GridIter;

    fn into_iter(self) -> GridIter {
        GridIter { grid: self, i: 0, j: 0 }
    }
}

struct GridIter {
    grid: Grid,
    i: usize,
    j: usize,
}

impl Iterator for GridIter {
    type Item = (u32, u32);

    fn next(&mut self) -> Option<(u32, u32)> {
        if self.i >= self.grid.x_coords.len() {
            self.i = 0;
            self.j += 1;
            if self.j >= self.grid.y_coords.len() {
                return None;
            }
        }
        let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));
        self.i += 1;
        res
    }
}

fn main() {
    let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] };
    for (x, y) in grid {
        println!("point = {x}, {y}");
    }
}

Каждая реализация IntoIterator должна определять 2 типа:


  • Item — перебираемый тип, такой как i8
  • IntoIter — тип Iterator, возвращаемый методом into_iter

Обратите внимание, что IntoIter и Iter связаны: итератор должен иметь такой же тип Item. Это означает, что он должен возвращать Option<Type>.


В примере перебираются все комбинации координат x и y.


Обратите внимание, что IntoIterator::into_iter принимает владение (ownership) над self. Попробуйте дважды перебрать grid в функции main.


Решите эту проблему путем реализации IntoIterator для &Grid и сохранения ссылки на Grid в GridIter.


Аналогичная проблема может возникнуть при использовании стандартных типов: for e in some_vec принимает владение над some_vec и перебирает собственные элементы вектора. Для перебора ссылок на элементы вектора следует использовать for e in &some_vec.


FromIterator


Трейт FromIterator позволяет создавать коллекции из Iterator:


fn main() {
    let primes = vec![2, 3, 5, 7];
    let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>();
    println!("prime_squares: {prime_squares:?}");
}

Iterator реализует


fn collect<B>(self) -> B
where
    B: FromIterator<Self::Item>,
    Self: Sized

Существует 2 способа определить B для этого метода:


  • с помощью turbofish: some_iterator.collect::<COLLECTION_TYPE>(), как показано в примере. Сокращение _ позволяет Rust вывести тип элементов вектора самостоятельно
  • с помощью вывода типов: let prime_squares: Vec<_> = some_iterator.collect()

Базовые реализации IntoIterator существуют для Vec, HashMap и некоторых других типов. Существуют также более специализированные реализации, позволяющие делать клевые вещи, вроде преобразования Iterator<Item = Result<V, E>> в Result<Vec<V>, E>


Упражнение: цепочка методов итератора


В этом упражнении вам нужно найти и использовать некоторые методы трейта Iterator для реализации сложных вычислений.


Используйте выражение итератора и соберите (collect) результат для построения возвращаемого значения.


// Функция для вычисления разницы между элементами `values`, смещенными на `offset`.
// `values` перебираются по кругу.
//
// Элемент `n` результата - `values[(n+offset)%len] - values[n]`.
fn offset_differences<N>(offset: usize, values: Vec<N>) -> Vec<N>
where
    N: Copy + std::ops::Sub<Output = N>,
{
    todo!("реализуй меня")
}

fn main() {
    let res = offset_differences(1, vec![1, 3, 5, 7]);
    println!("{:?}", res);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_offset_one() {
        assert_eq!(offset_differences(1, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
        assert_eq!(offset_differences(1, vec![1, 3, 5]), vec![2, 2, -4]);
        assert_eq!(offset_differences(1, vec![1, 3]), vec![2, -2]);
    }

    #[test]
    fn test_larger_offsets() {
        assert_eq!(offset_differences(2, vec![1, 3, 5, 7]), vec![4, 4, -4, -4]);
        assert_eq!(offset_differences(3, vec![1, 3, 5, 7]), vec![6, -2, -2, -2]);
        assert_eq!(offset_differences(4, vec![1, 3, 5, 7]), vec![0, 0, 0, 0]);
        assert_eq!(offset_differences(5, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);
    }

    #[test]
    fn test_custom_type() {
        assert_eq!(
            offset_differences(1, vec![1.0, 11.0, 5.0, 0.0]),
            vec![10.0, -6.0, -5.0, 1.0]
        );
    }

    #[test]
    fn test_degenerate_cases() {
        assert_eq!(offset_differences(1, vec![0]), vec![0]);
        assert_eq!(offset_differences(1, vec![1]), vec![0]);
        let empty: Vec<i32> = vec![];
        assert_eq!(offset_differences(1, empty), vec![]);
    }
}

Решение
fn offset_differences<N>(offset: usize, values: Vec<N>) -> Vec<N>
where
    N: Copy + std::ops::Sub<Output = N>,
{
    let a = (&values).into_iter();
    let b = (&values).into_iter().cycle().skip(offset);
    a.zip(b).map(|(a, b)| *b - *a).take(values.len()).collect()
}

Модули


Модули


Мы видели, как блоки impl позволяют нам использовать функции пространства имен (namespace) для типа.


Аналогично, mod позволяет нам использовать типы и функции пространства имен:


mod foo {
    pub fn do_something() {
        println!("в модуле foo");
    }
}

mod bar {
    pub fn do_something() {
        println!("в модуле bar");
    }
}

fn main() {
    foo::do_something();
    bar::do_something();
}

Ремарки:


  • пакеты (packages) предоставляют функционал и включают файл Cargo.toml, описывающий сборку из нескольких крейтов
  • крейты (crates) — это дерево модулей, где бинарный крейт является исполняемым файлом, а библиотечный крейт компилируется в библиотеку
  • модули определяют организацию и область видимости кода

Иерархия файловой системы


Если опустить содержимое модуля, Rust будет искать его в другом файле:


mod garden;

Это сообщает Rust, что содержимое модуля Garden находится по адресу src/garden.rs. Аналогично, модуль Garden::vegetables следует искать по адресу src/garden/vegetables.rs.


Корневой crate находится в:


  • src/lib.rs (для библиотечного крейта)
  • src/main.rs (для бинарного крейта)

Модули, определенные в файлах, можно документировать с помощью "внутренних комментариев документа". Они документируют элемент, который их содержит — в данном случае модуль.


//! This module implements the garden, including a highly performant germination
//! implementation.

// Re-export types from this module.
pub use garden::Garden;
pub use seeds::SeedPacket;

/// Sow the given seed packets.
pub fn sow(seeds: Vec<SeedPacket>) {
    todo!()
}

/// Harvest the produce in the garden that is ready.
pub fn harvest(garden: &mut Garden) {
    todo!()
}

Ремарки:


  • до Rust 2018 модули должны были находиться в module/mod.rs вместо module.rs, и это по-прежнему работает
  • основная причина представления filename.rs в качестве альтернативы filename/mod.rs заключается в том, что при большом количестве файлов mod.rs становится сложно в них разбираться
  • при более глубокой вложенности можно использовать директории, даже если основной модуль является файлом:

src/
├── main.rs
├── top_module.rs
└── top_module/
    └── sub_module.rs

  • место поиска модулей может быть изменено с помощью директивы компилятора:

#[path = "some/path.rs"]
mod some_module;

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


Видимость


Модули являются приватными/закрытыми:


  • элементы модулей являются приватными по умолчанию (скрывают детали своей реализации)
  • родители и сиблинги всегда являются видимыми (для элементов модулей)
  • если элемент видим в модуле foo, он видим всем потомкам foo

mod outer {
    fn private() {
        println!("outer::private");
    }

    pub fn public() {
        println!("outer::public");
    }

    mod inner {
        fn private() {
            println!("outer::inner::private");
        }

        pub fn public() {
            println!("outer::inner::public");
            super::private();
        }
    }
}

fn main() {
    outer::public();
}

Ремарки:


  • для того, чтобы сделать модуль публичным/открытым, используется ключевое слово pub
  • существуют продвинутые спецификаторы pub, позволяющие ограничивать область публичной видимости

use, super, self


Модуль может импортировать элементы другого модуля в свою область видимости с помощью ключевого слова use. В начале каждого модуля можно увидеть что-то вроде этого:


use std::collections::HashSet;
use std::process::abort;

Пути


Путь (path) разрешается следующим образом:


  1. Как относительный путь:
    • foo или self::foo ссылается на foo в текущем модуле
    • super::foo ссылается на foo в родительском модуле
  2. Как абсолютный путь:
    • crate:foo ссылается на foo в корне текущего крейта
    • bar::foo ссылается на foo в крейте bar

Ремарки:


  • распространенной практикой является повторный экспорт элементов модулей. Например, корневой файл lib.rs может содержать:

mod storage;

pub use storage::disk::DiskStorage;
pub use storage::network::NetworkStorage;

Это сделает DiskStorage и NetworkStorage доступными другим крейтам по короткому пути.


  • в основном, необходимо use (использовать) только элементы, которые используются в модуле. Однако для того, чтобы вызывать методы трейта, он должен находиться в области видимости, даже если тип, реализующий этот трейт, уже находится в ней. Например, чтобы использовать метод read_to_string для типа, реализующего трейт Read, необходимо use std::io::Read
  • в операторе use может использоваться подстановочный знак: use std::io::*. Делать так не рекомендуется, поскольку неясно, какие элементы импортируются, и эти элементы могут измениться со временем

Упражнение: модули для библиотеки пользовательского интерфейса


В этом упражнении вы реорганизуете код "Библиотеки графического интерфейса" из раздела "Методы и трейты" в набор модулей. Обычно каждый тип или набор тесно связанных типов помещают в отдельный модуль, поэтому каждый тип виджета должен иметь свой собственный модуль.


Код:


pub trait Widget {
    fn width(&self) -> usize;

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write);

    fn draw(&self) {
        let mut buffer = String::new();
        self.draw_into(&mut buffer);
        println!("{buffer}");
    }
}

pub struct Label {
    label: String,
}

impl Label {
    fn new(label: &str) -> Label {
        Label { label: label.to_owned() }
    }
}

pub struct Button {
    label: Label,
}

impl Button {
    fn new(label: &str) -> Button {
        Button { label: Label::new(label) }
    }
}

pub struct Window {
    title: String,
    widgets: Vec<Box<dyn Widget>>,
}

impl Window {
    fn new(title: &str) -> Window {
        Window { title: title.to_owned(), widgets: Vec::new() }
    }

    fn add_widget(&mut self, widget: Box<dyn Widget>) {
        self.widgets.push(widget);
    }

    fn inner_width(&self) -> usize {
        std::cmp::max(
            self.title.chars().count(),
            self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),
        )
    }
}

impl Widget for Window {
    fn width(&self) -> usize {
        self.inner_width() + 4
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        let mut inner = String::new();
        for widget in &self.widgets {
            widget.draw_into(&mut inner);
        }

        let inner_width = self.inner_width();

        writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
        writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();
        writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();
        for line in inner.lines() {
            writeln!(buffer, "| {:inner_width$} |", line).unwrap();
        }
        writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
    }
}

impl Widget for Button {
    fn width(&self) -> usize {
        self.label.width() + 4
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        let width = self.width();
        let mut label = String::new();
        self.label.draw_into(&mut label);

        writeln!(buffer, "+{:-<width$}+", "").unwrap();
        for line in label.lines() {
            writeln!(buffer, "|{:^width$}|", &line).unwrap();
        }
        writeln!(buffer, "+{:-<width$}+", "").unwrap();
    }
}

impl Widget for Label {
    fn width(&self) -> usize {
        self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        writeln!(buffer, "{}", &self.label).unwrap();
    }
}

fn main() {
    let mut window = Window::new("Rust GUI Demo 1.23");
    window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));
    window.add_widget(Box::new(Button::new("Click me!")));
    window.draw();
}

Упражнение можно начать с выполнения следующих команд:


cargo init gui-modules
cd gui-modules
cargo run

Отредактируйте файл src/main.rs, добавив в него инструкции mod, и создайте необходимые файлы в директории src.


Решение
src
├── main.rs
├── widgets
│   ├── button.rs
│   ├── label.rs
│   └── window.rs
└── widgets.rs

// src/widgets.rs
mod button;
mod label;
mod window;

pub trait Widget {
    fn width(&self) -> usize;

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write);

    fn draw(&self) {
        let mut buffer = String::new();
        self.draw_into(&mut buffer);
        println!("{buffer}");
    }
}

pub use button::Button;
pub use label::Label;
pub use window::Window;

// src/widgets/label.rs
use super::Widget;

pub struct Label {
    label: String,
}

impl Label {
    pub fn new(label: &str) -> Label {
        Label { label: label.to_owned() }
    }
}

impl Widget for Label {
    fn width(&self) -> usize {
        self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        writeln!(buffer, "{}", &self.label).unwrap();
    }
}

// src/widgets/button.rs
use super::{Label, Widget};

pub struct Button {
    label: Label,
}

impl Button {
    pub fn new(label: &str) -> Button {
        Button { label: Label::new(label) }
    }
}

impl Widget for Button {
    fn width(&self) -> usize {
        self.label.width() + 4
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        let width = self.width();
        let mut label = String::new();
        self.label.draw_into(&mut label);

        writeln!(buffer, "+{:-<width$}+", "").unwrap();
        for line in label.lines() {
            writeln!(buffer, "|{:^width$}|", &line).unwrap();
        }
        writeln!(buffer, "+{:-<width$}+", "").unwrap();
    }
}

// src/widgets/window.rs
use super::Widget;

pub struct Window {
    title: String,
    widgets: Vec<Box<dyn Widget>>,
}

impl Window {
    pub fn new(title: &str) -> Window {
        Window { title: title.to_owned(), widgets: Vec::new() }
    }

    pub fn add_widget(&mut self, widget: Box<dyn Widget>) {
        self.widgets.push(widget);
    }

    fn inner_width(&self) -> usize {
        std::cmp::max(
            self.title.chars().count(),
            self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),
        )
    }
}

impl Widget for Window {
    fn width(&self) -> usize {
        self.inner_width() + 4
    }

    fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
        let mut inner = String::new();
        for widget in &self.widgets {
            widget.draw_into(&mut inner);
        }

        let inner_width = self.inner_width();

        writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
        writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();
        writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();
        for line in inner.lines() {
            writeln!(buffer, "| {:inner_width$} |", line).unwrap();
        }
        writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
    }
}

// src/main.rs
mod widgets;

use widgets::Widget;

fn main() {
    let mut window = widgets::Window::new("Rust GUI Demo 1.23");
    window
        .add_widget(Box::new(widgets::Label::new("This is a small text GUI demo.")));
    window.add_widget(Box::new(widgets::Button::new("Click me!")));
    window.draw();
}

Тестирование


Модульные/юнит-тесты


Rust и Cargo поставляются с фреймворком для тестирования:


  • модульные (unit) тесты поддерживаются прямо в коде, который мы пишем
  • интеграционные (integration) тесты поддерживаются через директорию tests

Тесты помечаются с помощью директивы #[test]. Юнит-тесты часто помещаются во вложенный модуль tests. Директива #[cfg(test)] сообщает компилятору, что содержащийся далее код следует компилировать только при запуске тестов:


fn first_word(text: &str) -> &str {
    match text.find(' ') {
        Some(idx) => &text[..idx],
        None => &text,
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_empty() {
        assert_eq!(first_word(""), "");
    }

    #[test]
    fn test_single_word() {
        assert_eq!(first_word("Hello"), "Hello");
    }

    #[test]
    fn test_multiple_words() {
        assert_eq!(first_word("Hello World"), "Hello");
    }
}

  • Модульные тесты позволяют тестировать приватный функционал
  • атрибут #[cfg(test)] активируется только при выполнении cargo test

Другие типы тестов


Интеграционные тесты


Интеграционные тесты используются для тестирования библиотеки от лица клиента.


Создаем файл .rs в директории tests/:


// tests/my_library.rs
use my_library::init;

#[test]
fn test_init() {
    assert!(init().is_ok());
}

Эти тесты имеют доступ только к публичному API тестируемого крейта.


Документационные тесты


Rust имеет встроенную поддержку документационных тестов:


/// Сокращает строку до указанной длины.
///
/// ```
/// # use playground::shorten_string;
/// assert_eq!(shorten_string("Hello World", 5), "Hello");
/// assert_eq!(shorten_string("Hello World", 20), "Hello World");
/// ```
pub fn shorten_string(s: &str, length: usize) -> &str {
    &s[..std::cmp::min(length, s.len())]
}

  • Блоки кода в комментариях /// считаются валидным кодом Rust (разумеется, если код компилируется)
  • код будет скомпилирован и выполнен как часть cargo test
  • добавление # в код скроет его из документации, но он по-прежнему будет компилироваться/выполняться

Полезные крейты


Rust предоставляет лишь базовую поддержку тестов.


Вот несколько крейтов, которые могут пригодиться для тестирования:


  • googletest — комплексная библиотека тестирования в лучших традициях GoogleTest для C++
  • proptest — библиотека тестирования на основе свойств (properties)
  • rstest — библиотека тестирования, поддерживающая фикстуры (fixtures) и параметризованные тесты

GoogleTest


Крейт googletest позволяет создавать гибкие тесты с использованием сопоставителей (matchers):


use googletest::prelude::*;

#[googletest::test]
fn test_elements_are() {
    let value = vec!["foo", "bar", "baz"];
    expect_that!(value, elements_are!(eq("foo"), lt("xyz"), starts_with("b")));
}

Если мы изменим b на ! в последнем элементе, тест провалится с выдачей структурированного сообщения об ошибке:


---- test_elements_are stdout ----
Value of: value
Expected: has elements:
  0. is equal to "foo"
  1. is less than "xyz"
  2. starts with prefix "!"
Actual: ["foo", "bar", "baz"],
  where element #2 is "baz", which does not start with "!"
  at src/testing/googletest.rs:6:5
Error: See failure output above

Ремарки:


  • выполните cargo add googletest для установки googletest
  • use googletest::prelude::*; импортирует некоторые часто используемые макросы и типы
  • googletest предоставляет большое количество сопоставителей
  • приятной особенностью googletest является то, что несоответствия в многострочных строках отображаются в виде разницы:

#[test]
fn test_multiline_string_diff() {
    let haiku = "Memory safety found,\n\
                 Rust's strong typing guides the way,\n\
                 Secure code you'll write.";
    assert_that!(
        haiku,
        eq("Memory safety found,\n\
            Rust's silly humor guides the way,\n\
            Secure code you'll write.")
    );
}

Вывод будет цветным:


    Value of: haiku
Expected: is equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure code you'll write."
Actual: "Memory safety found,\nRust's strong typing guides the way,\nSecure code you'll write.",
  which isn't equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure code you'll write."
Difference(-actual / +expected):
 Memory safety found,
-Rust's strong typing guides the way,
+Rust's silly humor guides the way,
 Secure code you'll write.
  at src/testing/googletest.rs:17:5=

Мокинг


Для мокинга (mocking — создание макета) широко используется библиотека mockall:


use std::time::Duration;

#[mockall::automock]
pub trait Pet {
    fn is_hungry(&self, since_last_meal: Duration) -> bool;
}

#[test]
fn test_robot_dog() {
    let mut mock_dog = MockPet::new();
    mock_dog.expect_is_hungry().return_const(true);
    assert_eq!(mock_dog.is_hungry(Duration::from_secs(10)), true);
}

Ремарки:


  • для установки mockall выполните команду cargo add mockall
  • на crates.io доступны и другие библиотеки для мокинга, в частности, для мокинга HTTP-сервисов. Другие библиотеки работают аналогично Mockall: они позволяют легко получить макет реализации определенного трейта
  • обратите внимание, что использование макетов несколько противоречиво: макеты позволяют полностью изолировать тест от его зависимостей. Непосредственным результатом является более быстрое и стабильное выполнение тестов. С другой стороны, макеты могут быть настроены неправильно и возвращать данные, отличные от того, что делали бы реальные зависимости. По-возможности следует использовать реальные зависимости. Например, многие базы данных позволяют настроить серверную часть в памяти (in-memory backend). Это означает, что мы получаем правильное поведение тестов, плюс они работают быстро и автоматически очищаются. Многие веб-фреймворки позволяют запускать внутрипроцессный сервер, который привязывается к произвольному порту на локальном хосте. Этот подход является более предпочтительным, чем мокинг, поскольку позволяет тестировать код в реальной среде
  • Mockall предоставляет много полезных функций. В частности, мы можем настроить ожидания (expectations), которые зависят от переданных аргументов. Здесь мы используем это, чтобы создать макет кошки, которая проголодалась через 3 часа после того, как ее в последний раз покормили:

#[test]
fn test_robot_cat() {
    let mut mock_cat = MockPet::new();
    mock_cat
        .expect_is_hungry()
        .with(mockall::predicate::gt(Duration::from_secs(3 * 3600)))
        .return_const(true);
    mock_cat.expect_is_hungry().return_const(false);
    assert_eq!(mock_cat.is_hungry(Duration::from_secs(1 * 3600)), false);
    assert_eq!(mock_cat.is_hungry(Duration::from_secs(5 * 3600)), true);
}

  • мы можем использовать .times(n), чтобы ограничить количество вызовов фиктивного метода до n — при превышении этого лимита программа запаникует

Линтер и Clippy


Компилятор Rust выдает фантастические сообщения об ошибках, а также полезные подсказки (lints). Clippy предоставляет еще больше подсказок, организованных в группы, которые можно включать/выключать для каждого проекта.


#[deny(clippy::cast_possible_truncation)]
fn main() {
    let x = 3;
    while (x < 70000) {
        x *= 2;
    }
    println!("X помещается в u16, верно? {}", x as u16);
}

Упражнение: алгоритм Луна


Алгоритм Луна используется для проверки номеров кредитных карт. Алгоритм принимает строку на вход и выполняет следующие действия:


  • игнорируем все пробелы
  • отклоняем номера, содержащие менее двух цифр
  • двигаясь справа налево, удваиваем каждую вторую цифру: для числа 1234 удваиваем 3 и 1, для числа 98765 удваиваем 6 и 8
  • после удвоения цифры суммируем цифры, если результат больше 9. Таким образом, удвоение 7 дает 14, что дает 1 + 4 = 5
  • суммируем все неудвоенные и удвоенные цифры
  • номер кредитной карты действителен, если сумма заканчивается на 0

Приведенный код содержит ошибочную реализацию алгоритма Луна, а также два модульных теста, которые подтверждают, что большая часть алгоритма реализована правильно:


pub fn luhn(cc_number: &str) -> bool {
    let mut sum = 0;
    let mut double = false;

    for c in cc_number.chars().rev() {
        if let Some(digit) = c.to_digit(10) {
            if double {
                let double_digit = digit * 2;
                sum +=
                    if double_digit > 9 { double_digit - 9 } else { double_digit };
            } else {
                sum += digit;
            }
            double = !double;
        } else {
            continue;
        }
    }

    sum % 10 == 0
}

fn main() {
    let cc_number = "1234 5678 1234 5670";
    println!(
        "{cc_number} является действительным номером кредитной карты? {}",
        if luhn(cc_number) { "Да" } else { "Нет" }
    );
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_valid_cc_number() {
        assert!(luhn("4263 9826 4026 9299"));
        assert!(luhn("4539 3195 0343 6467"));
        assert!(luhn("7992 7398 713"));
    }

    #[test]
    fn test_invalid_cc_number() {
        assert!(!luhn("4223 9826 4026 9299"));
        assert!(!luhn("4539 3195 0343 6476"));
        assert!(!luhn("8273 1232 7352 0569"));
    }

    #[test]
    fn test_non_digit_cc_number() {
        assert!(!luhn("foo"));
        assert!(!luhn("foo 0 0"));
    }

    #[test]
    fn test_empty_cc_number() {
        assert!(!luhn(""));
        assert!(!luhn(" "));
        assert!(!luhn("  "));
        assert!(!luhn("    "));
    }

    #[test]
    fn test_single_digit_cc_number() {
        assert!(!luhn("0"));
    }

    #[test]
    fn test_two_digit_cc_number() {
        assert!(luhn(" 0 0 "));
    }
}

Решение
pub fn luhn(cc_number: &str) -> bool {
    // Итоговая сумма цифр
    let mut sum = 0;
    // Индикатор необходимости удвоения цифры
    let mut double = false;
    // Количество цифр
    let mut digits = 0;

    // Перебираем цифры справа налево
    for c in cc_number.chars().rev() {
        // Если символ является валидным десятичным числом
        if let Some(digit) = c.to_digit(10) {
            // Увеличиваем количество цифр
            digits += 1;
            // Если цифру нужно удвоить
            if double {
                let double_digit = digit * 2;
                // Если удвоенная цифра больше 9, вычитаем из нее 9:
                // если получили 14, то 1 + 4 = 5, что эквивалентно 14 - 9 = 5
                sum +=
                    if double_digit > 9 { double_digit - 9 } else { double_digit };
            // Иначе просто добавляем цифру к сумме
            } else {
                sum += digit;
            }
            // Удваиваем каждую вторую цифру
            double = !double;
        // Игнорируем пробелы
        } else if c.is_whitespace() {
            continue;
        // Если строка содержит символ, отличающийся от цифры и пробела
        } else {
            return false;
        }
    }

    // Цифр должно быть больше двух и сумма должна заканчиваться на 0
    digits >= 2 && sum % 10 == 0
}

Обработка ошибок


Паника


Фатальные ошибки обрабатываются в Rust с помощью "паники" (panic).


Паника происходит при возникновении фатальной ошибки во время выполнения:


fn main() {
    let v = vec![10, 20, 30];
    println!("v[100]: {}", v[100]);
}

Ремарки:


  • паника связана с неисправимыми и неожиданными ошибками:
    • паника — это симптомы ошибок в программе
    • сбои во время выполнения, такие как неудачная проверка границ (boundaries), могут вызвать панику
    • утверждения (например, assert!) паникуют при неудаче
    • для целенаправленной паники можно использовать макрос panic!
  • паника "разматывает" (unwind) стек, сбрасывая значения так же, как если бы функции вернули значения
  • в примере для безопасного доступа к элементу вектора по индексу можно использовать Vec::get

По умолчанию паника разматывает стек. Разматывание может быть перехвачено:


use std::panic;

fn main() {
    let result = panic::catch_unwind(|| "No problem here!");
    println!("{result:?}");

    let result = panic::catch_unwind(|| {
        panic!("Oh no!");
    });
    println!("{result:?}");
}

  • Не пытайтесь реализовать исключения с помощью catch_unwind
  • это может быть полезно на серверах, которые должны продолжать работать даже в случае сбоя одного запроса
  • это не работает при установке panic = 'abort' в Cargo.toml

Оператор ?


Ошибки времени выполнения, такие как отказ в соединении или отсутствие файла, обрабатываются с помощью типа Result, но сопоставление (matching) этого типа при каждом вызове может быть утомительным и излишним. Оператор ? используется для возврата ошибок вызывающему (caller). Он позволяет заменить


match some_expression {
    Ok(value) => value,
    Err(err) => return Err(err),
}

на


some_expression?

Попробуйте упростить обработку ошибок в следующем коде:


use std::io::Read;
use std::{fs, io};

fn read_username(path: &str) -> Result<String, io::Error> {
    let username_file_result = fs::File::open(path);
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(err) => return Err(err),
    };

    let mut username = String::new();
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(err) => Err(err),
    }
}

fn main() {
    // fs::write("config.dat", "alice").unwrap();
    let username = read_username("config.dat");
    println!("username or error: {username:?}");
}

Подсказки:


  • переменная username может быть либо Ok(String), либо Err(error)
  • используйте fs::write для тестирования разных случаев: отсутствие файла, пустой файл, файл с именем пользователя
  • обратите внимание, что main может возвращать Result<(), E> до тех пор, пока реализует std::process:Termination. На практике это означает, что E реализует Debug. Исполняемый файл напечатает вариант Err и вернет ненулевой статус выхода в случае ошибки

Преобразования Try


Оператор ? работает немного сложнее, чем можно подумать.


Это:


expression?

Эквивалентно этому:


match expression {
    Ok(value) => value,
    Err(err)  => return Err(From::from(err)),
}

Вызов From::from здесь означает, что мы пытаемся преобразовать тип ошибки в тип, возвращаемый функцией. Это позволяет легко преобразовать локальные ошибки в ошибки более высокого уровня.


Пример


use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::fs::File;
use std::io::{self, Read};

#[derive(Debug)]
enum ReadUsernameError {
    IoError(io::Error),
    EmptyUsername(String),
}

impl Error for ReadUsernameError {}

impl Display for ReadUsernameError {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match self {
            Self::IoError(e) => write!(f, "Ошибка ввода/вывода: {e}"),
            Self::EmptyUsername(path) => write!(f, "Имя пользователя отсутствует в {path}"),
        }
    }
}

impl From<io::Error> for ReadUsernameError {
    fn from(err: io::Error) -> Self {
        Self::IoError(err)
    }
}

fn read_username(path: &str) -> Result<String, ReadUsernameError> {
    let mut username = String::with_capacity(100);
    File::open(path)?.read_to_string(&mut username)?;
    if username.is_empty() {
        return Err(ReadUsernameError::EmptyUsername(String::from(path)));
    }
    Ok(username)
}

fn main() {
    // fs::write("config.dat", "").unwrap();
    let username = read_username("config.dat");
    println!("Имя пользователя или ошибка: {username:?}");
}

Оператор ? должен возвращать значение, совместимое с типом значения, возвращаемого функцией. Для Result это означает, что типы ошибок должны быть совместимыми. Функция, возвращающая Result<T, ErrorOuter>, может использовать ? только для значения типа Result<U, ErrorInner>, если ErrorOuter и ErrorInner имеют один и тот же тип, или если ErrorOuter реализует From<ErrorInner>.


Распространенной альтернативой реализации From является Result::map_err, особенно когда преобразование происходит только в одном месте.


Для Option нет требований совместимости. Функция, возвращающая Option<T>, может использовать оператор ? на Option<U> для произвольных типов T и U.


Функция, возвращающая Result, не может использовать ? на Option, и наоборот. Однако, Option::ok_or преобразует Option в Result, а Result::okResult в Option.


Динамические типы ошибок


Иногда мы хотим возвращать любой тип ошибки без создания перечисления, охватывающего все варианты. Трейт std::error::Error позволяет легко создать трейт-объект, который может содержать любую ошибку:


use std::error::Error;
use std::fs;
use std::io::Read;

fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {
    let mut count_str = String::new();
    fs::File::open(path)?.read_to_string(&mut count_str)?;
    let count: i32 = count_str.parse()?;
    Ok(count)
}

fn main() {
    fs::write("count.dat", "1i3").unwrap();
    match read_count("count.dat") {
        Ok(count) => println!("Содержимое: {count}"),
        Err(err) => println!("Ошибка: {err}"),
    }
}

Функция read_count может возвращать std::io::Error (из операций с файлом) или std::num::ParseIntError (из String::parse).


Использование динамических (boxing) ошибок сокращает количество кода, но лишает возможности по-разному обрабатывать разные ошибки. Поэтому использовать Box<dyn Error> в общедоступном API библиотеки не рекомендуется, но это может быть хорошим вариантом, когда мы просто хотим где-то отображать сообщение об ошибке.


При создании кастомных типов ошибок убедитесь, что они реализуют std::error::Error, чтобы их можно было оборачивать в Box.


thiserror и anyhow


Крейты thiserror и anyhow широко используются для упрощения обработки ошибок. thiserror помогает создавать кастомные типы ошибок, реализующие From<T>. anyhow помогает с обработкой ошибок в функциях, включая добавление контекстуальной информации в ошибки.


use anyhow::{bail, Context, Result};
use std::fs;
use std::io::Read;
use thiserror::Error;

#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("Имя пользователя отсутствует в {0}")]
struct EmptyUsernameError(String);

fn read_username(path: &str) -> Result<String> {
    let mut username = String::with_capacity(100);
    fs::File::open(path)
        .with_context(|| format!("Ошибка при открытии {path}"))?
        .read_to_string(&mut username)
        .context("Ошибка при чтении")?;
    if username.is_empty() {
        bail!(EmptyUsernameError(path.to_string()));
    }
    Ok(username)
}

fn main() {
    // fs::write("config.dat", "").unwrap();
    match read_username("config.dat") {
        Ok(username) => println!("Имя пользователя: {username}"),
        Err(err) => println!("Ошибка: {err:?}"),
    }
}

thiserror:


  • макрос error предоставляется thiserror и содержит большое количество атрибутов для лаконичного определения типов ошибок
  • трейт std::error::Error реализуется автоматически
  • сообщение из #[error] используется для автоматической реализации трейта Display

anyhow:


  • anyhow::Error — это обертка над Box<dyn Error>. Опять же это не лучший выбор для общедоступного API библиотеки, но он широко используется в приложениях
  • anyhow::Result<V> — это синоним типа Result<V, anyhow::Error>
  • при необходимости фактический тип ошибки внутри него можно извлечь для проверки
  • anyhow::Context — это трейт, реализованный для стандартных типов Result и Option. use anyhow::Context необходим для включения .context() и .with_context() на этих типах

Упражнение: без паники


Следующий код реализует очень простой синтаксический анализатор языка выражений. Однако он обрабатывает ошибки путем паники. Перепишите его, чтобы вместо этого использовать идиоматическую обработку ошибок и распространять ошибки на возврат из main. Не стесняйтесь использовать thiserror и anyhow.


Подсказка: начните исправлять ошибки в функции parse. После того, как она заработает, обновите Tokenizer для реализации Iterator<Item=Result<Token, TokenizerError>> и обработайте его в парсере.


use std::iter::Peekable;
use std::str::Chars;

// Арифметический оператор
#[derive(Debug, PartialEq, Clone, Copy)]
enum Op {
    Add,
    Sub,
}

// Токен языка
#[derive(Debug, PartialEq)]
enum Token {
    Number(String),
    Identifier(String),
    Operator(Op),
}

// Выражение языка
#[derive(Debug, PartialEq)]
enum Expression {
    // Ссылка на переменную
    Var(String),
    // Литеральное число
    Number(u32),
    // Бинарная операция
    Operation(Box<Expression>, Op, Box<Expression>),
}

fn tokenize(input: &str) -> Tokenizer {
    return Tokenizer(input.chars().peekable());
}

struct Tokenizer<'a>(Peekable<Chars<'a>>);

impl<'a> Iterator for Tokenizer<'a> {
    type Item = Token;

    fn next(&mut self) -> Option<Token> {
        let c = self.0.next()?;
        match c {
            '0'..='9' => {
                let mut num = String::from(c);
                while let Some(c @ '0'..='9') = self.0.peek() {
                    num.push(*c);
                    self.0.next();
                }
                Some(Token::Number(num))
            }
            'a'..='z' => {
                let mut ident = String::from(c);
                while let Some(c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() {
                    ident.push(*c);
                    self.0.next();
                }
                Some(Token::Identifier(ident))
            }
            '+' => Some(Token::Operator(Op::Add)),
            '-' => Some(Token::Operator(Op::Sub)),
            _ => panic!("Неожиданный символ {c}"),
        }
    }
}

fn parse(input: &str) -> Expression {
    let mut tokens = tokenize(input);

    fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Expression {
        let Some(tok) = tokens.next() else {
            panic!("Неожиданный конец ввода");
        };
        let expr = match tok {
            Token::Number(num) => {
                let v = num.parse().expect("Невалидное 32-битное целое число");
                Expression::Number(v)
            }
            Token::Identifier(ident) => Expression::Var(ident),
            Token::Operator(_) => panic!("Неожиданный токен {tok:?}"),
        };
        // Проверяем наличие бинарной операции
        match tokens.next() {
            None => expr,
            Some(Token::Operator(op)) => Expression::Operation(
                Box::new(expr),
                op,
                Box::new(parse_expr(tokens)),
            ),
            Some(tok) => panic!("Неожиданный токен {tok:?}"),
        }
    }

    parse_expr(&mut tokens)
}

fn main() {
    let expr = parse("10+foo+20-30");
    println!("{expr:?}");
}

Решение
use thiserror::Error;
use std::iter::Peekable;
use std::str::Chars;

#[derive(Debug, PartialEq, Clone, Copy)]
enum Op {
    Add,
    Sub,
}

#[derive(Debug, PartialEq)]
enum Token {
    Number(String),
    Identifier(String),
    Operator(Op),
}

#[derive(Debug, PartialEq)]
enum Expression {
    Var(String),
    Number(u32),
    Operation(Box<Expression>, Op, Box<Expression>),
}

fn tokenize(input: &str) -> Tokenizer {
    return Tokenizer(input.chars().peekable());
}

#[derive(Debug, Error)]
enum TokenizerError {
    #[error("Неожиданный символ {0}")]
    UnexpectedCharacter(char),
}

struct Tokenizer<'a>(Peekable<Chars<'a>>);

impl<'a> Iterator for Tokenizer<'a> {
    type Item = Result<Token, TokenizerError>;

    fn next(&mut self) -> Option<Result<Token, TokenizerError>> {
        let c = self.0.next()?;
        match c {
            '0'..='9' => {
                let mut num = String::from(c);
                while let Some(c @ '0'..='9') = self.0.peek() {
                    num.push(*c);
                    self.0.next();
                }
                Some(Ok(Token::Number(num)))
            }
            'a'..='z' => {
                let mut ident = String::from(c);
                while let Some(c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() {
                    ident.push(*c);
                    self.0.next();
                }
                Some(Ok(Token::Identifier(ident)))
            }
            '+' => Some(Ok(Token::Operator(Op::Add))),
            '-' => Some(Ok(Token::Operator(Op::Sub))),
            _ => Some(Err(TokenizerError::UnexpectedCharacter(c))),
        }
    }
}

#[derive(Debug, Error)]
enum ParserError {
    #[error("Ошибка токенизатора: {0}")]
    TokenizerError(#[from] TokenizerError),
    #[error("Неожиданный конец ввода")]
    UnexpectedEOF,
    #[error("Неожиданный токен {0:?}")]
    UnexpectedToken(Token),
    #[error("Невалидное число")]
    InvalidNumber(#[from] std::num::ParseIntError),
}

fn parse(input: &str) -> Result<Expression, ParserError> {
    let mut tokens = tokenize(input);

    fn parse_expr<'a>(
        tokens: &mut Tokenizer<'a>,
    ) -> Result<Expression, ParserError> {
        let tok = tokens.next().ok_or(ParserError::UnexpectedEOF)??;
        let expr = match tok {
            Token::Number(num) => {
                let v = num.parse()?;
                Expression::Number(v)
            }
            Token::Identifier(ident) => Expression::Var(ident),
            Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)),
        };

        Ok(match tokens.next() {
            None => expr,
            Some(Ok(Token::Operator(op))) => Expression::Operation(
                Box::new(expr),
                op,
                Box::new(parse_expr(tokens)?),
            ),
            Some(Err(e)) => return Err(e.into()),
            Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)),
        })
    }

    parse_expr(&mut tokens)
}

fn main() -> anyhow::Result<()> {
    let expr = parse("10+foo+20-30")?;
    println!("{expr:?}");
    Ok(())
}

Небезопасный Rust


Небезопасный Rust


Rust состоит из двух частей:


  • безопасный Rust — работа с памятью является безопасной, отсутствует неопределенное поведение
  • небезопасный Rust — код может приводить к неопределенному поведению при нарушении определенных условий

В этом курсе мы видели в основном безопасный Rust, но важно понимать, что такое небезопасный Rust.


Небезопасный код обычно небольшой и изолированный, и его корректность должна быть тщательно документирована. Обычно он оборачивается в безопасный уровень абстракции (safe abstraction layer).


Небезопасный Rust предоставляет доступ к 5 новым возможностям:


  • разыменование сырых указателей (raw pointers)
  • доступ и модификация мутабельных статичных переменных
  • доступ к полям union
  • вызов unsafe функций, включая extern (внешние) функции
  • реализация unsafe трейтов

Небезопасный Rust не означает, что код неправильный. Он означает, что разработчики отключили некоторые функции безопасности компилятора и им приходится писать правильный код самостоятельно. Это означает, что компилятор не обеспечивает соблюдение правил безопасности памяти Rust.


Разыменование сырых указателей


Создание указателей является безопасным, но их разыменование требует unsafe:


fn main() {
    let mut s = String::from("careful!");

    let r1 = &mut s as *mut String;
    let r2 = r1 as *const String;

    // Безопасно, поскольку r1 и r2 были получены из ссылок и поэтому
    // гарантированно не равны нулю и правильно выровнены (properly aligned), объекты, лежащие в основе ссылок,
    // из которых они были получены, активны на протяжении всего небезопасного блока,
    // и к ним нельзя получить доступ ни через ссылки, ни (конкурентно) через другие указатели
    unsafe {
        println!("r1 is: {}", *r1);
        *r1 = String::from("uhoh");
        println!("r2 is: {}", *r2);
    }

    // Небезопасно. Не делайте так
    /*
    let r3: &String = unsafe { &*r1 };
    drop(s);
    println!("r3 is: {}", *r3);
    */
}

Хорошей практикой является написание комментария для каждого небезопасного блока, объясняющего, как код внутри него удовлетворяет требованиям безопасности выполняемых им небезопасных операций.


В случае разыменования указателей это означает, что указатели должны быть валидными, т.е.:


  • указатель не должен равняться нулю
  • указатель должен быть разыменовываемым (в пределах одного выделенного объекта)
  • объект не должен быть освобожден
  • не должно быть одновременного доступа к одной и той же локации памяти
  • если указатель был получен путем приведения ссылки (reference coercion), базовый объект должен быть активным и никакая ссылка не может использоваться для доступа к памяти

В большинстве случаев указатель также должен быть правильно выровнен.


В разделе "Небезопасно" приведен пример распространенной ошибки неопределенного поведения: *r1 имеет 'static время жизни, поэтому r3 имеет тип &'static String и, таким образом, переживает s. Создание ссылки из указателя требует большой осторожности.


Модификация статичных переменных


Чтение иммутабельной статичной переменной является безопасным:


static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("HELLO_WORLD: {HELLO_WORLD}");
}

Однако, учитывая риск возникновения гонки данных (data race), чтение и модификация мутабельных статичных переменных являются небезопасными:


static mut COUNTER: u32 = 0;

fn add_to_counter(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_counter(42);

    unsafe {
        println!("COUNTER: {COUNTER}");
    }
}

Программа в примере безопасна, поскольку она однопоточная. Однако компилятор Rust консервативен и предполагает худшее. Попробуйте удалить unsafe и увидите предупреждение компилятора о том, что изменение статики из нескольких потоков может привести к неопределенному поведению.


Использование изменяемой статики, как правило, является плохой идеей, но в некоторых случаях это может иметь смысл в низкоуровневом коде no_std, например, при реализации распределителя кучи (heap allocator) или работе с некоторыми API языка C.


Объединения


Объединения (unions) похожи на перечисления, но активное поле нужно отслеживать самостоятельно:


#[repr(C)]
union MyUnion {
    i: u8,
    b: bool,
}

fn main() {
    let u = MyUnion { i: 42 };
    println!("int: {}", unsafe { u.i });
    println!("bool: {}", unsafe { u.b }); // неопределенное поведение
}

В Rust объединения нужны очень редко, поскольку обычно можно использовать перечисления. Иногда они необходимы для взаимодействия с API библиотек языка C.


Если мы просто хотим интерпретировать байты как другой тип, нам, вероятно, понадобится std::mem::transmute или безопасная оболочка, такая как крейт zerocopy.


Небезопасные функции


Вызов небезопасных функций


Функция или метод могут быть помечены как unsafe, если у них есть дополнительные условия, которые должны быть соблюдены во избежание неопределенного поведения:


extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    let emojis = "?∈?";

    // Безопасно, потому что индексы находятся в правильном порядке, в пределах
    // фрагмента строки (string slice) и последовательности UTF-8
    unsafe {
        println!("эмодзи: {}", emojis.get_unchecked(0..4));
        println!("эмодзи: {}", emojis.get_unchecked(4..7));
        println!("эмодзи: {}", emojis.get_unchecked(7..11));
    }

    println!("количество символов: {}", count_chars(unsafe { emojis.get_unchecked(0..7) }));

    unsafe {
        // Потенциально неопределенное поведение
        println!("абсолютное значение -3 согласно C: {}", abs(-3));
    }

    // Несоблюдение требований кодировки UTF-8 нарушает безопасность памяти
    // println!("эмодзи: {}", unsafe { emojis.get_unchecked(0..3) });
    // println!("количество символов: {}", count_chars(unsafe {
    // emojis.get_unchecked(0..3) }));
}

fn count_chars(s: &str) -> usize {
    s.chars().count()
}

Создание небезопасных функций


Мы можем пометить собственные функции как unsafe, если они требуют соблюдения определенных условий во избежание неопределенного поведения:


/// Меняет значения, на которые указывают указатели
///
/// # Безопасность
///
/// Указатели должны быть валидными и правильно выровненными
unsafe fn swap(a: *mut u8, b: *mut u8) {
    let temp = *a;
    *a = *b;
    *b = temp;
}

fn main() {
    let mut a = 42;
    let mut b = 66;

    // Безопасно, поскольку...
    unsafe {
        swap(&mut a, &mut b);
    }

    println!("a = {}, b = {}", a, b);
}

Вызов небезопасных функций


get_unchecked, как и большинство функций _unchecked, небезопасна, поскольку может привести к неопределенному поведению, если диапазон неверен. abs небезопасна по другой причине: это внешняя функция (FFI). Вызов внешних функций обычно является проблемой только тогда, когда эти функции совершают действия с указателями, которые могут нарушить модель памяти Rust, но в целом любая функция C может иметь неопределенное поведение при определенных обстоятельствах.


Создание небезопасных функций


На самом деле в примере создания небезопасной функции мы не стали бы использовать указатели — такую функцию можно безопасно реализовать с помощью ссылок.


Обратите внимание, что небезопасный код разрешен внутри небезопасной функции без блока unsafe. Мы можем запретить это с помощью #[deny(unsafe_op_in_unsafe_fn)]. Попробуйте добавить его и посмотрите, что произойдет. Вероятно, это изменится в будущей версии Rust.


Небезопасные трейты


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


Например, крейт zerocopy имеет небезопасный трейт, который выглядит примерно так:


use std::mem::size_of_val;
use std::slice;

/// ...
/// # Безопасность
/// Тип должен иметь определенное представление и не иметь отступов (padding)
pub unsafe trait AsBytes {
    fn as_bytes(&self) -> &[u8] {
        unsafe {
            slice::from_raw_parts(
                self as *const Self as *const u8,
                size_of_val(self),
            )
        }
    }
}

// Безопасно, поскольку `u32` имеет определенное представление и не имеет отступов
unsafe impl AsBytes for u32 {}

В Rustdoc должен быть раздел # Safety (безопасность) с требованиями к безопасной реализации трейта.


Реальный раздел безопасности для AsBytes гораздо длиннее и сложнее.


Встроенные трейты Send и Sync являются небезопасными.


Упражнение: безопасная обертка FFI


Обратите внимание: это упражнение является сложным и опциональным.


В Rust имеется отличная поддержка вызова функций через интерфейс внешних функций (foreign function interface, FFI). Мы будем использовать это для создания безопасной оболочки для функций libc, которые используются в C для чтения имен файлов в директории.


Полезно изучить следующие страницы руководства:



Также полезно изучить документацию модуля std::ffi. Там вы найдете несколько типов строк, которые вам понадобятся для упражнения:


Типы Кодировка Назначение
str и String UTF-8 Обработка текста в Rust
CStr и CString NUL-завершенная Взаимодействие с функциями C
OsStr и OsString Зависит от ОС Взаимодействие с ОС

Вы будете выполнять следующие преобразования типов:


  • &str в CString — необходимо выделение пространства для завершающего символа \0
  • CString в *const i8 — для вызова функций C нужен указатель
  • *const i8 в &CStr — требуется средство обнаружения завершающего символа \0
  • &CStr в &[u8] — срез байтов — это универсальный интерфейс для "некоторых неизвестных данных"
  • &[u8] в &OsStr&OsStr — это шаг на пути к OsString, используйте OsStrExt для ее создания
  • &OsStr в OsString — данные в &OsStr нужно клонировать для того, чтобы иметь возможность их вернуть и повторно вызвать readdir

В Nomicon имеется отличный раздел о FFI.


mod ffi {
    use std::os::raw::{c_char, c_int};
    #[cfg(not(target_os = "macos"))]
    use std::os::raw::{c_long, c_uchar, c_ulong, c_ushort};

    // Непрозрачный тип. См. https://doc.rust-lang.org/nomicon/ffi.html.
    #[repr(C)]
    pub struct DIR {
        _data: [u8; 0],
        _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
    }

    // Макет в соответствии со страницей руководства Linux для `readdir(3)`, где `ino_t` и
    // `off_t` разрешаются согласно определениям в
    // /usr/include/x86_64-linux-gnu/{sys/types.h, bits/typesizes.h}.
    #[cfg(not(target_os = "macos"))]
    #[repr(C)]
    pub struct dirent {
        pub d_ino: c_ulong,
        pub d_off: c_long,
        pub d_reclen: c_ushort,
        pub d_type: c_uchar,
        pub d_name: [c_char; 256],
    }

    // Макет в соответствии со страницей руководства `macOS` для `dir(5)`.
    #[cfg(all(target_os = "macos"))]
    #[repr(C)]
    pub struct dirent {
        pub d_fileno: u64,
        pub d_seekoff: u64,
        pub d_reclen: u16,
        pub d_namlen: u16,
        pub d_type: u8,
        pub d_name: [c_char; 1024],
    }

    extern "C" {
        pub fn opendir(s: *const c_char) -> *mut DIR;

        #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))]
        pub fn readdir(s: *mut DIR) -> *const dirent;

        // См. https://github.com/rust-lang/libc/issues/414 и раздел
        // _DARWIN_FEATURE_64_BIT_INODE на странице руководства `macOS` для `stat(2)`.
        //
        // "Платформы, существовавшие до того, как эти обновления стали доступны"
        // (platforms that existed before these updates were available) относятся к
        // macOS (но не к iOS, wearOS и т.д.) на Intel и PowerPC.
        #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
        #[link_name = "readdir$INODE64"]
        pub fn readdir(s: *mut DIR) -> *const dirent;

        pub fn closedir(s: *mut DIR) -> c_int;
    }
}

use std::ffi::{CStr, CString, OsStr, OsString};
use std::os::unix::ffi::OsStrExt;

#[derive(Debug)]
struct DirectoryIterator {
    path: CString,
    dir: *mut ffi::DIR,
}

impl DirectoryIterator {
    fn new(path: &str) -> Result<DirectoryIterator, String> {
        // Вызываем `opendir` и возвращаем значение `Ok` при успехе
        // и `Err` с сообщением при неудаче
        unimplemented!()
    }
}

impl Iterator for DirectoryIterator {
    type Item = OsString;
    fn next(&mut self) -> Option<OsString> {
        // Продолжаем вызывать `readdir` до тех пор, пока не вернется указатель на значение NULL
        unimplemented!()
    }
}

impl Drop for DirectoryIterator {
    fn drop(&mut self) {
        // Вызывваем `closedir` по необходимости
        unimplemented!()
    }
}

fn main() -> Result<(), String> {
    let iter = DirectoryIterator::new(".")?;
    println!("файлы: {:#?}", iter.collect::<Vec<_>>());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;

    #[test]
    fn test_nonexisting_directory() {
        let iter = DirectoryIterator::new("no-such-directory");
        assert!(iter.is_err());
    }

    #[test]
    fn test_empty_directory() -> Result<(), Box<dyn Error>> {
        let tmp = tempfile::TempDir::new()?;
        let iter = DirectoryIterator::new(
            tmp.path().to_str().ok_or("Non UTF-8 character in path")?,
        )?;
        let mut entries = iter.collect::<Vec<_>>();
        entries.sort();
        assert_eq!(entries, &[".", ".."]);
        Ok(())
    }

    #[test]
    fn test_nonempty_directory() -> Result<(), Box<dyn Error>> {
        let tmp = tempfile::TempDir::new()?;
        std::fs::write(tmp.path().join("foo.txt"), "The Foo Diaries\n")?;
        std::fs::write(tmp.path().join("bar.png"), "<PNG>\n")?;
        std::fs::write(tmp.path().join("crab.rs"), "//! Crab\n")?;
        let iter = DirectoryIterator::new(
            tmp.path().to_str().ok_or("Non UTF-8 character in path")?,
        )?;
        let mut entries = iter.collect::<Vec<_>>();
        entries.sort();
        assert_eq!(entries, &[".", "..", "bar.png", "crab.rs", "foo.txt"]);
        Ok(())
    }
}

Решение
impl DirectoryIterator {
    fn new(path: &str) -> Result<DirectoryIterator, String> {
        // Вызываем `opendir` и возвращаем значение `Ok` при успехе
        // и `Err` с сообщением при неудаче
        let path =
            CString::new(path).map_err(|err| format!("Invalid path: {err}"))?;
        // Безопасность: `path.as_ptr()` не может возвращать NULL
        let dir = unsafe { ffi::opendir(path.as_ptr()) };
        if dir.is_null() {
            Err(format!("Could not open {:?}", path))
        } else {
            Ok(DirectoryIterator { path, dir })
        }
    }
}

impl Iterator for DirectoryIterator {
    type Item = OsString;
    fn next(&mut self) -> Option<OsString> {
        // Продолжаем вызывать `readdir` до тех пор, пока не вернется указатель на значение NULL
        // Безопасность: `self.dir` никогда не должно иметь значение NULL
        let dirent = unsafe { ffi::readdir(self.dir) };
        if dirent.is_null() {
            // Мы достигли конца директории
            return None;
        }
        // Безопасность: `dirent` не должно иметь значение NULL и `dirent.d_name` должно завершаться NUL
        let d_name = unsafe { CStr::from_ptr((*dirent).d_name.as_ptr()) };
        let os_str = OsStr::from_bytes(d_name.to_bytes());
        Some(os_str.to_owned())
    }
}

impl Drop for DirectoryIterator {
    fn drop(&mut self) {
        // Вызываем `closedir` по необходимости
        if !self.dir.is_null() {
            // Безопасноть: `self.dir` не должно иметь значение NULL
            if unsafe { ffi::closedir(self.dir) } != 0 {
                panic!("Could not close {:?}", self.path);
            }
        }
    }
}

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


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



Happy coding!




Теги:
Хабы:
Всего голосов 20: ↑19 и ↓1+25
Комментарии2

Публикации

Информация

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

Истории