Перейти к основному содержанию

3. Владение (Ownership)

В Rust используется концепция владения (ownership) данными. Это некий набор разумных правил, предназначенных для обеспечения безопасной работы с памятью.

Суть владения

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

Соответственно, после того, как переменная, владеющая этими данными станет не нужна, переменная удаляется и память, содержащая данные, которыми она владела, освобождается.

А как определить, что переменная больше не нужна? Очень просто, когда она вышла за пределы области видимости.

Область видимости

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

Копирование и перемещение

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

Копирование данных

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

fn main() {
	let n = 8;
	let m = n;
	println!("m = {}", m);
	println!("n = {}", n);
}
Результат: m = 8 n = 8

Во второй строке программы переменную n связываем с ячейкой в памяти, в которую записано число 8. В третьей строке переменную m связываем с ячейкой в памяти, в которую скопировано значение 8 из ячейки памяти, связанной с переменной n.

Иными словами, переменная n владеет данными в своей ячейке памяти, а m владеет данными в своей. Каждая из переменных может что угодно делать со своим значением в своей ячейке памяти, не затрагивая другую.

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

Перечень копируемых типов данных (Copy Type):

  • Целочисленные типы,
  • числа с плавающей запятой,
  • bool,
  • char,
  • фиксированная строка &str,
  • а также кортежи и массивы (содержащие в себе исключительно вышеназванные типы).

Перемещение данных

А вот пример со строками работает по другому:

fn main() {
    let s: String = String::from("Пример");
    let t: String = s;
    println!("t = {}", t);
    println!("s = {}", s);
}
Ошибка: 5 | println!("s = {}", s); | ^ value borrowed here after move

Переменную s в строке 5 мы больше не можем использовать, т.к. она передала своё значение во владение переменной t. Почему мы наблюдаем именно такое поведение? Дело в том, что копирование строк и других сложных структур может быть очень тяжелой операцией. Поэтому разработчики языка Rust отказались от их копирования. Как вариант, можно было-бы оставить одни данные, но сделать так, чтобы обе переменные ссылались на одни и те же ячейки в памяти. Однако это приведет к проблеме повторного освобождении памяти когда переменные, одна за другой, начнут уходить из области видимости. Чтобы решить данную проблему разработчики Раста пошли на ухищрение. Да, они копируют указатель (ptr, len, capacity) переменной s в переменную t. Но при этом переменная s признаётся недействительной и её не придётся освобождать. По сути значение переменной s перемещено в t.

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


Схема перемещения данных:

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

Данные непримитивных типов могут иметь произвольный размер и хранятся в куче, затраты на их копирование в памяти велики, поэтому они перемещаются. Примеры таких типов (Move Type):

  • String Objects,
  • Vector,
  • а также кортежи и массивы (содержащие в себе вышеназванные типы).

Приведем образные примеры копирования и перемещения в виде следующих историй:

У Егора возникла идея и он рассказал её Нине.

fn main() {
	let i_egor: &str = "идея";
	let i_nina: &str = i_egor; // Егор поделился идеей с Ниной
	println!("У Нины появилась {}", i_nina);
	println!("У Егора осталась {}", i_egor);
}
Результат: У Нины появилась идея У Егора осталась идея

А в другой ситуации Егор отдает Нине книгу

fn main() {
	let real_egor: String = "книга".to_string();
	let real_nina: String = real_egor; // Егор отдал книгу Нине
	println!("У Нины появилась {}", real_nina);
	//println!("У Егора осталась {}", real_egor); // Ошибка, значение передано другой переменной.
	println!("У Егора теперь нет книги");
}
Результат: У Нины появилась книга У Егора теперь нет книги

Не стоит воспринимать эти примеры серьёзно. Разница обусловлена, конечно же, не значением переменной. Программе не важно это “идея” или “книга”. Важен тип данных. &str – примитивный тип данных, поэтому проще сделать дубликат в памяти, а копирование String может привести к ненужным затратам, поэтому значение этой переменной просто передается во владение другой переменной, а первая её теряет.

Клонирование данных

Что делать, если все-таки захочется сделать дубликат данных первой переменной, даже если мы используем перемещаемый тип данных? Для этого есть метод clone().

В нашем шуточном примере пусть Егор оставит у себя свою книгу, а для Нины отсканирует и распечатает идентичную копию.

fn main() {
	let real_egor: String = "книга".to_string();
	let real_nina: String = real_egor.clone(); // Егор отдал копию книги Нине
	println!("У Нины появилась {}", real_nina);
	println!("У Егора осталась {}", real_egor);
}
Результат: У Нины появилась книга У Егора осталась книга

Копирование и перемещение данных в функцию

В функциях все происходит по аналогии как с переменными. Если тип данных простой, то при передаче в функцию, он копируется в её переменную, а если сложный, то перемещается.

fn main() {
	let x: i8 = 5;
	use_var(x);
	println!("Значение х в main: {}",x);
}

fn use_var(var: i8) {
	println!("Значение х, переданное в use_var: {}", var);
}
Результат: Значение х, переданное в use_var: 5 Значение х в main 5

Здесь значение переменной х скопировалось в переменную var, поэтому мы можем использовать её и после передачи в функцию.

Попробуем проделать то же самое со строкой.

fn main() {
	let s: String = "Учиться легко".to_string();
	use_str_var(s);
	println!("Значение s в main: {}",s);
}

fn use_str_var(var: String) {
	println!("Значение s, переданное в use_str_var: {}", var);
}
Ошибка: 4 | println!("Значение s в main: {}",s); | ^ value borrowed here after move

Как мы и ожидали, значение переменной s не было скопировано в var, а перемещено в нее. Поэтому переменная s была удалена и стала недоступна.

Но что же делать, если нас не устраивает данная ситуация? Как решить проблему?

Антипример

Давайте попробуем использовать переданную переменную и вернуть использованное значение обратно.

fn main() {
	let mut s: String = "Учиться легко".to_string();
	s = use_str_var(s);
	println!(Значение х в main: {}", s);
}

fn use_str_var(var: String) -> String {
	println!("Значение s, переданное в use_str_var: {}", var);
	var 
}
Результат: Значение s, переданное в use_str_var: Учиться легко Значение s в main: Учиться легко

В этом примере мы сделали “финт ушами”. В прошлом примере мы просто вызвали функцию, которая как-то использовала s, а потом возобновлялся основной поток программы. Здесь мы изменяем переменную s значением, которое вернет внешняя функция. А на самом деле мы выполняем необходимую нам задачу и просто возвращаем исходное значение.

Мы вроде своего добились, но этот код просто ужасен. Надеюсь вы понимаете, как это уродливо и как потом тяжело искать логику в подобных ситуациях. Нам нужно другое решение.

Если пойти по простому пути, то можно клонировать значение переменной s:

fn main() {
	let s: String = "Учиться легко".to_string();
	use_str_var(s.clone());
	println!("Значение s в main: {}",s);
}

fn use_str_var(var: String) {
	println!("Значение s, переданное в use_str_var: {}", var);
}
Результат: Значение s, переданное в use_str_var: Учиться легко Значение s в main: Учиться легко

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

Для просмотра заданий и решений, а также публикации своих решений необходимо зарегистрироваться на сайте.

Всё бесплатно, мы просто хотим с вами познакомиться и понять насколько актуально то, что мы делаем.

© Клют И. А., 2022. Копирование контента возможно только с письменного разрешения автора.