Ниже представлено графическое описание перемещения, копирования и заимствования в языке программирования Rust. В основном, эти понятия специфичны только для Rust, являясь общим камнем преткновения для многих новичков.
Чтобы избежать путаницы, я попытался свести текст к минимуму. Данная заметка не является заменой различных учебных руководств, и лишь сделана для тех, кто считает, что визуально информация воспринимается легче. Если вы только начали изучать Rust и считаете данные графики полезными, то я бы порекомендовал вам отмечать свой код похожими схемами для лучшего закрепления понятий.
Картинка кликабельна, вы можете её увеличить. Также вы можете получить схемы без перевода в виде PNG, SVG или PDF.
Верхние две схемы изображают два основных вида семантики данных, которые нам доступны: либо перемещение, либо копирование.
- Схема семантики перемещения (?) выглядит очень простой. Здесь нет никакого обмана: семантика перемещения выглядит странной только потому, что большинство языков позволяют использовать переменные столько раз, сколько пожелает программист. В реальном мире обычно всё не так: я не могу просто дать кому-нибудь свою ручку и при этом всё еще использовать её для записи! В Rust, любая переменная, тип которой не реализует типаж
Copy
, имеет семантику перемещения, поведение которой показано на рисунке. - Семантика копирования (?) зарезервирована для типов, которые реализуют типаж
Copy
. В этом случае каждое использование объекта будет приводить к копированию, как показано на схеме — раздвоением.
Две центральные схемы описывают два метода заимствования объекта, которым вы владеете, и то, что каждый из этих методов предлагает.
- Для изменяемого заимствования я использовал символ замка для того, чтобы показать, что исходный объект заблокирован на всё время заимствования, что приводит к невозможности его использования.
- Для противоположного, неизменяемого заимствования, я использовал символ снежинки, чтобы показать, что исходный объект всего лишь заморожен: вы до сих пор можете брать неизменяемые ссылки, но не можете перемещать или брать изменяемые ссылки на него.
В обеих схемах '?
это имя, которое я выбрал для обозначения времени жизни ссылок. Я специально использовал греческую букву, так как, на текущий момент, нет никакого синтаксиса для описания конкретных времен жизни в Rust.
Последние две схемы подводят итог, показывая основные отличия и общие черты между двумя видами ссылок, как в виде изображения, так и в виде текста. Спецификатор "внешне" важен, так как у вас может быть внутренняя изменяемость через вещи похожие на Cell
.
Примечание переводчика
Хотелось бы выразить отдельную благодарность Андрею Лесникову (@ozkiff), Serhii Plyhun (@snuk182) и Сергею Веселкову (@vessd) за помощь в переводе и последующее ревью.
Комментарии (36)
Halt
19.02.2017 10:16+7Ну и перевод «must not outlive» как «не должна пережить», как мне кажется, не делает достаточного акцента. Может показаться, что программист должен сам это контролировать, тогда как на деле контролирует и запрещает такое компилятор.
Я бы написал «не может пережить».
Saffron
19.02.2017 16:51-2Я хотел освоить rust, открыл его hello world и он меня очень смутил. Чуть более сложный пример, где требуется ввести имя и программа его печатает. Там задаётся мутабельная строка, потом передаётся процедуре чтения. Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку. Каких-то разумных предположений я не смог придумать и отложил раст в долгий ящик.
pftbest
19.02.2017 18:55+6Не видел этот пример, так что не скажу почему автор написал именно так, но никто не запрещает создать строку напрямую:
let mut abc = String::new();
Это будет действительно немножко эффективнее чем инициализация пустой строкой:
let mut abc = String::from("");
Но может автор хотел заодно показать этим примером, как можно создать не только пустые строки, но и с заданным текстом.
Если очень сильно заботится о производительности, то можно предположить сколько текста будет на входе и выделить память заранее:
let mut abc = String::with_capacity(10);
Sirikid
19.02.2017 19:28+3Но во всём этом меня смущает, что мутабельная строка инициализирована пустой строчкой. Зачем? Если процедура сама заполняет строку, то зачем ей давать какой-то готовый объект, пусть модифицирует ссылку.
Ссылка не может быть сама по себе, она должна на что-то ссылаться, а про то как создать строку уже рассказал pftbest.
Saffron
19.02.2017 22:34> Ссылка не может быть сама по себе, она должна на что-то ссылаться
А что в rust не бывает null?asdf87
19.02.2017 23:25+7За пределами unsafe не бывает.
Вместо него есть Option с гораздо более явной семмантикой, чем null.Saffron
20.02.2017 10:36Option — это вроде из функционального мира. Там вообще не принято передавать мутабельные аргументы по ссылкам, чтобы в них записывали данные, там обычно функция возвращает все нужные данные кортежом.
ozkriff
20.02.2017 11:01+1А чем Option не вписывается в приземленную императивщину? Лучше передает намерение писавшего код, все такое.
khim
20.02.2017 01:39+3Бывает, но только в строго оговоренных местах. Не в hello, world.
Собственно null — это одновременно великая и ужасная вещь. Именно когда разработчики Java «сдались» и сделали так, что обьявлять возможность кидания NullPointerException стало не нужно вся хвалёная «безопасность» «свернулась в трубочку и стекла на пол». Я не видел ни одной программы на Java сколько-нибудь приличного размера, которая бы не падала из-за того, что кто-то где-то таки кинул NPE и он бы не улетел далеко за пределы того модуля где был порождён (он может быть пойман и завёрнут в пять обёрток, но сути это не меняет).
Сможет ли эту проблему решить rust — неизвестно, пока на нём программ в миллионы строк никто не писал. Но попытка достойная.Beholder
20.02.2017 11:03+1Ну точно так же можно «не глядя» вызывать get() у Optional, и так же будет пролетать NoSuchElementException через всю программу.
khim
20.02.2017 13:12+2Вопрос будет в количестве этих Optional. Разница в том, что в Java — любой объект может отсутствовать, а потому понять нужно проверять на null или нет нельзя. В rust же предполагается что большинство обьектов таки не будут описываться как Optional и, соответственно, небольшое количество Optional — будет проверяться.
Как оно будет на практике — посмотрим.
DarkEld3r
20.02.2017 14:01+5К аргументу khim добавлю, что этот
get
(на самом деле,unwrap
) придётся писать, а это дополнительные телодвижения и это хорошо. Сразу приходится задумываться: а может тут неunwrap
нужен, а напримерunwrap_or
.
Saffron
20.02.2017 21:25А как вы планируете избавляться от null в джаве? Что будет записано в объекте при его выделении? Не будете же инициализацию пихать в garbage collector? Нет, сборщик мусора, он же управитель памятью, сделает вам пустой объект, где все ссылки забиты null-ами и дальше уже на этапе инициализации они будут переписываться на верные значения. Или останутся null, если произойдёт какая-нибудь исключительная ситуация
khim
20.02.2017 21:36+6А как вы планируете избавляться от null в джаве?
Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком». Безопасности нет, переносимости нет, для того, чтобы получить приличную скорость нужно, фактически, реализовывать «своё» управление памятью, размещая обьекты в массивах и самостоятельно отмечая — где живые, где мёртвые.
При этом ещё и варианта сделать как в большинстве скриптовых языков («выйти» в C и там всё реализовать эффективно) — в общем тоже нет, так как JNI — штука весьма и весьма своенравная и не очень эффективная.
Нет, сборщик мусора, он же управитель памятью, сделает вам пустой объект, где все ссылки забиты null-ами и дальше уже на этапе инициализации они будут переписываться на верные значения.
И это — очередное 100500е место, где сборщик мусора мешает сделать нормальный язык. Возникает вопрос: а оно точно надо? Мы точно получаем выигрыш из-за того, что не используем разного рода scoped_ptr'ы или shared_ptr'ы, а используем «модный» «полноценный» сборщик мусора?
Обидно то, что мы теперь с этим убожеством связаны на многие годы, так как эта каракатица поселилась в самом сердце Android'а…Sirikid
20.02.2017 22:49+1Я не предлагаю от него избавляться. Я предлагаю избавляться от Java. Ибо все преимущества ради которых заплатили весьма и весьма немалую цену оказались «пшиком».
Где пруфы?
У Java есть обратаная совместимость и отличная виртуальная машина, сам язык довольно простой опять же за счет сборки мусора.
Про сборщики мусора вы и Saffron несете какую-то ерунду, посмотрите на Kotlin, он работает на той же виртуальной машине и предоставляет очень неплохой интероп с Java и он null-safe, все места из которых может вылететь NPE определяются статически.
Saffron
21.02.2017 00:48> он null-safe, все места из которых может вылететь NPE определяются статически.
А пруфы есть? Система типов java, насколько я помню, не слишком-то и sound, и вряд ли надстройка способна это изменитьSirikid
21.02.2017 01:22Есть, NPE можно либо явно бросить, либо получить развернув nullable-тип или platform-тип.
throw NullPointerException() foo: Bar? foo!! foo: Bar! foo!! // или baz: Bar = foo
khim
21.02.2017 02:08+1Вот прямо тут есть чудесная статья, которая показывает, что заявление «предоставляет очень неплохой интероп с Java и он null-safe» всё-таки несколько преувеличено. Kotlin — предоставляет неплохой интероп с Java ИЛИ (а не и!) он null-safe.
Никто не спорит с тем, что поверх Java-машины можно водрузить всё, что душе угодно (в качестве безумного варианта: водрузите JPC поверх вашей JVM — и получите любой язык, с любыми свойствами), но вот сохранить при этом совместимость с Java — уже не получится. Или — или, на выбор.
BlessMaster
20.02.2017 03:22+7Дело даже не в наличии/отсутствии null.
Ссылки — это инструмент контролирующий совместный доступ к памяти, поэтому null для ссылки — в принципе лишён смысла.
Если нет переменной, то не нужна и ссылка.
При этом неинициализированные переменные (заранее объявленные) вполне допустимы, компилятор контролирует порядок исполнения и не даст скомпилировать код, читающий переменную до инициализации.
Другой момент — каждый раз создавать строку в процессе преобразований — не самый эффективный паттерн, хотя, конечно, так можно (например, функция будет каждый раз создавать String и возвращать её).
Но для ввода-вывода API строится так, что пользователь сам создаёт буфер и передаёт его для заполнения в функцию. Кроме прочего, это позволяет реализовывать эффективную конкатенацию и в случае необходимости переиспользовать буфер, без повторного выделения памяти в куче (достаточно «дорогая» операция).
При этом буфер может быть произвольного типа, лишь бы он реализовал соответствующий необходимый типаж Read или Write.
При таком подходе весь контроль над тем, что на самом деле будет происходить, — в руках программиста. И это достаточно важно для языка, предназначенного быть эффективным и позиционируемого как низкоуровневый/системный.
ozkriff
20.02.2017 10:53+1Может https://docs.rs/text_io или https://docs.rs/scan_fmt (или еще что похожее) взять?
acmnu
20.02.2017 15:06Не спец по Rust, но то что копирование или перемещение зависит от типа при сходных действиях, мне очень не нравится ибо смотря на выражение let a = b я не уверен что произойдет c переменными a и b.
ozkriff
20.02.2017 15:13+1На ранних стадиях развития языка с явной семантикой перемещения экспериментировали, но в итоге пришли к текущей схеме
https://www.reddit.com/r/rust/comments/2x0wq0/did_rust_ever_experiment_with_making_moves
khim
20.02.2017 17:28+3смотря на выражение let a = b я не уверен что произойдет c переменными a и b
Ну почему же? Ясно что окажется вa
, а вот останется ли что-нибудь вb
— уже не так ясно, но это и не очень страшно: если обьект простой и копируемый, то его ненужную копию «изведёт» оптимизатор, а если сложный — и некопируемый, то попытка его использовать — это ошибка, компилятор вам не даст ничего напутать. Проблема будет если обьект копируемый, но сложный и его копировать дорого — так просто лучше не делать.
Halt
Графическое представление регионов очень удачное, только мне кажется, что левая нижняя картинка может ввести в заблуждение.
Я понимаю, что речь идет исключительно о вре?менном даунгрейде &mut до &. Однако, не знакомый с Rust человек может подумать, что это обычное использование неконстантных объектов в константном контексте.
Имхо, этот момент стоило бы пояснить подробнее.