Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Владение

(ownership)

Главная отличительная особенность Rust от других языков заключается в том, что он, не имея сборщика мусора, предлагает производительность уровня языков с ручным управлением памятью. При этом Rust не требует ручного управления памятью и гарантирует отсутствие утечек памяти. Такой результат достигается за счёт концепции владения данными.

Дело в том, что в Rust любой объект непримитивного типа должен иметь только одного владельца. Владелец — это переменная, которой присвоен объект. Поэтому когда значение одной переменной присваивается другой переменной, владение объектом переходит от первой переменной ко второй. При этом первая переменная становится недействительной.

Рассмотрим пример:

fn main() {
  let s1 = String::from("some string");
  let s2 = s1; // владение строкой переходит от s1 к s2
  // В этом месте переменная s1 уже недействильна.

  println!("{}", s2); // Теперь можно работать только с s2, но не s1
}

Попытка обратиться к недействильной переменной приведёт к ошибке компиляции:

fn main() {
  let s1 = String::from("some string");
  let s2 = s1;

  println!("{}", s1);
// 2 |   let s1 = String::from("some string");
//   |       -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
// 3 |   let s2 = s1;
//   |            -- value moved here
// 4 |
// 5 |   println!("{}", s1);
//   |                  ^^ value borrowed here after move
}

Тот факт, что у любого объекта есть только один владелец, позволяет компилятору однозначно понять в каком месте память, занимаемая объектом, должна быть очищена. И это место — там, где владелец объекта прекращает своё существование.

Таким образом, в тех местах где переменные прекращают своё существование, компилятор вставляет код вызова деструктора для данных, которыми переменные владели.

Такой подход позволяет управлять памятью без сборщика мусора, при этом гарантировать отсутствие утечек.

Механизм компилятора Rust, который отслеживает время жизни объектов и гарантирует, что память будет очищена, когда нужно, и что в коде не будет обращений к уже очищенной памяти, называется борроу-чекером (borrow-checker).

Владение и скоупы

Как мы знаем, время жизни переменной привязано к скоупу, в котором эта переменная объявлена: когда скоуп завершается, все входящие в него переменные пропадают, а принадлежавшая им память очищается.

Однако переменная, объявленная внутри скоупа, может “отдать” свои данные другой переменной, которая объявлена за пределами этого скоупа.

fn main() {
  let s1;
  {
    let s2 = String::from("some string");
    s1 = s2; // значение отдаётся переменной из внешнего скоупа
  }
  println!("{s1}"); // OK
}

Этот пример тривиален, однако по нему хорошо видно, как работает передача владения при переходе между скоупами. Эти знания нам понадобятся в дальнейшем, когда мы будем разбирать Лайфтаймы

Передача владения

Мы уже знаем, что передача владения объектом происходит при присваивании, но есть и другие сценарии.

Вот полный список операций, при которых происходит передача владения объектом:

  • присваивание
  • передача объекта в функцию в качестве аргумента
  • возврат значения из функции
  • захват объекта замыканием (этот сценарий мы рассмотрим позже)

Давайте рассмотрим сценарий передачи владения при вызове функции:

fn main() {
    let name = String::from("Stas");

    // Строка из переменной name уходит в функцию, делая name недействительной
    let greeting = greet(name);
    // <- Здесь переменная name уже не может быть использована

    println!("{}", greeting); // Hello Stas!!!
}

fn greet(name: String) -> String {
    // объект строки, возвращаемый вызовом format, перемещается в вызывающий код
    format!("Hello {}!!!", name)
}

Теперь, когда мы разобрались с тем, что передача переменной в качестве аргумента уничтожает эту самую переменную, давайте посмотрим на пример, который выглядит вполне обычно с точки зрения большинства языков программирования, но станет сюрпризом для изучающих Rust.

Допустим, мы хотим написать программу, которая выводит на консоль строку (неважно откуда мы её берём), а также информацию о длине этой строки. Для того, чтобы подсчитать длину строки, мы сделаем отдельную функцию, которая будет просто вызывать len() на переданной строке.

fn len_of_string(s: String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("aaa");
    let len = len_of_string(s);
    println!("{}", s); // <- переменная s уже недействильна здесь
}

И вот здесь нас сразу ожидает проблема: мы пытаемся распечатать переменную s, которая уже не действительна, так как она отдала владение своими данными в вызов функции на предыдущей строке.

Как мы можем решить эту проблему? Исключительно абсурдности ради давайте вспомним, что при помощи кортежей мы можем возвращать из функции несколько значений, а значит, мы можем вернуть обратно объект, переданный как аргумент.

fn len_of_string(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

fn main() {
    let s = String::from("aaa");
    let (s, len) = len_of_string(s);
    println!("Len of {s} is {len}"); // "Len of aaa is 3"
}

Выглядит странно, но хорошо демонстрирует, как работает перемещение владения объектом при вызове функций.

К счастью, Rust предоставляет куда более удобный механизм для решения этой проблемы — одалживание.

Note

В стандартной библиотеке Rust есть функция drop, которая уничтожает переданный ей объект.

#![allow(unused)]
fn main() {
let s = String::from("aaa");
drop(s);
}

Если посмотреть реализацию функции drop (мы пока не знакомы с генериками, поэтому не можем полностью понять её сигнатуру), то мы увидим, что она не делает ничего.

#![allow(unused)]
fn main() {
pub fn drop<T>(_x: T) {}
}

Уничтожение объекта происходит просто за счёт того, что функция drop, забирает себе владение над объектом и не передаёт его никому дальше.

Одалживание (borrowing)

Вместо того, чтобы передавать в качестве аргумента значение переменной, мы можем передавать ссылку на это значение. Таким образом, владение объектом остаётся за переменной, и мы лишь даём функции попользоваться объектом.

fn len_of_string(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("aaa");
    let len = len_of_string(&s);
    println!("Len of {s} is {len}");
}

Такая передача аргумента по ссылке в Rust называется “одалживаем” (borrowing). Это название обусловлено тем, что функция словно берёт объект попользоваться, но возвращает его собственнику после завершения своей работы.

Tip

Если бы мы писали подобную функцию для реальной программы, мы бы разумеется сделали тип аргумента в len_of_string не ссылкой на строку &String, а слайсом &str, что позволило бы вызывать функцию как для строк String, так и для строковых литералов.
Мы написали эту функцию таким образом — исключительно для того чтобы было проще объяснить одалживание.

Безопасность ссылок (referential safety)

Из раздела выше мы уже знаем, что в Rust можно взять ссылку на объект и передать её в функцию.

Но давайте рассмотрим такой сценарий:

1) Мы создаём вектор с буфером на 3 элемента и заполняем его значениями.

let mut vector: Vec<i32> = Vec::with_capacity(3);
vector.push(1);
vector.push(2);
vector.push(3);

2) Дальше мы берём немутабельную ссылку на второй элемент вектора. Эта ссылка “указывает” непосредственно на адрес в памяти, по которому хранится второй элемент.

let reference: &i32 = &vector[1];

3) Ссылка на второй элемент еще “жива”, но теперь мы берём еще и мутабельную ссылку на весь вектор целиком. С её помощью мы добавляем в вектор еще один элемент. Буфер вектора был уже заполнен, поэтому в куче аллоцируется новый буфер большего размера, и в него копируются все элементы из старого буфера. После этого в новый буфер добавляется новый элемент, а старый буфер очищается из памяти.

let vec_ref = &mut vector;
vec_ref.push(4);

Вопрос: на что теперь указывает ссылка, которая ссылалась на второй элемент вектора? Разумеется, такая ссылка становится недействительной.

К счастью, Rust является безопасным языком, поэтому компилятор не позволит написать такой код. Выдавая ошибку, он будет руководствоваться правилом безопасности ссылок:

В любом месте кода для любого объекта может существовать либо только одна мутабельная ссылка, либо любое количество немутабельных ссылок.

Также это правило позволяет сделать безопасным использование ссылок в многопоточной среде. Ведь если мы только читаем данные, то это можно делать безопасно из любого количества параллельных потоков. При этом любая операция чтения данных будет потенциально опасной, если существует параллельный поток, который изменяет эти данные.

Чтобы лучше понять как работает контроль ссылок, давайте рассмотрим более простой пример:

fn main() {
    let mut s = String::from("x");

    let r1 = &mut s; // <-- взятие мутабельной ссылки
    let r2 = & s;    // <-- попытка взять немутабельную ссылку

    println!("{r1}, {r2}");
}

Компилятор выдаст ошибку:

4 |   let r1 = &mut s; // <-- взятие мутабельной ссылки
  |            ------ mutable borrow occurs here
5 |   let r2 = & s;    // <-- попытка взять немутабельную ссылку
  |            ^^^ immutable borrow occurs here
6 |
7 |   println!("{r1}, {r2}");
  |              -- mutable borrow later used here

Перемещение владения для примитивных типов

Все вышеописанные правила владения данными не относятся к примитивным типам.

Примитивные типы занимают мало памяти и не владеют какими-то дополнительными ресурсами, поэтому в тех ситуациях, где для составных типов происходит передача владения, для примитивных типов просто выполняется копирование.

fn increment(a: i32) -> i32 {
    a + 1
}

fn main() {
    let x = 5;
    let y = increment(x);
    println!("x={}, y={}", x, y);
}

Цикл for и владение

Еще один интересный момент, который следует рассмотреть — это то, как владение работает при итерировании циклом for.

Рассмотрим пример:

fn main() {
    let arr = [String::from("1"), String::from("2"), String::from("3")];

    for n in arr {
        println!("{n}");
    }

    println!("{arr:?}");
}

Этот пример не скомпилируется, так как в цикле for, на каждом витке итерации следующий элемент массива присваивается переменной n для последующего использования в теле цикла. Присваивание приводит к передаче владения.

В итоге, мы уничтожили массив просто распечатав его.

Эта проблема решается так же просто, как и проблема с передачей аргумента в функцию — путём взятия ссылки.

fn main() {
    let arr = [String::from("1"), String::from("2"), String::from("3")];

    for n in &arr {
        println!("{n}");
    }

    println!("{arr:?}");
}

Теперь когда мы заменили arr на &arr в заголовке цикла, на каждой итерации в переменную n присваивается не очередной элемент массива, а ссылка на него. А значит сам массив не уничтожается.

Note

После этого примера может показаться, что написание программ на Rust превращается в постоянную борьбу с компилятором. На первых порах так оно может и быть. Однако после того как вы привыкнете в концепции владения данными, вы будете на автомате писать так, как надо.