Исправление ошибок владения
Умение исправлять ошибки владения — это базовый навык в Rust. Когда проверка заимствований отклоняет ваш код, как реагировать? В этом разделе мы рассмотрим несколько типичных случаев ошибок владения. Каждый кейс представит функцию, отклонённую компилятором. Затем мы объясним, почему Rust отклоняет функцию, и покажем несколько способов её исправить.
Общая тема — понимание, является ли функция фактически безопасной или небезопасной. Rust всегда отклоняет небезопасную программу1. Но иногда Rust отклоняет и безопасную программу. Эти кейсы покажут, как реагировать на ошибки в обеих ситуациях.
Исправление небезопасной программы: возврат ссылки на стек
Первый кейс — возврат ссылки на стек, как мы обсуждали в разделе “Данные должны переживать все ссылки на них”. Вот функция, которую мы рассматривали:
fn return_a_string() -> &String {
let s = String::from("Hello world");
&s
}
Думая, как исправить эту функцию, нужно спросить: почему эта программа небезопасна? Здесь проблема со временем жизни данных, на которые ссылается функция. Если вы хотите передавать ссылку на строку, нужно убедиться, что исходная строка живёт достаточно долго.
В зависимости от ситуации есть четыре способа продлить время жизни строки. Один — передать владение строкой из функции, изменив &String на String:
#![allow(unused)] fn main() { fn return_a_string() -> String { let s = String::from("Hello world"); s } }
Другой вариант — вернуть строковый литерал, который живёт вечно (обозначается 'static). Это решение подходит, если строка никогда не будет изменяться, и тогда выделение в куче не нужно:
#![allow(unused)] fn main() { fn return_a_string() -> &'static str { "Hello world" } }
Ещё один вариант — отложить проверку заимствований на время выполнения, используя сборку мусора. Например, можно использовать указатель с подсчётом ссылок:
#![allow(unused)] fn main() { use std::rc::Rc; fn return_a_string() -> Rc<String> { let s = Rc::new(String::from("Hello world")); Rc::clone(&s) } }
Мы обсудим подсчёт ссылок подробнее в главе 15.4 “Rc<T>, умный указатель с подсчётом ссылок”. Вкратце, Rc::clone клонирует только указатель на s, а не сами данные. Во время выполнения Rc проверяет, когда последний Rc, указывающий на данные, будет удалён, и затем освобождает данные.
Ещё один вариант — чтобы вызывающий код предоставил “слот” для строки с помощью изменяемой ссылки:
#![allow(unused)] fn main() { fn return_a_string(output: &mut String) { output.replace_range(.., "Hello world"); } }
При такой стратегии вызывающий код отвечает за создание места для строки. Этот стиль может быть многословным, но также может быть более эффективным по памяти, если вызывающему нужно тщательно контролировать, когда происходят выделения.
Какой стратегии следует выбрать, зависит от вашего приложения. Но ключевая идея — распознать корневую проблему, лежащую в основе ошибки владения. Как долго должна жить моя строка? Кто должен отвечать за её освобождение? Когда у вас есть чёткий ответ на эти вопросы, остаётся лишь изменить ваш API соответствующим образом.
Исправление небезопасной программы: недостаточно прав
Другая распространённая проблема — попытка изменить данные только для чтения или попытка удалить данные через ссылку. Например, представим, что мы пытаемся написать функцию stringify_name_with_title. Эта функция должна создать полное имя человека из вектора частей имени, включая дополнительный титул.
Эта программа отклоняется проверкой заимствований, потому что name — это неизменяемая ссылка, но name.push(..) требует права W. Эта программа небезопасна, потому что push может сделать недействительными другие ссылки на name за пределами stringify_name_with_title, например:
В этом примере ссылка first на name[0] создаётся до вызова stringify_name_with_title. Функция name.push(..) перераспределяет содержимое name, что делает first недействительной, вызывая чтение освобождённой памяти в println.
Так как исправить этот API? Одно простое решение — изменить тип name с &Vec<String> на &mut Vec<String>:
fn stringify_name_with_title(name: &mut Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
Но это не хорошее решение! Функции не должны изменять свои входные данные, если вызывающий код не ожидает этого. Человек, вызывающий stringify_name_with_title, вероятно, не ожидает, что его вектор будет изменён этой функцией. Другая функция, например add_title_to_name, может ожидать изменения входных данных, но не наша.
Другой вариант — взять владение именем, изменив &Vec<String> на Vec<String>:
fn stringify_name_with_title(mut name: Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
Но это тоже не хорошее решение! Очень редко функции в Rust берут владение над структурами данных в куче, такими как Vec и String. Эта версия stringify_name_with_title сделает входной name непригодным для использования, что очень неудобно для вызывающего кода, как мы обсуждали в начале раздела “Ссылки и заимствование”.
Таким образом, выбор &Vec на самом деле хорош, и мы не хотим его менять. Вместо этого мы можем изменить тело функции. Есть много возможных исправлений, которые различаются по объёму используемой памяти. Один вариант — клонировать входной name:
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut name_clone = name.clone();
name_clone.push(String::from("Esq."));
let full = name_clone.join(" ");
full
}
Клонируя name, мы можем изменять локальную копию вектора. Однако клон копирует каждую строку во входном векторе. Мы можем избежать ненужных копий, добавив суффикс позже:
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut full = name.join(" ");
full.push_str(" Esq.");
full
}
Это решение работает, потому что slice::join уже копирует данные из name в строку full.
В общем, написание функций на Rust — это тщательный баланс в запросе правильного уровня прав. Для этого примера наиболее идиоматично ожидать только права на чтение для name.
Исправление небезопасной программы: псевдонимы и изменение структуры данных
Другая небезопасная операция — использование ссылки на данные в куче, которые освобождаются другим псевдонимом. Например, вот функция, которая получает ссылку на самую длинную строку в векторе, а затем использует её, изменяя вектор:
Примечание: этот пример использует [итераторы] и [замыкания] для краткого нахождения ссылки на самую длинную строку. Мы обсудим эти возможности в следующих главах, а здесь дадим интуитивное представление о том, как они работают.
Эта программа отклоняется проверкой заимствований, потому что let largest = .. удаляет права W для dst. Однако dst.push(..) требует права W. Снова спросим: почему эта программа небезопасна? Потому что dst.push(..) может освободить содержимое dst, делая ссылку largest недействительной.
Чтобы исправить программу, ключевое понимание — нужно сократить время жизни largest, чтобы оно не перекрывалось с dst.push(..). Один вариант — клонировать largest:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest: String = dst.iter().max_by_key(|s| s.len()).unwrap().clone(); for s in src { if s.len() > largest.len() { dst.push(s.clone()); } } } }
Однако это может вызвать потерю производительности из-за выделения и копирования данных строки.
Другой вариант — выполнить все сравнения длин сначала, а затем изменить dst afterwards:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest: &String = dst.iter().max_by_key(|s| s.len()).unwrap(); let to_add: Vec<String> = src.iter().filter(|s| s.len() > largest.len()).cloned().collect(); dst.extend(to_add); } }
Однако это также вызывает потерю производительности из-за выделения вектора to_add.
Последний вариант — скопировать длину largest, так как нам на самом деле не нужно содержимое largest, только его длина. Это решение, пожалуй, самое идиоматичное и производительное:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest_len: usize = dst.iter().max_by_key(|s| s.len()).unwrap().len(); for s in src { if s.len() > largest_len { dst.push(s.clone()); } } } }
Все эти решения объединяет ключевая идея: сокращение времени жизни заимствований dst так, чтобы они не перекрывались с изменением dst.
Исправление небезопасной программы: копирование vs. перемещение из коллекции
Распространённая путаница для изучающих Rust возникает при копировании данных из коллекции, например вектора. Например, вот безопасная программа, которая копирует число из вектора:
Операция разыменования *n_ref ожидает только права R, которые есть у пути *n_ref. Но что произойдёт, если изменить тип элементов вектора с i32 на String? Тогда у нас больше не будет необходимых прав:
Первая программа скомпилируется, а вторая — нет. Rust выдаёт следующее сообщение об ошибке:
error[E0507]: cannot move out of `*s_ref` which is behind a shared reference
--> test.rs:4:9
|
4 | let s = *s_ref;
| ^^^^^^
| |
| move occurs because `*s_ref` has type `String`, which does not implement the `Copy` trait
Проблема в том, что вектор v владеет строкой “Hello world”. Когда мы разыменовываем s_ref, это пытается взять владение строкой из вектора. Но ссылки — это неуправляющие указатели; мы не можем взять владение через ссылку. Поэтому Rust жалуется, что мы “не можем переместить из […] общей ссылки”.
Но почему это небезопасно? Мы можем проиллюстрировать проблему, имитируя отклонённую программу:
Что происходит здесь — двойное освобождение. После выполнения let s = *s_ref и v, и s думают, что владеют “Hello world”. После удаления s строка “Hello world” освобождается. Затем удаляется v, и неопределённое поведение происходит при втором освобождении строки.
Примечание: после выполнения
s = *s_refнам даже не нужно использоватьvилиs, чтобы вызвать неопределённое поведение через двойное освобождение. Как только мы перемещаем строку изs_ref, неопределённое поведение произойдёт, как только элементы будут удалены.
Однако это неопределённое поведение не происходит, когда вектор содержит элементы i32. Разница в том, что копирование String копирует указатель на данные в куче. Копирование i32 этого не делает.
В технических терминах Rust говорит, что тип i32 реализует типаж Copy, а String не реализует Copy (мы обсудим типажи в следующей главе).
Таким образом, если значение не владеет данными в куче, его можно скопировать без перемещения. Например:
i32не владеет данными в куче, поэтому его можно скопировать без перемещения.Stringвладеет данными в куче, поэтому его нельзя скопировать без перемещения.&Stringне владеет данными в куче, поэтому его можно скопировать без перемещения.
Примечание: Одно исключение из этого правила — изменяемые ссылки. Например,
&mut i32— не копируемый тип. Так что если вы делаете что-то вроде:let mut n = 0; let a = &mut n; let b = a;Тогда
aнельзя использовать после присвоенияb. Это предотвращает одновременное использование двух изменяемых ссылок на одни и те же данные.
Итак, если у нас есть вектор не-Copy типов, таких как String, как безопасно получить доступ к элементу вектора? Вот несколько разных способов сделать это безопасно. Во-первых, можно избежать взятия владения строкой и просто использовать неизменяемую ссылку:
fn main() {
let v: Vec<String> = vec![String::from("Hello world")];
let s_ref: &String = &v[0];
println!("{s_ref}!");
}
Во-вторых, можно клонировать данные, если нужно получить владение строкой, оставив вектор нетронутым:
fn main() {
let v: Vec<String> = vec![String::from("Hello world")];
let mut s: String = v[0].clone();
s.push('!');
println!("{s}");
}
Наконец, можно использовать метод, такой как Vec::remove, чтобы переместить строку из вектора:
fn main() {
let mut v: Vec<String> = vec![String::from("Hello world")];
let mut s: String = v.remove(0);
s.push('!');
println!("{s}");
assert!(v.len() == 0);
}
Исправление безопасной программы: изменение разных полей кортежа
Приведённые выше примеры — случаи, когда программа небезопасна. Rust также может отклонять безопасные программы. Одна распространённая проблема — Rust пытается отслеживать права на очень детальном уровне. Однако Rust может объединять два разных места как одно и то же.
Сначала посмотрим на пример детального отслеживания прав, который проходит проверку заимствований. Эта программа показывает, как можно заимствовать одно поле кортежа и записывать в другое поле того же кортежа:
Инструкция let first = &name.0 заимствует name.0. Это заимствование удаляет права WO у name.0. Оно также удаляет права WO у name. (Например, нельзя передать name в функцию, принимающую значение типа (String, String).) Но name.1 по-прежнему сохраняет право W, поэтому операция name.1.push_str(...) допустима.
Однако Rust может потерять точное понимание, какие места заимствованы. Например, представим, что мы рефакторим выражение &name.0 в функцию get_first. Обратите внимание, как после вызова get_first(&name) Rust теперь удаляет право W у name.1:
Теперь мы не можем сделать name.1.push_str(..)! Rust вернёт эту ошибку:
error[E0502]: cannot borrow `name.1` as mutable because it is also borrowed as immutable
--> test.rs:11:5
|
10 | let first = get_first(&name);
| ----- immutable borrow occurs here
11 | name.1.push_str(", Esq.");
| ^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
12 | println!("{first} {}", name.1);
| ----- immutable borrow later used here
Это странно, ведь программа была безопасна до редактирования. Сделанное изменение существенно не меняет поведение во время выполнения. Так почему важно, что мы поместили &name.0 в функцию?
Проблема в том, что Rust не смотрит на реализацию get_first при решении, что get_first(&name) должно заимствовать. Rust смотрит только на сигнатуру типа, которая просто говорит “некоторый String во входных данных заимствуется”. Rust консервативно решает, что тогда и name.0, и name.1 заимствуются, и устраняет права на запись и владение для обоих.
Помните, ключевая идея в том, что программа выше безопасна. В ней нет неопределённого поведения! Будущая версия Rust может быть достаточно умной, чтобы позволить ей скомпилироваться, но сегодня она отклоняется. Так как обойти проверку заимствований сегодня? Один вариант — встроить выражение &name.0, как в исходной программе. Другой вариант — отложить проверку заимствований на время выполнения с помощью [ячеек], что мы обсудим в будущих главах.
Исправление безопасной программы: изменение разных элементов массива
Подобная проблема возникает при заимствовании элементов массива. Например, посмотрим, какие места заимствуются, когда мы берём изменяемую ссылку на массив:
Проверка заимствований Rust не содержит отдельных мест для a[0], a[1] и так далее. Она использует одно место a[_], представляющее все индексы a. Rust делает это, потому что не всегда может определить значение индекса. Например, представьте более сложный сценарий:
let idx = a_complex_function();
let x = &mut a[idx];
Каково значение idx? Rust не будет угадывать, поэтому предполагает, что idx может быть чем угодно. Например, представим, что мы пытаемся прочитать из одного индекса массива, одновременно записывая в другой:
Однако Rust отклоняет эту программу, потому что a отдала свои права на чтение x. Сообщение об ошибке компилятора говорит то же самое:
error[E0502]: cannot borrow `a[_]` as immutable because it is also borrowed as mutable
--> test.rs:4:9
|
3 | let x = &mut a[1];
| --------- mutable borrow occurs here
4 | let y = &a[2];
| ^^^^^ immutable borrow occurs here
5 | *x += *y;
| -------- mutable borrow later used here
Опять же, эта программа безопасна. Для таких случаев Rust часто предоставляет функцию в стандартной библиотеке, которая может обойти проверку заимствований. Например, мы могли бы использовать slice::split_at_mut:
fn main() {
let mut a = [0, 1, 2, 3];
let (a_l, a_r) = a.split_at_mut(2);
let x = &mut a_l[1];
let y = &a_r[0];
*x += *y;
}
Вы можете спросить, но как реализован split_at_mut? В некоторых библиотеках Rust, особенно в основных типах, таких как Vec или slice, вы часто найдёте unsafe блоки. unsafe блоки позволяют использовать “сырые” указатели, которые не проверяются на безопасность проверкой заимствований. Например, мы могли бы использовать unsafe-блок для выполнения нашей задачи:
fn main() {
let mut a = [0, 1, 2, 3];
let x = &mut a[1] as *mut i32;
let y = &a[2] as *const i32;
unsafe { *x += *y; } // НЕ ДЕЛАЙТЕ ЭТОГО, если не знаете, что делаете!
}
Небезопасный код иногда необходим для обхода ограничений проверки заимствований. Как общая стратегия, скажем, проверка заимствований отклоняет программу, которую вы считаете фактически безопасной. Тогда вам стоит искать функции стандартной библиотеки (такие как split_at_mut), содержащие unsafe блоки, которые решают вашу проблему. Мы обсудим небезопасный код подробнее в Главе 20. Пока просто имейте в виду, что небезопасный код — это то, как Rust реализует определённые иначе невозможные шаблоны.
Резюме
При исправлении ошибки владения вы должны спросить себя: моя программа фактически небезопасна? Если да, то нужно понять коренную причину небезопасности. Если нет, то нужно понять ограничения проверки заимствований, чтобы их обойти.
-
Эта гарантия применяется к программам, написанным на “безопасном подмножестве” Rust. Если вы используете
unsafeкод или вызываете небезопасные компоненты (например, библиотеку на C), то должны проявить дополнительную осторожность, чтобы избежать неопределённого поведения. ↩