Основы
ГОЛОС ЗА КАДРОМ: в этой главе есть фундаментальная ошибка, точнее, её неясные очертания. Собственно, про неё и написана эта книга. Как только мы стали использовать
unsafe, у нас появилась возможность делать большие ошибки, при этом наши программы вполне компилироваться и на первый взгляд даже работают. Фундаментальная ошибка будет раскрыта в следующей главе. А пока — не используйте содержимое этой главы в продуктовом коде!
Самое время вернуться к основам. Как мы конструируем наш список?
Раньше мы просто делали:
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
}
Но теперь мы не можем использовать Option для tail:
> cargo build
error[E0308]: mismatched types
--> src/fifth.rs:15:34
|
15 | List { head: None, tail: None }
| ^^^^ expected *-ptr, found
| enum `std::option::Option`
|
= note: expected type `*mut fifth::Node<T>`
found type `std::option::Option<_>`
Мы могли бы использовать Option, но, в отличие от Box, *mut может принимать значение null.
Это значит, что мы не получаем преимуществ от оптимизации нулевого указателя.
Вместо этого мы используем null, чтобы представить вариант None.
Так как же нам получить указатель на null?
Есть несколько способов, но я предпочитаю вызывать std::ptr::null_mut().
Если хотите, можете писать 0 as *mut _, но на мой взгляд это выглядит неряшливо.
use std::ptr;
// определения...
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: ptr::null_mut() }
}
}
cargo build
warning: field is never used: `head`
--> src/fifth.rs:4:5
|
4 | head: Link<T>,
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `tail`
--> src/fifth.rs:5:5
|
5 | tail: *mut Node<T>,
| ^^^^^^^^^^^^^^^^^^
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
warning: field is never used: `head`
--> src/fifth.rs:12:5
|
12 | head: Link<T>,
| ^^^^^^^^^^^^^
Тише, компилятор, скоро мы используем все эти определения.
Ладно, давайте снова напишем push.
Теперь, вместо того, чтобы после вставки получить Option<&mut Node<T>>, мы сразу же получим *mut Node<T> из Box.
Мы без проблем можем это сделать, потому что содержимое Box имеет стабильный адрес, даже если мы перемещаем Box.
Конечно, это не безопасно, потому что если мы удалим Box, у нас останется указатель на освобождённую память.
Как получить сырой указатель из обычного указателя? С помощью приведения типа! К переменной, объявленной как сырой указатель, можно привести обычную ссылку.
let raw_tail: *mut _ = &mut *new_tail;
Теперь у нас есть вся необходимая информация. Мы можем переписать наш код так, чтобы он был более-менее похож на предыдущую эталонную реализацию.
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
// .is_null проверяет на null, эквивалент проверки на None
if !self.tail.is_null() {
// Если старый хвост был, обновляем его, чтобы он указывал на новый
self.tail.next = Some(new_tail);
} else {
// В противном случае, обновляем голову, чтобы указывала на него
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | self.tail.next = Some(new_tail);
| ----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
Что?
У нас есть указатель на Node, почему нам недоступно поле next?
Когда вы используете сырые указатели, Rust становится довольно грубым. Для доступа к содержимому сырого указателя нужно ручное разыменование, поскольку это крайней небезопасная операция. Так давайте, наконец, разыменуем:
*self.tail.next = Some(new_tail);
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | *self.tail.next = Some(new_tail);
| -----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
У-у-у-у, приоритет операторов.
(*self.tail).next = Some(new_tail);
> cargo build
error[E0133]: dereference of raw pointer is unsafe and requires
unsafe function or block
--> src/fifth.rs:31:13
|
31 | (*self.tail).next = Some(new_tail);
| ^^^^^^^^^^^^^^^^^ dereference of raw pointer
|
= note: raw pointers may be NULL, dangling or unaligned;
they can violate aliasing rules and cause data races:
all of these are undefined behavior
ЭТО. НЕ ДОЛЖНО. БЫТЬ. ТАКИМ. СЛОЖНЫМ.
Помните, я говорила, что Небезопасный Rust — это своего рода FFI для Безопасного Rust?
Ну вот, компилятор требует, чтобы мы явно указывали, где именно применяем FFI.
У нас есть два варианта.
Во-первых, мы можем пометить всю нашу функцию ключевым словом unsafe, при этом она становится функцией Небезопасного Rust и может быть вызвана только в небезопасном контексте.
Это не очень здорово, потому что мы хотим, чтобы нашим списком можно было пользоваться в безопасном коде.
Во-вторых, мы можем написать блок unsafe внутри нашей функции, чтобы обозначить границы FFI.
При этом сама функция будет считаться безопасной.
Хорошо, давайте сделаем:
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
if !self.tail.is_null() {
// Привет, компилятор. Я знаю, что рискую и обещаю
// быть хорошим программистом, который никогда не ошибается.
unsafe {
(*self.tail).next = Some(new_tail);
}
} else {
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
Ага!
Довольно интересно, но это пока единственное место, где нам пришлось написать небезопасный код.
Сырые указатели у нас повсюду, почему unsafe потребовался только здесь?
Дело в том, что, когда речь заходит об unsafe, Rust становится настоящим педантом, помешанным на правилах.
Мы вполне обоснованно хотим максимизировать множество программ на Безопасном Rust, потому что в отношении них мы можем быть гораздо более уверены.
Чтобы этого достигнуть, Rust старается выделять минимальную возможную небезопасную область.
Обратите внимание, что в других местах, где мы работали с сырыми указателями, мы либо присваивали им значение, либо просто проверяли их на null.
Если вы никогда не разыменовываете сырой указатель все эти операции совершенно безопасны. Вы просто читаете и записываете целые числа! Неприятности с сырым указателем могут возникнуть только при разыменовании. Так что Rust считает небезопасной только эту операцию, а все остальные — нет.
Супер. Педантично. Но технически корректно.
ГОЛОС ЗА КАДРОМ: Где-то на другом конце света инженер-электронщица чувствует, как мурашки бегут по её коже — должно быть, кто-то опять утверждает, что указатели — это всего лишь целые числа. Она смотрит на своё предложение по новой схеме аутентификации аппаратных указателей и скупая слеза бежит по её щеке. Инженер-сборщик по соседству не чувствует ничего — он давно научился всегда надевать тёплый свитер.
Из-за того, что действительно небезопасными является только часть операций, возникает интересная проблема.
Не смотря на то, что небезопасная область ограничена блоком unsafe, в действительности, она зависит от состояния, заложенного вне этого блока и даже вне функции!
Это то, что я называю небезопасным заражением.
Как только вы используете unsafe в модуле, весь модуль заражается небезопасностью.
Всё должно быть написано корректно, чтобы гарантировать соблюдение всех инвариантов в небезопасном коде.
Проблема заражения решается благодаря приватности. Вне нашего модуля поля нашей структуры абсолютно приватны, поэтому никто не другой не может произвольно менять наше состояние. До тех пор, пока никакая комбинация вызовов API не приводит к негативным последствиям, с точки зрения внешнего наблюдателя весь наш код безопасен! И, по сути, это ничем не отличается от FFI. Никого не волнует, что математическая библиотека Python вызывает код на C, если она предоставляет безопасный интерфейс.
Ладно, теперь давайте напишем pop, который практически дословно повторяет эталонную версию:
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
let head = *head;
self.head = head.next;
if self.head.is_none() {
self.tail = ptr::null_mut();
}
head.elem
})
}
Мы встретили ещё один сценарий, в котором безопасность зависит от состояния.
Если мы не обнулим указатель на хвост в этой функции, у нас не будет никаких проблем.
Однако последующие вызовы push начнут писать в висячий указатель!
Давайте протестируем:
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// Проверяем, что пустой список ведёт себя правильно
assert_eq!(list.pop(), None);
// Заполняем список
list.push(1);
list.push(2);
list.push(3);
// Проверяем обычное удаление
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// Вставляем новые значения, просто чтобы проверить, что ничего не сломается
list.push(4);
list.push(5);
// Проверяем обычное удаление
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// Проверяем граничный случай
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
// Check the exhaustion case fixed the pointer right
list.push(6);
list.push(7);
// Check normal removal
assert_eq!(list.pop(), Some(6));
assert_eq!(list.pop(), Some(7));
assert_eq!(list.pop(), None);
}
}
Это всё тот же тест стека, но результаты pop здесь находятся в обратном порядке.
Кроме того, я добавила пару дополнительных проверок в конец, чтобы убедиться, что при вызове pop указатель на хвост не повреждается.
cargo test
running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
Отличная работа!
ГОЛОС ЗА КАДРОМ: Ну-ну.