Безопасная работа с памятью в низкоуровневом языке программирования, гарантированная самим языком. Для людей, близко знакомых с, к примеру, программированием на C, это утверждение может показаться оксюмороном, однако, разработчики языка Rust™ пытаются достичь именно этого при помощи трех концепций: ownership, borrowing и lifetimes. Попробуем разоблраться, как именно.
Начнем с ownership. Любое определение в расте "принадлежит" какому-то объекту, к примеру, функции, в которой он определен. И когда мы выходим из скоупа, раст освободит память, занимаемую всеми объектами, которые ему принадлежат. Строго, но справедливо, и пока понятно. Перейдем к более интересным концепциям - в любой момент времени на любой ресурс может быть ровно одна ссылка. Это уже более интересный факт, который ведет к некоторым спецеффектам, к примеру:
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]);
--------------------------------
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^
Произошло то, что мы разименовали ссылку на v, чтобы присвоить ее v2, и теперь мы не можем использовать v. Этот концепт разительно отличается от хендлинга ссылок в других языках программирования, и по началу здорово сбивает с толку. Посмотрим на еще более интересный спецеффект:
fn take(v: Vec<i32>) {
// what happens here isn’t important.
}
let v = vec![1, 2, 3];
take(v);
println!("v[0] is: {}", v[0]);
-------------------------------------------------
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^
Передав v в функцию take мы, на самом деле, передали принадлежность v этой функции. И да, это тоже означает, что мы больше не можем использовать v в изначальном коде. Такие пироги.
Важное уточнение! Такие спецеффекты наблюдаются только с типами, передающимися по ссылке. Если же тип реализует интерфейс `Copy`, то при создании новых ссылок через let значение будет скопировано, и мы можем пользоваться обеими переменными.
Хорошо, но если мы все же хотим использовать что-то после того, как передали это в функцию? Очень просто (нет) - мы можем вернуть принадлежность обратно, через возвращаемое значение функции!
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
Если вам кажется, что это не очень удобно, то создатели языка с вами согласны. Перейдем к концепту borrowing, который призван решить эту проблему.
Немного преобразуем последний листинг:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
// return the answer
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
// we can use v1 and v2 here!
Мы избавились от необходимости возвращать все входные данные вместе с выходными! Для этого мы приписали к каждому входному параметру &. Это дает сигнал компилятору, что мы хотим "одолжить" эти ресурсы, и потом вернуть в основную функцию. Отлично! Но есть нюанс. Мы не можем даже ничего добавить в наши векторы!
error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^
По умолчанию все ссылки иммутабельны. Можно ли сделать ее мутабельной? Да, при помощи ключевого слова mut. Звучит здорово, однако есть нюансы. А именно правило, что одновременно может существовать только одно из двух:
* неограниченное количество мутабельных ссылок (&T)
* одна и только одна мутабельная ссылка (&mut T)
Почему именно так? Обратимся к определению гонки данных:
There is a ‘data race’ when two or more pointers access the same memory location at the same time, where at least one of them is writing, and the operations are not synchronized.
Думаю, этого достаточно для вводной информации. В следующий раз рассмотрим более сложные концепты управления одалживанием переменных - именоваными lifetime'ами.