Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Концепция владения в 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 позволяет нам передавать значение функции по ссылке или по значению. Подведём итоги:

  1. Если значение реализует Copy и не заимствовано, оно будет передано по значению.
  2. Если значение реализует Copy и заимствовано, оно будет передано по ссылке.
  3. Если значение не реализует Copy, оно должно быть заимствовано и поэтому будет передано по ссылке.
  4. Ссылки реализуют 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

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

Каким должен быть язык программирования?

Анализ и критика

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

●  Концепция владения в Rust на примерах

●●  Концепция владения в Rust на примерах, часть 2

●●  Концепция владения в Rust на примерах, часть 3

●  О неулучшаемой архитектуре процессоров

●  Двадцать тысяч строк кода, которые потрясут мир?

●  Почему владение/заимствование в Rust такое сложное?

●  Масштабируемые архитектуры программ

●  О создании языков

●  Почему Хаскелл так мало используется в отрасли?

●  Программирование исчезнет. Будет дрессировка нейронных сетей

●  Бесплатный софт в мышеловке

●  Исповедь правового нигилиста

●  Русской операционной системой должна стать ReactOS

●  Почему обречён язык Форт

●  Программирование без программистов — это медицина без врачей

●  Электроника без электронщиков

●  Программисты-профессионалы и программирующие инженеры

●  Статьи Дмитрия Караваева

●●  Идеальный транслятор

●●  В защиту PL/1

●●  К вопросу о совершенствовании языка программирования

●●  О реализации метода оптимизации при компиляции

●●  О реализации метода распределения регистров при компиляции

●●  О распределении памяти при выполнении теста Кнута

●●  Опыты со стеком или «чемпионат по выполнению теста Кнута»

●●  О размещении переменных в стеке

●●  Сколько проходов должно быть у транслятора?

●●  Чтение лексем

●●  Экстракоды при синтезе программ

●●  Об исключенных командах или за что «списали» инструкцию INTO?

●●  Типы в инженерных задачах

●●  Непрерывное компилирование

●●  Об одной реализации специализированных операторов ввода-вывода

●●  Особенности реализации структурной обработки исключений в Win64

●●  О русском языке в программировании

●●  Формула расчета точности для умножения

●●  Права доступа к переменным

●●  Заметки о выходе из функции без значения и зеркальности get и put

●●  Модификация исполняемого кода как способ реализации массивов с изменяемыми границами

●●  Ошибка при отсутствии выполняемых действий

●●  О PL/1 и почему в нём не зарезервированы ключевые слова

●●  Не поминайте всуе PL/1

●●  Скорость в попугаях

●●  Крах операции «Инкогнито»

●●  Предопределённый результат

●●  Поддержка профилирования кода программы на низком уровне

●  Следующие 7000 языков программирования

●●  Что нового с 1966 года?

●●  Наблюдаемая эволюция языка программирования

●●  Ряд важных языков в 2017 году

●●  Слоны в комнате

●●  Следующие 7000 языков программирования: заключение

Компьютерный юмор

Новости и прочее




Последние отзывы

2021/01/20 12:11 ••• Александр Коновалов aka Маздайщик
Концепция владения в Rust на примерах, часть 3

2021/01/20 00:10 ••• Автор сайта
Признаки устаревшего языка

2021/01/19 23:24 ••• Автор сайта
Выбор кодировки для компилятора

2021/01/17 23:15 ••• Бурановский дедушка
Изменение длины объекта в стеке во время исполнения

2021/01/16 22:47 ••• Автор сайта
Предложения и замечания

2021/01/11 17:51 ••• Автор сайта
Утилита транслитерации русского C/C++ в стандартный

2021/01/09 18:08 ••• kt
Типы в инженерных задачах

2021/01/06 15:34 ••• Автор сайта
О неулучшаемой архитектуре процессоров

2020/12/18 16:04 ••• Автор сайта
Не поминайте всуе PL/1

2020/10/30 15:33 ••• Бурановский дедушка
Указатели и ссылки в C++

2020/10/29 15:02 ••• kt
Экстракоды при синтезе программ

2020/10/08 23:00 ••• Автор сайта
Короткие комментарии

2020/10/07 17:34 ••• NuShaman
Энтузиасты-разработчики компиляторов и их проекты

2020/10/05 23:19 ••• Неслучайный читатель
Обработка ошибок