Графическое описание владения и заимствования в Rust

https://rufflewind.com/2017-02-15/rust-move-copy-borrow
  • Перевод

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



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



Схема


Картинка кликабельна, вы можете её увеличить. Также вы можете получить схемы без перевода в виде PNG, SVG или PDF.


Верхние две схемы изображают два основных вида семантики данных, которые нам доступны: либо перемещение, либо копирование.


  • Схема семантики перемещения (⤳) выглядит очень простой. Здесь нет никакого обмана: семантика перемещения выглядит странной только потому, что большинство языков позволяют использовать переменные столько раз, сколько пожелает программист. В реальном мире обычно всё не так: я не могу просто дать кому-нибудь свою ручку и при этом всё еще использовать её для записи! В Rust, любая переменная, тип которой не реализует типаж Copy, имеет семантику перемещения, поведение которой показано на рисунке.
  • Семантика копирования (⎘) зарезервирована для типов, которые реализуют типаж Copy. В этом случае каждое использование объекта будет приводить к копированию, как показано на схеме — раздвоением.

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


  • Для изменяемого заимствования я использовал символ замка для того, чтобы показать, что исходный объект заблокирован на всё время заимствования, что приводит к невозможности его использования.
  • Для противоположного, неизменяемого заимствования, я использовал символ снежинки, чтобы показать, что исходный объект всего лишь заморожен: вы до сих пор можете брать неизменяемые ссылки, но не можете перемещать или брать изменяемые ссылки на него.

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


Последние две схемы подводят итог, показывая основные отличия и общие черты между двумя видами ссылок, как в виде изображения, так и в виде текста. Спецификатор "внешне" важен, так как у вас может быть внутренняя изменяемость через вещи похожие на Cell.


Примечание переводчика


Хотелось бы выразить отдельную благодарность Андрею Лесникову (@ozkiff), Serhii Plyhun (@snuk182) и Сергею Веселкову (@vessd) за помощь в переводе и последующее ревью.

Поделиться публикацией

Похожие публикации

Комментарии 36

    +2
    Графическое представление регионов очень удачное, только мне кажется, что левая нижняя картинка может ввести в заблуждение.

    Я понимаю, что речь идет исключительно о вре́менном даунгрейде &mut до &. Однако, не знакомый с Rust человек может подумать, что это обычное использование неконстантных объектов в константном контексте.

    Имхо, этот момент стоило бы пояснить подробнее.
      +7
      Ну и перевод «must not outlive» как «не должна пережить», как мне кажется, не делает достаточного акцента. Может показаться, что программист должен сам это контролировать, тогда как на деле контролирует и запрещает такое компилятор.

      Я бы написал «не может пережить».
        +6
        Заглавная картинка некорректна. Текстом дано определение круга, а нарисована окружность, почему-то подписанная кругом.
          0
          Ну, почему сразу некорректна. Внутри белый круг, граница круга — окружность. Просто несколько двусмысленна.
            +3
            Соглашусь и поправлюсь.
              0
              Еще и в квадрате такого же цвета, что сбивает с толку.
                +1
                Квадрат подходит под определение круга, если использовать манхеттенское расстояние.
              +1
              Отличная статья, отличный перевод!
              Люблю картинки)
                –2
                Я хотел освоить rust, открыл его hello world и он меня очень смутил. Чуть более сложный пример, где требуется ввести имя и программа его печатает. Там задаётся мутабельная строка, потом передаётся процедуре чтения. Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку. Каких-то разумных предположений я не смог придумать и отложил раст в долгий ящик.
                  +6

                  Не видел этот пример, так что не скажу почему автор написал именно так, но никто не запрещает создать строку напрямую:


                  let mut abc = String::new();

                  Это будет действительно немножко эффективнее чем инициализация пустой строкой:


                  let mut abc = String::from("");

                  Но может автор хотел заодно показать этим примером, как можно создать не только пустые строки, но и с заданным текстом.
                  Если очень сильно заботится о производительности, то можно предположить сколько текста будет на входе и выделить память заранее:


                  let mut abc = String::with_capacity(10);
                    +3
                    Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку.

                    Ссылка не может быть сама по себе, она должна на что-то ссылаться, а про то как создать строку уже рассказал pftbest.

                      0
                      > Ссылка не может быть сама по себе, она должна на что-то ссылаться

                      А что в rust не бывает null?
                        +7
                        За пределами unsafe не бывает.
                        Вместо него есть Option с гораздо более явной семмантикой, чем null.
                          0
                          Option — это вроде из функционального мира. Там вообще не принято передавать мутабельные аргументы по ссылкам, чтобы в них записывали данные, там обычно функция возвращает все нужные данные кортежом.
                            +1

                            А чем Option не вписывается в приземленную императивщину? Лучше передает намерение писавшего код, все такое.

                          +6

                          У ссылок нет конечно, это же не указатель.

                            –2
                            Так сделай ссылку на нулевой указатель
                              0
                              Ссылка может быть на объект. Аналог в Rust есть — Option<Box>. По факту это именно указатель. Если он 0 — None, нет — Some<Box>.
                                –3
                                Всё есть объект. Указатель — тоже
                                +1
                                сделай ссылку на нулевой указатель

                                let a: *const i32 = std::ptr::null();
                                let b: &*const i32 = &a;

                                но это вообще несколько другое.

                              +3
                              Бывает, но только в строго оговоренных местах. Не в hello, world.

                              Собственно null — это одновременно великая и ужасная вещь. Именно когда разработчики Java «сдались» и сделали так, что обьявлять возможность кидания NullPointerException стало не нужно вся хвалёная «безопасность» «свернулась в трубочку и стекла на пол». Я не видел ни одной программы на Java сколько-нибудь приличного размера, которая бы не падала из-за того, что кто-то где-то таки кинул NPE и он бы не улетел далеко за пределы того модуля где был порождён (он может быть пойман и завёрнут в пять обёрток, но сути это не меняет).

                              Сможет ли эту проблему решить rust — неизвестно, пока на нём программ в миллионы строк никто не писал. Но попытка достойная.
                                +1
                                Ну точно так же можно «не глядя» вызывать get() у Optional, и так же будет пролетать NoSuchElementException через всю программу.
                                  +2
                                  Вопрос будет в количестве этих Optional. Разница в том, что в Java — любой объект может отсутствовать, а потому понять нужно проверять на null или нет нельзя. В rust же предполагается что большинство обьектов таки не будут описываться как Optional и, соответственно, небольшое количество Optional — будет проверяться.

                                  Как оно будет на практике — посмотрим.
                                    +5

                                    К аргументу khim добавлю, что этот get (на самом деле, unwrap) придётся писать, а это дополнительные телодвижения и это хорошо. Сразу приходится задумываться: а может тут не unwrap нужен, а например unwrap_or.

                                    0
                                    А как вы планируете избавляться от null в джаве? Что будет записано в объекте при его выделении? Не будете же инициализацию пихать в garbage collector? Нет, сборщик мусора, он же управитель памятью, сделает вам пустой объект, где все ссылки забиты null-ами и дальше уже на этапе инициализации они будут переписываться на верные значения. Или останутся null, если произойдёт какая-нибудь исключительная ситуация
                                      +6
                                      А как вы планируете избавляться от null в джаве?
                                      Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком». Безопасности нет, переносимости нет, для того, чтобы получить приличную скорость нужно, фактически, реализовывать «своё» управление памятью, размещая обьекты в массивах и самостоятельно отмечая — где живые, где мёртвые.

                                      При этом ещё и варианта сделать как в большинстве скриптовых языков («выйти» в C и там всё реализовать эффективно) — в общем тоже нет, так как JNI — штука весьма и весьма своенравная и не очень эффективная.

                                      Нет, сборщик мусора, он же управитель памятью, сделает вам пустой объект, где все ссылки забиты null-ами и дальше уже на этапе инициализации они будут переписываться на верные значения.
                                      И это — очередное 100500е место, где сборщик мусора мешает сделать нормальный язык. Возникает вопрос: а оно точно надо? Мы точно получаем выигрыш из-за того, что не используем разного рода scoped_ptr'ы или shared_ptr'ы, а используем «модный» «полноценный» сборщик мусора?

                                      Обидно то, что мы теперь с этим убожеством связаны на многие годы, так как эта каракатица поселилась в самом сердце Android'а…
                                        +1
                                        Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком».

                                        Где пруфы?
                                        У Java есть обратаная совместимость и отличная виртуальная машина, сам язык довольно простой опять же за счет сборки мусора.


                                        Про сборщики мусора вы и Saffron несете какую-то ерунду, посмотрите на Kotlin, он работает на той же виртуальной машине и предоставляет очень неплохой интероп с Java и он null-safe, все места из которых может вылететь NPE определяются статически.

                                          0
                                          > он null-safe, все места из которых может вылететь NPE определяются статически.

                                          А пруфы есть? Система типов java, насколько я помню, не слишком-то и sound, и вряд ли надстройка способна это изменить
                                            0

                                            Есть, NPE можно либо явно бросить, либо получить развернув nullable-тип или platform-тип.


                                            throw NullPointerException()
                                            
                                            foo: Bar?
                                            foo!!
                                            
                                            foo: Bar!
                                            foo!!
                                            // или
                                            baz: Bar = foo
                                              0
                                              > Есть

                                              А дайте ссылку на статью
                                              +1
                                              Вот прямо тут есть чудесная статья, которая показывает, что заявление «предоставляет очень неплохой интероп с Java и он null-safe» всё-таки несколько преувеличено. Kotlin — предоставляет неплохой интероп с Java ИЛИ (а не и!) он null-safe.

                                              Никто не спорит с тем, что поверх Java-машины можно водрузить всё, что душе угодно (в качестве безумного варианта: водрузите JPC поверх вашей JVM — и получите любой язык, с любыми свойствами), но вот сохранить при этом совместимость с Java — уже не получится. Или — или, на выбор.
                                      +7
                                      Дело даже не в наличии/отсутствии null.
                                      Ссылки — это инструмент контролирующий совместный доступ к памяти, поэтому null для ссылки — в принципе лишён смысла.
                                      Если нет переменной, то не нужна и ссылка.
                                      При этом неинициализированные переменные (заранее объявленные) вполне допустимы, компилятор контролирует порядок исполнения и не даст скомпилировать код, читающий переменную до инициализации.

                                      Другой момент — каждый раз создавать строку в процессе преобразований — не самый эффективный паттерн, хотя, конечно, так можно (например, функция будет каждый раз создавать String и возвращать её).
                                      Но для ввода-вывода API строится так, что пользователь сам создаёт буфер и передаёт его для заполнения в функцию. Кроме прочего, это позволяет реализовывать эффективную конкатенацию и в случае необходимости переиспользовать буфер, без повторного выделения памяти в куче (достаточно «дорогая» операция).
                                      При этом буфер может быть произвольного типа, лишь бы он реализовал соответствующий необходимый типаж Read или Write.
                                      При таком подходе весь контроль над тем, что на самом деле будет происходить, — в руках программиста. И это достаточно важно для языка, предназначенного быть эффективным и позиционируемого как низкоуровневый/системный.
                                    +1

                                    Может https://docs.rs/text_io или https://docs.rs/scan_fmt (или еще что похожее) взять?

                                    0

                                    Не спец по Rust, но то что копирование или перемещение зависит от типа при сходных действиях, мне очень не нравится ибо смотря на выражение let a = b я не уверен что произойдет c переменными a и b.

                                      +1

                                      На ранних стадиях развития языка с явной семантикой перемещения экспериментировали, но в итоге пришли к текущей схеме


                                      https://www.reddit.com/r/rust/comments/2x0wq0/did_rust_ever_experiment_with_making_moves

                                        +3
                                        смотря на выражение let a = b я не уверен что произойдет c переменными a и b
                                        Ну почему же? Ясно что окажется в a, а вот останется ли что-нибудь в b — уже не так ясно, но это и не очень страшно: если обьект простой и копируемый, то его ненужную копию «изведёт» оптимизатор, а если сложный — и некопируемый, то попытка его использовать — это ошибка, компилятор вам не даст ничего напутать. Проблема будет если обьект копируемый, но сложный и его копировать дорого — так просто лучше не делать.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое