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
— перебираемый тип, такой как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) разрешается следующим образом:
- Как относительный путь:
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
— необходимо выделение пространства для завершающего символа\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);
}
}
}
}
Это конец четвертой части руководства и руководства, в целом.
Материалы для более глубокого изучения рассмотренных тем:
- Книга/учебник по 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!