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

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

Перевод статьи «Rust Ownership by Example».

Введение

Rust – язык безопасного системного программирования. И хотя C и C++ тоже являются системными языками, но они небезопасны. В частности, Rust – это «типобезопасный язык», что означает, что компилятор гарантирует, что каждая программа имеет четко определенное поведение. Хотя есть языки, которые дают такую же гарантию, Rust делает это без сборщика мусора, среды выполнения или ручного управления памятью.

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

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

Запуск примеров кода и эксперименты очень полезны для изучения Rust. Самый простой способ сделать это – скопировать и вставить приведенные здесь примеры в веб-приложение Rust Playground (примечание переводчикаили онлайн-компилятор ideone.com). Если вы предпочитаете компилировать и запускать код на своем компьютере, самый простой способ сделать это – rustup.

Право собственности начинается с переуступки

Как и в других языках, Rust использует символ равенства «=» для присвоения значения переменной. Эта переменная описывает её дальнейшее использование.

Следующая программа присваивает переменной x значение 42, затем печатает значение x.

fn main() {
    let x = 42;

    println!("x: {}", x);
}
Но Rust делает еще один шаг вперед. Правопреемник (x) становится единственным владельцем этой величины. Никаких исключений. Тесная связь между присвоением и владением создает основу для многих уникальных возможностей Rust.

С концом области видимости владение заканчивается

Когда переменная выходит за пределы области видимости, связанное с ней значение, если оно есть, удаляется. Нельзя снова использовать удалённое значение, потому что связанный с ней ресурс немедленно освобождается.

Это правило позволяет легко получать ответ о времени жизни величины. Пока переменная остается в области видимости, принадлежащее ей значение всегда существует. Когда владение выйдет за рамки видимости, величина удаляется. Значение может быть удалено и задолго до конца области видимости, если компилятор видит, что переменная-владелец больше не используется.

Мы можем увидеть это правило воочию, назначив переменную в анонимной области видимости, созданной левой и правой фигурными скобками («{» и «}»):

fn main() {
    {
        let x = 42;
        println!("x: {}", x);
    }
    println!("x: {}", x); // ОШИБКА: x не в области видимости
}
Компилятор отвергнет этот код, сообщив, что переменная, используемая во втором println! (x) не входит в область видимости.
8 |     println!("x: {}", x);
  |                       ^ не найден в этой области видимости
Большинство языков также не позволяют использовать x за пределами его локальной области. Но в Rust это ограничение идет дальше. Когда анонимная область действия заканчивается, значение, принадлежащее x (это величина 42), удаляется. Чуть более сложный пример доказывает порядок событий. Некоторые пояснения могут быть полезны. Мы определяем специальный тип DropMe, который реализует типаж Drop и связанный с ним метод drop. drop вызывается перед удалением экземпляра, распечатывая прощальное сообщение перед цифровым забвением. Остальной синтаксис будет объяснен позже.
#[derive(Debug)]
struct DropMe;

impl Drop for DropMe {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    println!("Begin outer scope."); {
        println!("Begin inner scope.");
        let x = DropMe;
        println!("x: {:?}", x);
    }
    println!("End outer scope.");
}
Из выведенного программой на экран видно, что значение, принадлежащее x, удаляется, когда переменная выходит за пределы области видимости:
Begin outer scope.
Begin inner scope.
x: DropMe
Dropping!
End outer scope.

Переназначение перемещает владение

Если присвоение создаёт отношения собственности, то как насчет переназначения? Представьте, что мы хотим переназначить значение, принадлежащее a, новой переменной b:

fn main() {
    let a = vec![1, 2, 3]; // a — растущий литерал массива
    let b = a;        // перемещение: `a` не может больше использоваться
    println!("a: {:?}", b);
}

Векторы (также известные как Vec) – это растущие массивы Rust. Векторные литералы создаются с помощью функции-макроса vec!.

Этот код компилируется и запускается, выводя результат

a: [1, 2, 3].

Следуя правилу, согласно которому присвоение создает отношения собственности, мы ожидаем, что b станет новым владельцем. Учитывая, что у значения может быть только один владелец, мы также ожидаем, что a будет неинициализированным и, следовательно, непригодным для использования. Оба ожидания верны.

Передача права собственности (как в let b = a) известна как перемещение. Движение приводит к тому, что бывший правопреемник становится неинициализированным и, следовательно, не может использоваться в дальнейшем.

Мы можем подтвердить это, откомпилировав:

fn main() {
    let a = vec![1, 2, 3];
    let b = a;

    println!("a:{:?}\nb:{:?}", a, b); // ошибка: заимствование перемещённой 
				      // величины:`a`
}

Компилятор обнаруживает нашу попытку повторно использовать теперь уже неинициализированный a и жалуется:

2 |     let a = vec![1,2,3];
  |         - имеет место перемещение, потому что `a` имеет тип 
              `std::vec::Vec`, который не реализует типаж `Copy`
3 |     let b = a;
  |             - здесь величена перемещена
4 | 
5 |     println!("a:{:?}\nb:{:?}", a, b);
  |                                ^ здесь величина заимствована после перемещения

Это сообщение об ошибке немного сбивает с толку, предвидя то, что мы хотим сделать, и предлагая способ дальнейших действий. Позже мы перейдем к «заимствованию» и типажу Copy. А пока обратите внимание на ошибку в строке 5, которая была вызвана тем, что мы пытались получить доступ к a после перемещения.

Иногда бывает трудно заметить перемещение. Рассмотрим, что происходит, когда мы передаем аргумент функции:

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 s = sum(v); // внимание, v был перемещён!
    println!("sum: {}", s);
}

Этот код компилируется и печатает результат sum: 6, как и ожидалось. Однако легко забыть о неявном перемещении, которое происходит при вызове sum. В частности, значение, принадлежащее v, перемещается в векторный параметр функции суммы.

Если бы мы использовали v после этого перемещения, то компилятор пожаловался бы:

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 s = sum(v);

    println!("sum of {:?}: {}", v, s); // ОШИБКА: v был перемещён!
}
Фактически, мы получаем по существу ту же ошибку (и очень полезное предложение), что и при более очевидном переназначении.

Еще одна форма переназначения происходит при возврате значения из функции:

fn create_series(x: i32) -> Vec {
    let result = vec![x, x+1, x+2];

    result
}

fn main() {
    let series = create_series(42);

    println!("series: {:?}", series);
}

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

Копирование

Что, если бы мы захотели повторно использовать переменную после переназначения? В предыдущем разделе было показано, что происходит, когда мы пробуем вот это:

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 s = sum(v);

    println!("sum of {:?}: {}", v, s); // ОШИБКА: v был перемещён!
}
Однако пример ниже компилируется и работает нормально. Почему в нём мы можем использовать как a, так и b, даже если они были переданы в качестве аргументов для суммирования?

fn sum(left: i32, right: i32) -> i32 {
    left + right
}

fn main() {
    let a = 42;
    let b = 1;
    let s = sum(a, b);

    println!("this sum of {} and {} is {}", a, b, s); // ошибки нет!
}

Вместо того чтобы перемещать значения, принадлежащие a и b, в параметры sum, значения копируются. Копия создает точную дубль значения, реализующего типаж Copy. В Rust числовые величины и некоторые другие «дешёвые» встроенные типы поддерживают копирование. Векторов нет.

Пример с Vec не может быть скомпилирован, потому что Vec не реализует типаж Copy. Пример с i32 успешен, потому что этот тип поддерживает копирование.

Это различие становится явным, когда переходим к пользовательским типам. Такие типы создаются из структуры. Например, мы можем определить тип Person:

#[derive(Debug)]
struct Person {
    age: i8
}

fn main() {
    let alice = Person { age: 42 };

    println!("alice: {:?}", alice);
}
Вверх определения Person содержит процедурный макрос. Его цель — автоматически сгенерировать код. В случае # [derive (Debug)] сгенерированный код позволит использовать Person в выводе println!.

Структуры не реализуют Copy по умолчанию. Переназначение величин типа Person приводит к перемещению, а не к копированию, как показано ниже:

#[derive(Debug)]
struct Person {
    age: i8
}

fn main() {
    let alice = Person { age: 42 };
    let bob = alice;

    println!("alice: {:?}\nbob: {:?}", alice, bob); // ОШИБКА: переменная
						    // alice перемещена
}
Здесь компилятор снова выдает знакомую ошибку: «здесь заимствуется значение после перемещения».

Однако мы можем преобразовать Person в тип, реализующий Copy. Для этого мы можем автоматически получить типаж Copy так же, как было получен типаж Debug. По причинам, выходящим за рамки данного руководства, также необходимо создать производный объект

Clone (Clone must also be derived).
#[derive(Debug,Clone,Copy)]
struct Person {
    age: i8
}

fn main() {
    let alice = Person { age: 42 };
    let bob = alice;

    println!("alice: {:?}\nbob: {:?}", alice, bob);
}
Компиляция и запуск этого кода дает ожидаемый результат:
alice: Person { age: 42 }
bob: Person { age: 42 }

Здесь копирование работает для величин, подобных Person, которые можно эффективно скопировать, но как насчет «дорогих» значений?



Продолжение — в части 2 и части 3.

Опубликовано: 2021.01.07, последняя правка: 2021.01.07    09:26

ОценитеОценки посетителей
   ██████████████████████████████████████████ 6 (100%)
   ▌ 0
   ▌ 0
   ▌ 0

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

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

Карта сайта


Содержание

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

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

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

Компилятор

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

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

●  О превращении кибернетики в шаманство

●  Про лебедей, раков и щук

●  О русском ассемблере

●  Арифметика синтаксиса-3

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

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

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

●  Суть побочных эффектов в чисто функциональных языках

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

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

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

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

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

●●  Джоэл Спольски о функциональном программировании

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

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

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

●  Десятка худших фич C#

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

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

●  ЕС ЭВМ — это измена, трусость и обман?

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

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

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

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

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

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

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

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

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

●●  Опыт самостоятельного развития средства программирования в РКК «Энергия»

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

●●  К вопросу о парадигмах

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

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

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

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

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

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

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

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




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

2024/03/19 02:19 ••• Ivan
Энтузиасты-разработчики компиляторов и их проекты

2024/03/18 23:25 ••• Автор сайта
Надёжные программы из ненадёжных компонентов

2024/03/18 22:44 ••• Автор сайта
О многократном резервировании функций

2024/03/17 17:18 ••• Городняя Лидия Васильевна
Раскрутка компилятора

2024/03/10 18:33 ••• Бурановский дедушка
Русской операционной системой должна стать ReactOS

2024/03/07 14:16 ••• Неслучайный читатель
«Двухмерный» синтаксис Python

2024/03/03 16:49 ••• Автор сайта
О неправомерном доступе к памяти через указатели

2024/02/28 18:59 ••• Вежливый Лис
Про лебедей, раков и щук

2024/02/24 18:10 ••• Бурановский дедушка
ЕС ЭВМ — это измена, трусость и обман?

2024/02/22 15:57 ••• Автор сайта
Русский язык и программирование

2024/02/19 17:58 ••• Сорок Сороков
О русском языке в программировании

2024/02/16 16:33 ••• Клихальт
Избранные компьютерные анекдоты

2024/02/10 22:40 ••• Автор сайта
Все языки эквивалентны. Но некоторые из них эквивалентнее других