Hello world!
Представляю вашему вниманию четвертую и последнюю часть практического руководства по Rust.
Другой формат, который может показаться вам более удобным.
Руководство основано на Comprehensive Rust — руководстве по Rust от команды Android в Google и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить ?
В этой части мы рассмотрим следующие темы:
- итераторы (iterators): глубокое погружение в трейт
Iterator - модули и видимость
- тестирование
- обработка ошибок: паника,
Resultи оператор? - небезопасный
Rust: случаи, когда безопасногоRustоказывается недостаточно
Материалы для более глубокого изучения названных тем:
- Книга/учебник по Rust (на русском языке) — главы 7, 9, 11, 13, 14 и 19
- rustlings — упражнения 10, 13, 15, 17 и 18
- Rust на примерах (на русском языке) — примеры 10-12, 18, 21 и 22
- Rust by practice — упражнения 13 и 14
Также см. Большую шпаргалку по 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— перебираемый тип, такой какi8IntoIter— тип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) разрешается следующим образом:
- Как относительный путь:
fooилиself::fooссылается наfooв текущем модулеsuper::fooссылается наfooв родительском модуле
- Как абсолютный путь:
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::ok — Result в 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— необходимо выделение пространства для завершающего символа\0CStringв*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); } } } }
Это конец четвертой части руководства и руководства, в целом.
Материалы для более глубокого изучения рассмотренных тем:
- Книга/учебник по Rust (на русском языке) — главы 7, 9, 11, 13, 14 и 19
- rustlings — упражнения 10, 13, 15, 17 и 18
- Rust на примерах (на русском языке) — примеры 10-12, 18, 21 и 22
- Rust by practice — упражнения 13 и 14
Happy coding!
ссылка на оригинал статьи https://habr.com/ru/articles/801201/

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