Я читаю документацию и постоянно качаю головой, думая о некоторых дизайнерских решениях языка. Но что меня действительно озадачило, так это то, как обрабатываются массивы.
Я бросился на площадку и попробовал это. Вы можете попробовать их тоже. Итак, первый пример:
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]
круто. Хорошо иметь многоиндексную замену, но f
STILL не видит изменений, даже если длина не изменилась.
Таким образом, чтобы подвести итог, общие ссылки на массив видят изменения, если вы меняете 1 элемент, но если вы меняете несколько элементов или добавляете элементы, создается копия.
Это выглядит как очень плохой дизайн для меня. Правильно ли я думаю об этом? Есть ли причина, по которой я не понимаю, почему массивы должны действовать так?
РЕДАКТИРОВАТЬ : массивы изменились и теперь имеют семантику значений. Гораздо более вменяемый!
std::shared_ptr
нет неатомной версии, был ответ, основанный на факте, а не на мнении (дело в том, что комитет рассматривал это, но не хотел его по разным причинам).Ответы:
Обратите внимание, что семантика и синтаксис массива были изменены в версии Xcode beta 3 ( запись в блоге ), поэтому вопрос больше не применяется. Следующий ответ относится к бета 2:
Это из соображений производительности. По сути, они стараются избегать копирования массивов как можно дольше (и заявляют о «C-подобной производительности»). Процитирую книгу по языку :
Я согласен, что это немного сбивает с толку, но, по крайней мере, есть четкое и простое описание того, как это работает.
Этот раздел также содержит информацию о том, как обеспечить уникальную ссылку на массив, как принудительно копировать массивы и как проверить, разделяют ли два массива хранилище.
источник
Из официальной документации языка Swift :
Прочитайте весь раздел Назначение и поведение копирования для массивов в этой документации. Вы обнаружите, что когда вы заменяете диапазон элементов в массиве, массив берет копию себя для всех элементов.
источник
Поведение изменилось с Xcode 6 beta 3. Массивы больше не являются ссылочными типами и имеют механизм копирования при записи , то есть, как только вы измените содержимое массива из одной или другой переменной, массив будет скопирован, и только одна копия будет изменена.
Старый ответ:
Как уже отмечали другие, Swift старается по возможности избегать копирования массивов, в том числе при одновременном изменении значений для отдельных индексов .
Если вы хотите быть уверены, что переменная массива (!) Уникальна, то есть не используется совместно с другой переменной, вы можете вызвать
unshare
метод. Это копирует массив, если он не имеет только одну ссылку. Конечно, вы также можете вызватьcopy
метод, который всегда будет делать копию, но предпочтительным является unshare, чтобы убедиться, что никакая другая переменная не удерживается в том же массиве.источник
unshare()
метод не определен.Поведение чрезвычайно похоже на
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
проясняет, что метод должен изменить переменную, для которой он вызывается. Во многих случаях полезно иметь возможность изменять переменные без использования статических методов; Быстрые адреса, что означает использование.
синтаксиса. Недостатком является то, что он теряет ясность относительно того, какие методы воздействуют на переменные и какие методы воздействуют на значения.источник
Для меня это имеет больше смысла, если вы сначала замените свои константы переменными:
В первой строке никогда не нужно менять размер
a
. В частности, ему никогда не нужно выделять память. Независимо от стоимостиi
, это легкая операция. Если представить, что под капотомa
находится указатель, он может быть постоянным указателем.Вторая строка может быть намного сложнее. В зависимости от значений
i
иj
, вам может потребоваться управление памятью. Если вы представляете, чтоe
это указатель, который указывает на содержимое массива, вы больше не можете предполагать, что это постоянный указатель; вам может потребоваться выделить новый блок памяти, скопировать данные из старого блока памяти в новый блок памяти и изменить указатель.Кажется, что разработчики языка старались сделать (1) как можно более легким. Поскольку (2) может в любом случае включать копирование, они прибегают к решению, которое всегда действует так, как если бы вы делали копию.
Это сложно, но я рад, что они не сделали его еще более сложным, например, в особых случаях, таких как «если в (2) i и j являются константами времени компиляции, и компилятор может сделать вывод, что размер e не равен изменить, то мы не копируем » .
Наконец, основываясь на моем понимании принципов проектирования языка Swift, я думаю, что общие правила таковы:
let
) всегда везде по умолчанию, и никаких серьезных сюрпризов не будет.var
), только если это абсолютно необходимо, и будьте осторожны в этих случаях, так как будут неожиданности [здесь: странные неявные копии массивов в некоторых, но не во всех ситуациях].источник
Я обнаружил следующее: массив будет изменчивой копией, на которую есть ссылка, если и только если операция может изменить длину массива . В последнем примере,
f[0..2]
индексируя по многим, операция может изменить свою длину (возможно, дубликаты не разрешены), поэтому она копируется.источник
var
массивы теперь полностью изменяемы, аlet
массивы полностью неизменяемы.У строк и массивов Delphi была та же самая «особенность». Когда вы посмотрели на реализацию, это имело смысл.
Каждая переменная является указателем на динамическую память. Эта память содержит счетчик ссылок, за которым следуют данные в массиве. Таким образом, вы можете легко изменить значение в массиве, не копируя весь массив или не изменяя указатели. Если вы хотите изменить размер массива, вы должны выделить больше памяти. В этом случае текущая переменная будет указывать на вновь выделенную память. Но вы не можете легко отследить все другие переменные, которые указывали на исходный массив, поэтому вы оставляете их в покое.
Конечно, было бы нетрудно сделать более последовательную реализацию. Если вы хотите, чтобы все переменные увидели изменение размера, сделайте следующее: каждая переменная является указателем на контейнер, хранящийся в динамической памяти. Контейнер содержит ровно две вещи: счетчик ссылок и указатель на фактические данные массива. Данные массива хранятся в отдельном блоке динамической памяти. Теперь есть только один указатель на данные массива, так что вы можете легко изменить его размер, и все переменные увидят изменения.
источник
Многие ранние последователи Swift жаловались на эту подверженную ошибкам семантику массива, и Крис Латтнер написал, что семантика массива была пересмотрена, чтобы обеспечить семантику полной стоимости ( ссылка Apple Developer для тех, у кого есть учетная запись ). Нам придется подождать хотя бы следующую бета-версию, чтобы понять, что именно это означает.
источник
Я использую .copy () для этого.
источник
Изменилось ли что-нибудь в поведении массивов в более поздних версиях Swift? Я просто запускаю ваш пример:
И мои результаты [1, 42, 3] и [1, 2, 3]
источник