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



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



Схема


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


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


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

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


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

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


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


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


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

Поделиться с друзьями
-->

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


  1. Halt
    19.02.2017 10:10
    +2

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

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

    Имхо, этот момент стоило бы пояснить подробнее.


  1. Halt
    19.02.2017 10:16
    +7

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

    Я бы написал «не может пережить».


  1. vagran
    19.02.2017 10:31
    +6

    Заглавная картинка некорректна. Текстом дано определение круга, а нарисована окружность, почему-то подписанная кругом.


    1. splav_asv
      19.02.2017 11:11

      Ну, почему сразу некорректна. Внутри белый круг, граница круга — окружность. Просто несколько двусмысленна.


    1. lukretsiy
      19.02.2017 11:11
      +3

      Соглашусь и поправлюсь.


    1. GennPen
      19.02.2017 13:32

      Еще и в квадрате такого же цвета, что сбивает с толку.


      1. Bronx
        21.02.2017 09:56
        +1

        Квадрат подходит под определение круга, если использовать манхеттенское расстояние.


  1. Gexon
    19.02.2017 16:29
    +1

    Отличная статья, отличный перевод!
    Люблю картинки)


  1. Saffron
    19.02.2017 16:51
    -2

    Я хотел освоить rust, открыл его hello world и он меня очень смутил. Чуть более сложный пример, где требуется ввести имя и программа его печатает. Там задаётся мутабельная строка, потом передаётся процедуре чтения. Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку. Каких-то разумных предположений я не смог придумать и отложил раст в долгий ящик.


    1. pftbest
      19.02.2017 18:55
      +6

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


      let mut abc = String::new();

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


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

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


      let mut abc = String::with_capacity(10);


    1. Sirikid
      19.02.2017 19:28
      +3

      Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку.

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


      1. Saffron
        19.02.2017 22:34

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

        А что в rust не бывает null?


        1. asdf87
          19.02.2017 23:25
          +7

          За пределами unsafe не бывает.
          Вместо него есть Option с гораздо более явной семмантикой, чем null.


          1. Saffron
            20.02.2017 10:36

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


            1. ozkriff
              20.02.2017 11:01
              +1

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


        1. Sirikid
          20.02.2017 00:34
          +6

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


          1. Saffron
            20.02.2017 09:18
            -2

            Так сделай ссылку на нулевой указатель


            1. splav_asv
              20.02.2017 13:51

              Ссылка может быть на объект. Аналог в Rust есть — Option<Box>. По факту это именно указатель. Если он 0 — None, нет — Some<Box>.


              1. Saffron
                20.02.2017 20:24
                -3

                Всё есть объект. Указатель — тоже


            1. ozkriff
              20.02.2017 14:01
              +1

              сделай ссылку на нулевой указатель

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

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


        1. khim
          20.02.2017 01:39
          +3

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

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

          Сможет ли эту проблему решить rust — неизвестно, пока на нём программ в миллионы строк никто не писал. Но попытка достойная.


          1. Beholder
            20.02.2017 11:03
            +1

            Ну точно так же можно «не глядя» вызывать get() у Optional, и так же будет пролетать NoSuchElementException через всю программу.


            1. khim
              20.02.2017 13:12
              +2

              Вопрос будет в количестве этих Optional. Разница в том, что в Java — любой объект может отсутствовать, а потому понять нужно проверять на null или нет нельзя. В rust же предполагается что большинство обьектов таки не будут описываться как Optional и, соответственно, небольшое количество Optional — будет проверяться.

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


            1. DarkEld3r
              20.02.2017 14:01
              +5

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


          1. Saffron
            20.02.2017 21:25

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


            1. khim
              20.02.2017 21:36
              +6

              А как вы планируете избавляться от null в джаве?
              Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком». Безопасности нет, переносимости нет, для того, чтобы получить приличную скорость нужно, фактически, реализовывать «своё» управление памятью, размещая обьекты в массивах и самостоятельно отмечая — где живые, где мёртвые.

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

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

              Обидно то, что мы теперь с этим убожеством связаны на многие годы, так как эта каракатица поселилась в самом сердце Android'а…


              1. Sirikid
                20.02.2017 22:49
                +1

                Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком».

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


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


                1. Saffron
                  21.02.2017 00:48

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

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


                  1. Sirikid
                    21.02.2017 01:22

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


                    throw NullPointerException()
                    
                    foo: Bar?
                    foo!!
                    
                    foo: Bar!
                    foo!!
                    // или
                    baz: Bar = foo


                    1. Saffron
                      22.02.2017 10:31

                      > Есть

                      А дайте ссылку на статью


                  1. khim
                    21.02.2017 02:08
                    +1

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

                    Никто не спорит с тем, что поверх Java-машины можно водрузить всё, что душе угодно (в качестве безумного варианта: водрузите JPC поверх вашей JVM — и получите любой язык, с любыми свойствами), но вот сохранить при этом совместимость с Java — уже не получится. Или — или, на выбор.


        1. BlessMaster
          20.02.2017 03:22
          +7

          Дело даже не в наличии/отсутствии null.
          Ссылки — это инструмент контролирующий совместный доступ к памяти, поэтому null для ссылки — в принципе лишён смысла.
          Если нет переменной, то не нужна и ссылка.
          При этом неинициализированные переменные (заранее объявленные) вполне допустимы, компилятор контролирует порядок исполнения и не даст скомпилировать код, читающий переменную до инициализации.

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


    1. ozkriff
      20.02.2017 10:53
      +1

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


  1. acmnu
    20.02.2017 15:06

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


    1. ozkriff
      20.02.2017 15:13
      +1

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


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


    1. khim
      20.02.2017 17:28
      +3

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