Как стать автором
Обновить
926.97
OTUS
Цифровые навыки от ведущих экспертов

Немного о семантиках перемещения, копирования и заимствования

Время на прочтение7 мин
Количество просмотров2.6K
Автор оригинала: dubrowgn

Существует три основных способа передачи данных в функции: перемещение (move), копирование (copy) и заимствование (borrow, иными словами, передача по ссылке). Поскольку изменяемость (мутабельность) неразрывно связана с передачей данных (например, эта функция может заимствовать мои данные, но только если она обещает смотреть на них и ничего более), в итоге мы получаем шесть различных комбинаций.

Перемещение, копирование, заимствование, мутабельность и иммутабельность

Каждый язык программирования имеет свой уровень поддержки и подход к этим семантикам:

Язык

Перемещение

Копирование

Заимствование

По умолчанию 
(для примитивных типов)

По умолчанию
(для сложных типов)

Иммутабельные параметры

С

Нет

Да

Нет*

Копирование

Нет

Да (указывается посредством 'const')

С#

Нет

Да

Да

Копирование

Мутабельное заимствование

Нет

Java

Нет

Да

Да

Копирование

Мутабельное заимствование

Да (указывается посредством 'final')

Rust

Да

Да

Да

Копирование

Перемещение/Копирование**

Да (отключаются посредством 'mut')

* Технически C поддерживает заимствование через указатели. Однако фактические данные (т.е. адрес, хранящийся в указателе) всегда копируются. Таким образом, можно утверждать, что C поддерживает косвенное заимствование, а не прямое заимствование, как, например, в Rust.

** В Rust по умолчанию используется перемещение, но типы, реализующие трейт Copy, по умолчанию копируются.

Кто знал, что все окажется так запутанно? В C все типы являются примитивными. Когда вы хотите передать “ссылку”, вы передаете адрес ячейки памяти через указатель. Этот указатель в действительности представляет собой просто целочисленное значение, которое копируется так же, как и все остальное. В большинстве языков со сборкой мусора, таких как C# или Java, сложные типы передаются по изменяемой ссылке. Rust же немного выделяется на фоне остальных языков, поддерживая все шесть семантик и по умолчанию используя для сложных типов семантику перемещения, а не заимствования.

Непоследовательность

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

C

int example1(int value, char message) {
	// value копируется
	// message копируется, память, на которую указывает message, не копируется
}
char* msg = malloc(100);
example1(1, msg);
// example1 неявно обязан никуда не передавать msg (заимствование)
free(msg);
int example2(int value, char message) {
	// value копируется
	// message копируется, память, на которую указывает message, не копируется
	free(msg);
}
char* msg = malloc(100);
example2(1, msg);
// example2 неявно обязан высвобождать msg (перемещение)
int example3(int value, const char message) {
	// value копируется
	// message копируется, память, на которую указывает message, не копируется
	// message является неизменяемым, однако мы можем убрать неизменяемость с помощью приведения типа (каста)...
	char mut_message = (char )message;
}
char* msg = malloc(100);
example3(1, msg);
free(msg);

Плюсы:

  1. Идеальная согласованность, так как все всегда копируется.

  2. Мутабельность явно указана в сигнатуре функции через 'const'

Недостатки:

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

  2. Функции, получившие константное значение, могут привести его обратно к неконстантному.

Мои наблюдения:

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

C#

int example1(int value, string message) {
	// value копируется
	// message заимствуется с возможностью изменения (передается по ссылке)
}
example1(1, "hello world");
int example2(ref int value, string message) {
	// value заимствуется с возможностью изменения (передается по ссылке)
	// message заимствуется с возможностью изменения (передается по ссылке)
}
example2(ref int_var, "hello world");

Плюсы:

  1. Передача примитивов по ссылке явно указана в сигнатуре функции (ref).

Недостатки:

  1. Передача примитивов по ссылке также является явной для вызывающей стороны. Изменение сигнатуры функции подразумевает изменения во всех местах вызова.

  2. Сложные типы могут передаваться только по ссылке (в C# они даже называются “ссылочными типами”).

  3. Нет семантики заимствования без возможности изменения.

  4. Нет семантики перемещения.

Мои наблюдения:

  1. При наличии сборщика мусора, в таких языках, как C# или Java, вероятно, нет особого смысла для семантики перемещения, поскольку сборщик мусора в конечном итоге позаботится обо всей памяти, выделенной в куче.

Rust

fn example1(value: i32, message: String) -> i32 {
	// value копируется
	// message перемещается
}
example1(1, "hello world".to_string());
fn example2(value: i32, message: SomeTypeThatImplementsCopy) -> i32 {
	// value копируется
	// message копируется
}
example2(1, some_copy_var);
fn example3(value: &i32, message: &mut String) -> i32 {
	// value заимствуется без возможности изменения
	// message заимствуется с возможностью изменения
}
let val = 1;
let mut msg = "hello world".to_string();
example3(&val, &mut msg);

Плюсы:

  1. Поддержка всех основных семантических случаев перемещения/копирования/заимствования.

  2. Заимствование явно указано в сигнатуре функции.

Недостатки:

  1. Что будет использоваться по умолчанию - перемещение или копирование, определяется для каждого типа отдельно. Это означает, что сигнатура функции не может сказать вам, что будет использовано для данного параметра.

  2. Если перемещение/копирование нежелательно в конкретной ситуации, то есть два способа отключить это поведение по умолчанию в зависимости от того, в каком направлении вам нужно двигаться (перемещение ->копирование или копирование->перемещение).

  3. Примитивные типы нельзя перемещать. Это, вероятно, не имеет такого большого значения, но я считаю это непоследовательным.

Мои наблюдения:

  1. Rust стремится превзойти C, поддерживая семантику перемещения, но неявное перемещение/копирование вносит некоторый беспорядок. Со стороны реализации функции не имеет значения, что в итоге будет задействовано, потому что данные в любом случае принадлежат функции. Но, с точки зрения вызывающей стороны, вы всегда должны помнить, какие типы реализуют трейт Copy. Это также может привести к неочевидным проблемам:

fn foo<T>(value: T) {
	// ...
}

// это работает
let c: char = 'c';
foo(c); // здесь c был скопирован
foo(c); // здесь c был скопирован

// а это нет
let s: String = "c".to_string();
foo(s); // здесь s была перемещена
foo(s); // s больше не находится в этой области видимости; ошибка компиляции

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

Копирование - это замаскированное перемещение

В чем разница между копированием и перемещением? Если вы немного поразмыслите над этим, вы сразу поймете, что копирование — это просто дублирование данных с последующим перемещением. Единственная разница, с точки зрения вызывающей стороны, заключается в том, перемещаете ли вы исходные данные или их копию. С точки зрения функции разницы нет; данные получены в любом случае. Возникает вопрос, почему вообще копирование связано с сигнатурами функций? Не лучше ли иметь какую-нибудь явную поддержку копирования на уровне языка, которая никак не связана с функциями или их сигнатурами? Rust, например, стоило очень многих хлопот сохранить явную аллокацию данных, но, в конечном итоге, главный индикатор, будет ли что-либо копироваться или перемещаться, сводится к тому, как это называется. Почему бы не сделать что-то простое и понятное как, например, это?

// отправляет сложный тип в другой поток или добавляет в очередь на выполнение и т.д.
fn send(item: move ComplexType) {
	...
}

let original: ComplextType = ComplexType::new();
// создаем явную копию элемента

let clone = copy original;
// отправляем копию оригинала

send (clone);
// - или -

send(copy original);
// отправляем исходный элемент

send(original);

Так копирование является явным и не связано с вызовом функции.

Чего же я на самом деле хочу от языка программирования?

Мои самые большие претензии к языкам программирования на данный момент таковы:

  1. Отсутствие последовательности.

  2. Копирование связано с вызовом функций.

Очевидным решением здесь является явная семантика перемещения/заимствования с последовательными умолчательным и явным операторами копирования.

  1. Семантически, поведением по умолчанию всегда является иммутабельное заимствование, независимо от типа

  2. Явно аннотируйте параметры функции для всех остальных случаев. Аннотации иммутабельности и заимствования могут быть необязательными.

  3. Явные аннотации не требуются в местах вызова.

  4. Мутабельность неявно преобразуется в иммутабельность. Иммутабельность никогда не может быть преобразована в мутабельность.

  5. Копирование всегда явное.

Язык

Перемещение

Копирование

Заимствование

По умолчанию 
(для примитивных типов)

По умолчанию
(для сложных типов)

Иммутабельные параметры

Гипотетический

Да

Да

Да

Иммутабельное заимствование

Иммутабельное заимствование

Да (отключаются посредством 'mut')


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

Заимствование

fn example1(value: i32, message: String) {
	// то же, что и для fn example1(value: ref i32, message: ref String) {
	// value и message заимствуются без возможности изменения
}
let mut val = 1; // val является мутабельной в этой области видимости
let msg = "hello world"; // msg иммутабельное в этой области видимости
example1(val, msg); // оба могут быть заимствованы без возможности изменения
example1(val, msg); // многократно
fn example2(value: mut i32, message: mut String) {
	// то же, что и для fn example2(value: mut ref i32, message: mut ref String) {
	// value и message заимствуются с возможностью изменения
	value += 2;
}

let val = 1;
let msg = "hello world";
example2(val, msg); // ошибка компиляции, и val, и msg иммутабельные

let mut val = 1;
let mut msg = "hello world";
example2(val, msg);
example2(val, msg);
// value равно 5

Перемещение

fn example3(value: move i32, message: move String) {
	// value и message перемещаются без возможности изменения
}
let mut val = 1;
let msg = "hello world";
example3(val, msg); // val становится иммутабельной при перемещении
example3(val, msg); // ошибка компиляции, val и msg перемещены в example3
fn example4(value: mut move i32, message: mut move String) {
	// value и message перемещаются с возможностью изменения
}
let mut val = 1;
let msg = "hello world";
example4(val, msg); // ошибка компиляции, msg иммутабельное
example4(val, msg); // ошибка компиляции, val и msg перемещены в example4

Копирование

fn example5(value: move i32, message: move String) {
	// value и message перемещаются без возможности изменения
}
let val = 1;
let msg = "hello world";
example5(copy val, copy msg);
example5(copy val, copy msg);
fn example6(value: mut move i32, message: mut move String) {
	// value и message копируются с возможностью изменения
	value += 2;
}
let mut val = 1;
let msg = "hello world";
// мутабельность определяется псевдонимом (в данном случае message)
// значит, тут все в порядке, хоть msg и иммутабельно
example6(copy val, copy msg);
example6(copy val, copy msg);
// val по-прежнему 1

Приглашаем всех желающих на открытое занятие «Сборка и запуск приложений. Туллинг Rust», которое состоится уже завтра в рамках онлайн-курса "Rust Developer. Basic". На занятии мы разберёмся, из каких этапов состоит сборка приложения, и как операционная система его запускает. Познакомимся с инструментами Rust для сборки и работы с кодом. Записаться на урок можно по ссылке.

Теги:
Хабы:
Всего голосов 14: ↑13 и ↓1+12
Комментарии27

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS