Концепция владения в Rust на примерах, часть 2
Заимствование
Многие ресурсы слишком «дороги» с точки зрения времени или памяти для того, чтобы их можно было копировать при каждом переназначении. В этих случаях Rust предлагает возможность заимствования.
Ранее мы видели, что не копируемую величину нельзя переназначить. Мы можем решить эту проблему, взяв вместо этого значение. Для этого мы ставим перед переменной-правопреемником символ амперсанда (&).
#[derive(Debug)] // больше нет копии
struct Person {
age: u8
}
fn main() {
let alice = Person { age: 8 };
let bob = &alice; // bob borrows alice
println!("alice: {:?}\nbob: {:?}", alice, bob);
}
Несмотря на отсутствие типажа Copy у Person, приведенный выше код компилируется и дает тот же результат, что и раньше:
alice: Person { age: 42 }
bob: Person { age: 42 }
Точно так же некопируемая величина может быть передана в качестве аргумента функции, если она заимствовано. Обратите внимание на использование обозначения заимствования (&) в сигнатуре для суммы:
fn sum(vector: &Vec) -> i32 { // сигнатура заимствования
let mut sum = 0;
for item in vector {
sum = sum + item
}
sum
}
fn main() {
let v = vec![1,2,3];
let v_ref = &v; // v_ref заимствует v
let s = sum(v_ref);
println!("sum of {:?}: {}", v_ref, s); // ошибки нет
}
Приведенный выше код дает ожидаемый результат:
sum of [1, 2, 3]: 6
Если присвоение всегда создает отношения собственности, то может показаться удивительным, что приведенный выше код работает. В конце концов, v_ref, ссылочное значение, не передается по ссылке, но к нему все еще можно получить доступ в println!. Ответ заключается в том, что есть одно заметное исключение: ссылки сами реализуют Copy. Хотя это может показаться странным, ссылки в примерах до сих пор передаются по значению.
Передача по ссылке или значению
В предыдущих разделах показано, как Rust позволяет нам передавать значение функции по ссылке или по значению. Подведём итоги:
-
Если значение реализует Copy и не заимствовано, оно будет передано по значению.
-
Если значение реализует Copy и заимствовано, оно будет передано по ссылке.
-
Если значение не реализует Copy, оно должно быть заимствовано и поэтому будет передано по ссылке.
-
Ссылки реализуют Copy и поэтому передаются по значению. Есть одно исключение, которое будет описано позже.
Обобщая эти правила в форме примера:
fn pass_number_by_reference(number: &i8) -> bool {
number.is_negative()
}
fn pass_number_by_value(number: i8) -> bool {
number.is_negative()
}
fn pass_vec_by_reference(vec: &Vec) -> bool {
vec.is_empty()
}
fn main() {
// числа реализуют Copy, поэтому могут передаваться по значению или ссылке
let number = 42;
// число не перемещается, потому что оно заимствовано
let is_negative_by_ref = pass_number_by_reference(&number);
// перемещение числа, которое никогда не будет снова использовано
let is_negative_by_value = pass_number_by_value(number);
// копия не реализована - необходимо передать по ссылке
let vec = vec![];
// vec не перемещается
let is_empty = pass_vec_by_reference(&vec);
println!("is_negative_by_value: {}", is_negative_by_value);
println!("is_negative_by_ref: {}", is_negative_by_ref);
println!("vec {:?} is_empty: {}", vec, is_empty);
}
Заимствование и строковые литералы
Работа со строками — важная возможность для любого языка. В Rust строковые литералы представляют собой заимствованные ссылки. Например, рассмотрите:
fn byte_length(string: &str) -> usize {
string.bytes().len()
}
fn main() {
let string = "🦀";
let length = byte_length(string);
println!("Bytes in \"{}\": {}", string, length);
}
Компилятор распознает значение, принадлежащее строке, как заимствованный ссылочный тип &str (ссылка на str). Использование строки после вызова byte_length в println! разрешено, потому что сама ссылка копируется в строковый параметр byte_length.
Возврат заимствованной величины
Иногда нам нужно, чтобы функция возвращала заимствованное значение. Например, мы можем захотеть вернуть одну из строк, большую по длине в байтах. Мы можем попробовать следующее.
// Ошибки!
fn longest(x: &str, y: &str) -> &str {
if x.bytes().len() > y.bytes().len() {
x
} else {
y
}
}
fn main() {
let alice = "Alice";
let bob = "Bob";
println!("{}", longest(alice, bob));
}
Однако мы столкнемся с загадочной ошибкой, относящейся к так называемому «времени жизни».
1 | fn longest(x: &str, y: &str) -> &str {
| ^ ожидается параметр времени жизни
|
= подсказка: тип возвращаемого значения этой функции содержит заимствованное значение,
но в сигнатуре не указано, заимствовано оно из `x` или` y`
Время жизни
Время жизни — это область видимости заимствованной ссылки. Компилятор Rust достаточно умен, чтобы во многих случаях определять время жизни, а это означает, что нам не нужно явно его описывать. Однако это имеет две стороны. Когда компилятору требуется время жизни, концепция выглядит инородной.
Давайте перепишем предыдущий пример с явным, но ненужным указанием времени жизни. Это делается добавлением параметра времени жизни. Параметр времени жизни можно добавить везде, где появляется заимствованная ссылка. Как и параметры типа (также известные как «универсальные»), параметр времени жизни необходимо ввести в область видимости, прежде чем его можно будет использовать. Мы делаем это, помещая параметр в угловые скобки («<» и «>») после имени функции. Сюда же входят объявления параметров типа.
fn byte_length<'a>(string: &'a str) -> usize { // не необходимое время жизни
string.bytes().len()
}
fn main() {
let string = "🦀";
let length = byte_length(string);
println!("Bytes in \"{}\": {}", string, length);
}
Этот пример компилируется и запускается так же, как и раньше. Есть только два отличия: (1) появилось объявление параметра времени жизни <'a> после byte_length; и (2) появился параметр времени жизни 'a, следующий сразу за амперсандом в определении типа строки параметров. Имя параметра времени жизни (например, 'a) начинается с символа апострофа (') и заканчивается одним или несколькими символами — обычно только одним. Содержимое в угловых скобках переносит параметр времени жизни 'a в область видимости, чтобы его можно было использовать для изменения &a.
В предыдущем разделе был представлен этот неудачный пример:
fn longest(x: &str, y: &str)-> &str { // ошибка: ожидается параметр времени жизни
if x.bytes().len() > y.bytes().len() {
x
} else {
y
}
}
fn main() {
let alice = "Alice";
let bob = "Bob";
println!("{}", longest(alice, bob));
}
Это не будет компилироваться, потому что мы еще не ограничили время жизни возвращаемого значения. Без ограничения этого значения невозможно исключить случаи, когда самая длинная функция возвращает ссылку на потерянное значение. Добавление ограничения в виде параметра времени жизни исключает эти случаи. Идея аналогична параметру типа, который исключает случаи, когда переменная содержит значение несовместимого типа.
Имея в виду эту идею, обновляем пример:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.bytes().len() > y.bytes().len() {
x
} else {
y
}
}
fn main() {
let alice = "Alice";
let bob = "Bob";
println!("{}", longest(alice, bob));
}
Это изменение позволяет компилятору определить, что время жизни (допустимая область видимости) значения, заимствованную ссылку которого он возвращает, соответствует времени жизни параметров x и y. Другими словами, самая длинная функция не может вернуть ссылку на потерянное значение.
Заманчиво думать о параметрах времени жизни как о способе «продлить жизнь» ссылке. Лучшим подходом является рассмотрение сходства между параметрами типа и параметрами времени жизни. Параметр типа ограничивает диапазон возможных типов для значения, принадлежащего переменной. Точно так же параметр времени жизни ограничивает диапазон возможных времен жизни (допустимые области) для значения, принадлежащего переменной.
Здесь — начало, здесь — окончание.
Опубликовано: 2021.01.07, последняя правка: 2021.01.06 01:24
Добавить свой отзыв
Написать автору можно на электронную почту mail(аt)compiler.su
|