Шаг 7. Владение и заимствование
Зачем#
В языках без сборщика мусора программист отвечает за освобождение памяти; в языках со сборщиком платят паузами и непредсказуемостью. OxL выбирает третий путь: у каждого значения ровно один владелец; когда владелец выходит из области видимости, значение уничтожается. Из этого одного правила следует отсутствие утечек, двойного освобождения и гонок данных между задачами — и всё это проверяется во время компиляции. Явных «времён жизни» (как в Rust) нет: проверка устроена проще (§7.2).
Три правила:
- Присваивание, передача в функцию и возврат перемещают значение (типы
Copy— числа,str,bool— просто копируются). &x— разделяемое заимствование (только чтение),&var x— исключительное (чтение и запись). Пока есть&var, других ссылок нет; пока есть&, менять нельзя.x.clone()— явная независимая копия.
Пример#
fn total(xs: List<i32>) -> i32 pure { // параметр по значению: функция становится владельцем
var s = 0
for x in xs { s += x }
s
}
fn count(xs: &List<i32>) -> usize pure => xs.len() // &T — чтение без передачи владения
fn addOne(xs: &var List<i32>) { // &var T — исключительное изменяемое заимствование
xs.push(1)
}
fn main() io {
var xs = [10, 20, 30]
print("{count(&xs)}") // &xs — одолжить; xs остаётся нашим
addOne(&var xs) // &var нужно писать явно
let copy = xs.clone() // clone — независимая копия
print("{total(xs)}") // xs перемещён в total; дальше его использовать нельзя
print("{copy.len()} {copy}")
}3 61 4 [10, 20, 30, 1]
Разбор#
total(xs: List<i32>)— параметр по значению: вызвавtotal(xs), вы отдали список. После этогоxsиспользовать нельзя.count(xs: &List<i32>)и вызовcount(&xs)— функция только читает и ничего не забирает.addOne(&var xs)—&varпишется явно на месте вызова: читатель сразу видит, что функция может изменить аргумент.clone()дорогое (копирует данные), поэтому оно никогда не происходит неявно.
Типичные ошибки#
Использование после перемещения:
fn main() {
let xs = [1, 2, 3]
let ys = xs // значение переехало в ys
print(xs.len()) // ошибка: xs больше не владеет списком
}use-after-move
Диагностика use-after-move подсвечивает и место перемещения. Исправления: передать &xs, сделать
xs.clone() или вернуть переданное значение из функции.
Изменение во время чтения:
fn main() {
var xs = [1, 2, 3]
let first = &xs
xs.push(4) // изменять нельзя, пока жив &xs
print(first.len())
}borrow-conflict
borrow-conflict: пока first держит заимствование списка, push запрещён — иначе указатель мог бы
повиснуть после перераспределения памяти. Заимствование живёт до последнего использования, поэтому достаточно
переставить строки. Ссылки нельзя хранить в полях data и передавать в другие задачи (borrow-escape): для
общих данных используется freeze(x) — неизменяемое значение, которое можно разделять между задачами (§7.3).
Что запомнить#
- Один владелец; перемещение по умолчанию;
&читать,&varменять,cloneкопировать. - Типы
Copy(числа,bool,str,bytes) перемещения не боятся. - Ошибки владения — ошибки компиляции, а не падения в рантайме.
Дальше: эффекты.