Есть ли причина, по которой назначение массива Swift является непоследовательным (ни ссылка, ни глубокая копия)?

216

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

Я бросился на площадку и попробовал это. Вы можете попробовать их тоже. Итак, первый пример:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

Вот aи то bи другое [1, 42, 3], что я могу принять. Ссылки на массивы - ОК!

Теперь посмотрим на этот пример:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

cэто [1, 2, 3, 42]НО dесть [1, 2, 3]. То есть dвидел изменение в последнем примере, но не видит его в этом. Документация говорит, что это потому, что длина изменилась.

Теперь, как насчет этого:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

eэто [4, 5, 3]круто. Хорошо иметь многоиндексную замену, но fSTILL не видит изменений, даже если длина не изменилась.

Таким образом, чтобы подвести итог, общие ссылки на массив видят изменения, если вы меняете 1 элемент, но если вы меняете несколько элементов или добавляете элементы, создается копия.

Это выглядит как очень плохой дизайн для меня. Правильно ли я думаю об этом? Есть ли причина, по которой я не понимаю, почему массивы должны действовать так?

РЕДАКТИРОВАТЬ : массивы изменились и теперь имеют семантику значений. Гораздо более вменяемый!

Cthutu
источник
95
Для протокола, я не думаю, что этот вопрос должен быть закрыт. Swift - это новый язык, поэтому такие вопросы будут возникать какое-то время, пока мы все учимся. Я нахожу этот вопрос очень интересным, и я надеюсь, что кто-то будет иметь убедительные аргументы в защиту.
Джоэл Бергер
4
@Joel Хорошо, спросите об этом у программистов, переполнение стека предназначено для конкретных неопознанных проблем программирования.
bjb568
21
@ bjb568: Это не мнение, хотя. Этот вопрос должен отвечать фактам. Если приходит какой-то разработчик Swift и отвечает: «Мы сделали это так для X, Y и Z», то это факт. Вы можете не согласиться с X, Y и Z, но если решение было принято для X, Y и Z, то это просто исторический факт дизайна языка. Так же, как когда я спросил, почему std::shared_ptrнет неатомной версии, был ответ, основанный на факте, а не на мнении (дело в том, что комитет рассматривал это, но не хотел его по разным причинам).
Кукурузные початки
7
@JasonMArcher: Только самый последний абзац основан на мнении (которое, возможно, должно быть удалено). Фактическое название вопроса (которое я принимаю за сам вопрос) соответствует фактам. Там является причиной массивы были разработаны , чтобы работать так , как они работают.
Кукурузные початки
7
Да, как сказал API-Beast, это обычно называется «Copy-on-Half-Assed-Language-Design».
Р. Мартиньо Фернандес

Ответы:

109

Обратите внимание, что семантика и синтаксис массива были изменены в версии Xcode beta 3 ( запись в блоге ), поэтому вопрос больше не применяется. Следующий ответ относится к бета 2:


Это из соображений производительности. По сути, они стараются избегать копирования массивов как можно дольше (и заявляют о «C-подобной производительности»). Процитирую книгу по языку :

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

Я согласен, что это немного сбивает с толку, но, по крайней мере, есть четкое и простое описание того, как это работает.

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

Lukas
источник
61
Я нахожу тот факт, что вы не только делитесь, но и копируете БОЛЬШОЙ красный флаг в дизайне.
Cthutu
9
Это верно. Инженер сказал мне, что для языкового дизайна это нежелательно, и что они надеются «исправить» в будущих обновлениях Swift. Голосовать с радаров.
Эрик Кербер
2
Это просто что-то вроде копирования при записи (COW) в управлении дочерними процессами Linux, верно? Возможно, мы можем назвать это копированием по длине (COLA). Я вижу это как позитивный дизайн.
полугодие
3
@justhalf Я могу предсказать, что куча смущенных новичков придет в SO и спросит, почему их массивы были / не были разделены (только менее понятным способом).
Джон Дворак
11
@justhalf: COW в любом случае является пессимизацией в современном мире, и, во-вторых, COW - это метод только для реализации, и этот материал COLA приводит к абсолютно случайному обмену и разглашению.
Щенок
25

Из официальной документации языка Swift :

Обратите внимание, что массив не копируется, когда вы устанавливаете новое значение с синтаксисом нижнего индекса, поскольку установка одного значения с синтаксисом нижнего индекса не может изменить длину массива. Однако, если вы добавляете новый элемент в массив, вы изменяете длину массива . Это побуждает Swift создать новую копию массива в точке, в которую вы добавляете новое значение. Отныне, это отдельная, независимая копия массива .....

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

iPatel
источник
4
Спасибо. Я смутно упомянул этот текст в своем вопросе. Но я показал пример, когда изменение диапазона индекса не изменило длину и все равно скопировало. Поэтому, если вам не нужна копия, вы должны менять ее по одному элементу за раз.
Cthutu
21

Поведение изменилось с Xcode 6 beta 3. Массивы больше не являются ссылочными типами и имеют механизм копирования при записи , то есть, как только вы измените содержимое массива из одной или другой переменной, массив будет скопирован, и только одна копия будет изменена.


Старый ответ:

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

Если вы хотите быть уверены, что переменная массива (!) Уникальна, то есть не используется совместно с другой переменной, вы можете вызвать unshareметод. Это копирует массив, если он не имеет только одну ссылку. Конечно, вы также можете вызвать copyметод, который всегда будет делать копию, но предпочтительным является unshare, чтобы убедиться, что никакая другая переменная не удерживается в том же массиве.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
паскаль
источник
хм, для меня этот unshare()метод не определен.
Хлунг
1
@Hlung Это было удалено в бета-версии 3, я обновил свой ответ.
Паскаль
12

Поведение чрезвычайно похоже на Array.Resizeметод в .NET. Чтобы понять, что происходит, может быть полезно взглянуть на историю .токена в C, C ++, Java, C # и Swift.

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

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

В Java все пользовательские типы переменных просто идентифицируют объекты, и вызов метода для переменной сообщит методу, какой объект идентифицирован переменной. Переменные не могут содержать какой-либо тип составного типа данных напрямую, и нет никаких средств, с помощью которых метод может получить доступ к переменной, для которой он вызывается. Эти ограничения, хотя и семантически ограничивающие, значительно упрощают время выполнения и облегчают проверку байт-кода; такие упрощения позволили снизить затраты ресурсов Java в то время, когда рынок был чувствителен к таким проблемам, и, таким образом, помогли ему завоевать популярность на рынке. Они также подразумевали, что не было необходимости в токене, эквивалентном .используемому в C или C ++. Хотя Java могла бы использоваться так ->же, как C и C ++, создатели решили использовать односимвольный. так как это не было нужно для каких-либо других целей.

В C # и других языках .NET переменные могут либо идентифицировать объекты, либо напрямую содержать составные типы данных. При использовании для переменной составного типа данных .действует на содержимое переменной; при использовании на переменную ссылочного типа, .действует на объект идентифицированнойпо этому. Для некоторых видов операций семантическое различие не особенно важно, но для других это так. Наиболее проблематичными являются ситуации, в которых метод составного типа данных, который изменяет переменную, для которой он вызывается, вызывается для переменной только для чтения. Если предпринята попытка вызвать метод для значения или переменной только для чтения, компиляторы обычно копируют переменную, позволяют методу воздействовать на нее и отбрасывают переменную. Обычно это безопасно для методов, которые только читают переменную, но не безопасно для методов, которые записывают в нее. К сожалению, у .does пока нет средств указать, какие методы можно безопасно использовать с такой заменой, а какие нет.

В Swift методы на агрегатах могут явно указывать, будут ли они изменять переменную, для которой они вызываются, а компилятор запрещает использование методов с переменными переменными только для чтения (вместо того, чтобы заставлять их изменять временные копии переменной, которые затем быть сброшенным). Из-за этого различия использование .токена для вызова методов, которые изменяют переменные, для которых они вызываются, намного безопаснее в Swift, чем в .NET. К сожалению, тот факт, что один и тот же .токен используется для этой цели, чтобы воздействовать на внешний объект, идентифицируемый переменной, означает возможность путаницы.

Если был машина времени и вернулся к созданию C # и / или Свифта, можно было бы задним числом избежать большой части путаницы вокруг таких вопросов, имея язык используют .и ->маркер в моде гораздо ближе к использованию C ++. Методы как агрегатов, так и ссылочных типов могут использоваться .для воздействия на переменную, для которой они были вызваны, и ->для воздействия на значение (для композитов) или на определенную вещь (для ссылочных типов). Однако ни один из этих языков не разработан таким образом.

В C # обычной практикой для метода для изменения переменной, для которой он вызывается, является передача переменной в качестве refпараметра методу. Таким образом, вызов « Array.Resize(ref someArray, 23);когда someArrayидентифицирует массив из 20 элементов» приведет someArrayк идентификации нового массива из 23 элементов, не влияя на исходный массив. Использование refпроясняет, что метод должен изменить переменную, для которой он вызывается. Во многих случаях полезно иметь возможность изменять переменные без использования статических методов; Быстрые адреса, что означает использование .синтаксиса. Недостатком является то, что он теряет ясность относительно того, какие методы воздействуют на переменные и какие методы воздействуют на значения.

Supercat
источник
5

Для меня это имеет больше смысла, если вы сначала замените свои константы переменными:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

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

Вторая строка может быть намного сложнее. В зависимости от значений iи j, вам может потребоваться управление памятью. Если вы представляете, что eэто указатель, который указывает на содержимое массива, вы больше не можете предполагать, что это постоянный указатель; вам может потребоваться выделить новый блок памяти, скопировать данные из старого блока памяти в новый блок памяти и изменить указатель.

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

Это сложно, но я рад, что они не сделали его еще более сложным, например, в особых случаях, таких как «если в (2) i и j являются константами времени компиляции, и компилятор может сделать вывод, что размер e не равен изменить, то мы не копируем » .


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

  • Используйте constants ( let) всегда везде по умолчанию, и никаких серьезных сюрпризов не будет.
  • Используйте переменные ( var), только если это абсолютно необходимо, и будьте осторожны в этих случаях, так как будут неожиданности [здесь: странные неявные копии массивов в некоторых, но не во всех ситуациях].
Юкка Суомела
источник
5

Я обнаружил следующее: массив будет изменчивой копией, на которую есть ссылка, если и только если операция может изменить длину массива . В последнем примере, f[0..2]индексируя по многим, операция может изменить свою длину (возможно, дубликаты не разрешены), поэтому она копируется.

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3
Кумар К.Л.
источник
8
«рассматривается как длина изменилась» Я могу понять, что она будет скопирована, если длина будет изменена, но в сочетании с цитатой выше, я думаю, что это действительно вызывающая беспокойство «особенность», и я думаю, что многие люди ошибаются
Джоэл Бергер
25
Тот факт, что язык является новым, не означает, что он может содержать вопиющие внутренние противоречия.
Гонки легкости на орбите
Это было «исправлено» в бета-версии 3, varмассивы теперь полностью изменяемы, а letмассивы полностью неизменяемы.
Паскаль
4

У строк и массивов Delphi была та же самая «особенность». Когда вы посмотрели на реализацию, это имело смысл.

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

Конечно, было бы нетрудно сделать более последовательную реализацию. Если вы хотите, чтобы все переменные увидели изменение размера, сделайте следующее: каждая переменная является указателем на контейнер, хранящийся в динамической памяти. Контейнер содержит ровно две вещи: счетчик ссылок и указатель на фактические данные массива. Данные массива хранятся в отдельном блоке динамической памяти. Теперь есть только один указатель на данные массива, так что вы можете легко изменить его размер, и все переменные увидят изменения.

Торговые идеи Филиппа
источник
4

Многие ранние последователи Swift жаловались на эту подверженную ошибкам семантику массива, и Крис Латтнер написал, что семантика массива была пересмотрена, чтобы обеспечить семантику полной стоимости ( ссылка Apple Developer для тех, у кого есть учетная запись ). Нам придется подождать хотя бы следующую бета-версию, чтобы понять, что именно это означает.

гэл
источник
1
Новое поведение массива теперь доступно с SDK, включенного в iOS 8 / Xcode 6 Beta 3.
smileyborg
0

Я использую .copy () для этого.

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
источник
1
Я получаю "Значение типа '[Int]' не имеет члена 'copy'", когда я запускаю ваш код
jreft56
0

Изменилось ли что-нибудь в поведении массивов в более поздних версиях Swift? Я просто запускаю ваш пример:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

И мои результаты [1, 42, 3] и [1, 2, 3]

jreft56
источник