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

221

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

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

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

Здесь a и b являются как [1, 42, 3], которые я могу принять. Массивы ссылаются - OK!

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

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], что является прохладным. Хорошо иметь замену с несколькими индексами, но f STILL не видит изменения, даже если длина не изменилась.

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

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

EDIT: Массивы изменились и теперь имеют семантику значений. Гораздо более разумно!

  • 2
    Семантика Swift-массива довольно запутанная, говорят, что его значение типизировано. Вот обсуждение этого devforums.apple.com/message/975661#975661
  • 95
    Для протокола, я не думаю, что этот вопрос должен быть закрыт. Swift - это новый язык, поэтому такие вопросы будут возникать какое-то время, пока мы все учимся. Я нахожу этот вопрос очень интересным, и я надеюсь, что кто-то будет иметь убедительные аргументы в защиту.
Показать ещё 17 комментариев
Теги:
arrays

9 ответов

105
Лучший ответ

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


Это по соображениям производительности. В основном, они стараются избегать копирования массивов, пока они могут (и требуют "C-like performance" ). Чтобы процитировать язык book:

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

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

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

  • 61
    Я нахожу тот факт, что у вас есть и общий доступ, и скопируйте БОЛЬШОЙ красный флаг в дизайне.
  • 9
    Это правильно. Инженер сказал мне, что для языкового дизайна это нежелательно, и что они надеются «исправить» в будущих обновлениях Swift. Голосовать с радаров.
Показать ещё 12 комментариев
26

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

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

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

  • 4
    Благодарю. Я смутно упомянул этот текст в своем вопросе. Но я показал пример, когда изменение диапазона индекса не изменило длину и все равно скопировало. Поэтому, если вам не нужна копия, вы должны менять ее по одному элементу за раз.
21

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


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

Как указывали другие, 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]
  • 0
    хм, для меня этот метод unshare() не определен.
  • 1
    @Hlung Это было удалено в бета-версии 3, я обновил свой ответ.
12

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

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

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

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

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

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

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

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

5

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

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

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

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

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

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


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

  • Использовать константы (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
    «рассматривается как длина изменилась» Я могу понять, что она будет скопирована, если длина будет изменена, но в сочетании с цитатой выше, я думаю, что это действительно вызывающая беспокойство «особенность», и я думаю, что многие люди ошибаются
  • 0
    @JoelBerger: Хм, спасибо за информацию
Показать ещё 2 комментария
4

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

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

Строки и массивы Delphi имели ту же самую "особенность". Когда вы посмотрели на реализацию, это имело смысл.

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

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

0

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню