Чи є причина, що присвоєння масиву Swift суперечливе (ні посилання, ні глибока копія)?


217

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

Я кинувся на дитячий майданчик і спробував їх. Ви можете спробувати їх теж. Отже перший приклад:

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 елемент, але якщо ви змінюєте кілька елементів або додаєте елементи, робиться копія.

Це здається мені дуже поганим дизайном. Я правий, думаючи про це? Чи є причина, чому я не розумію, чому масиви повинні діяти так?

РЕДАГУВАТИ : Масиви змінились і тепер мають значення семантики. Набагато розумніший!


95
Для протоколу я не думаю, що це питання слід закривати. Свіфт - це нова мова, тому такі питання, як ми всі вчимо, будуть виникати деякий час. Я вважаю це питання дуже цікавим, і я сподіваюся, що хтось матиме вагомі аргументи щодо захисту.
Джоель Бергер,

4
@Joel Fine, поцікавтеся у програмістів, Stack Overflow - для конкретних непрофільних проблем програмування.
bjb568

21
@ bjb568: Однак це не думка. На це питання слід відповідати фактами. Якщо якийсь розробник Swift приходить і відповідає: "Ми зробили це так для X, Y і Z", то це прямо факт. Ви можете не погодитися з X, Y та Z, але якщо було прийнято рішення щодо X, Y та Z, то це лише історичний факт дизайну мови. Подібно до того, як я запитав, чому std::shared_ptrнемає неатомної версії, була відповідь, заснована на фактах, а не на думці (справа в тому, що комітет розглядав її, але не хотів її з різних причин).
Cornstalks

7
@JasonMArcher: Лише останній абзац базується на думці (яку, можливо, слід вилучити). Фактична назва питання (яке я приймаю як власне питання) відповідає фактами. Там є причиною масиви були розроблені , щоб працювати так , як вони працюють.
Cornstalks

7
Так, як сказав API-звір, це зазвичай називається "Копіювання на половину-мовою-дизайн мови".
R. Martinho Fernandes

Відповіді:


110

Зверніть увагу, що семантика масиву та синтаксис були змінені у версії Xcode beta 3 ( допис у блозі ), тому питання більше не застосовується. До бета-версії 2 застосовувалася така відповідь:


Це з міркувань продуктивності. В основному вони намагаються уникати копіювання масивів до тих пір, поки можуть (і заявляють про "подібність до С"). Щоб процитувати мовну книгу :

Для масивів копіювання відбувається лише тоді, коли ви виконуєте дію, яка може змінити довжину масиву. Це включає додавання, вставку або видалення елементів або використання індексу діапазону для заміни діапазону елементів у масиві.

Я згоден, що це трохи заплутано, але принаймні є чіткий і простий опис того, як це працює.

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


61
Я вважаю той факт, що ви одночасно скасували спільний доступ та скопіювали ВЕЛИКИЙ червоний прапор у дизайні.
Ктуту

9
Це вірно. Інженер описав мені, що для мовного дизайну це не бажано, і це те, що вони сподіваються "виправити" у майбутніх оновленнях Swift. Голосуйте за допомогою радарів.
Ерік Кербер

2
Це просто щось на зразок копіювання на запис (COW) у керуванні пам’яттю дочірнього процесу Linux, правда? Можливо, ми можемо назвати це зміною копіювання на довжину (COLA). Я бачу в цьому позитивний дизайн.
justhalf

3
@justhalf Я можу передбачити купу заплутаних початківців, які приходять до SO та запитують, чому їх масиви були / не були спільними (лише менш зрозумілим способом).
Джон Дворжак,

11
@justhalf: COW у будь-якому випадку є песимізацією в сучасному світі, а по-друге, COW - це техніка, що застосовується лише для реалізації, і ця штука COLA призводить до абсолютно випадкового обміну та розшарування спільного доступу.
Щеня,

25

З офіційної документації мови Swift :

Зверніть увагу, що масив не копіюється, коли ви встановлюєте нове значення з синтаксисом індексу, оскільки встановлення одного значення з синтаксисом індексу не може змінити довжину масиву. Однак якщо ви додаєте новий елемент до масиву, ви змінюєте довжину масиву . Це спонукає Swift створити нову копію масиву в той момент, коли ви додасте нове значення. Відтепер a - це окрема, незалежна копія масиву .....

Прочитайте весь розділ Призначення та копіювання поведінки масивів у цій документації. Ви побачите, що коли ви заміните діапазон елементів у масиві, тоді масив робить копію себе для всіх елементів.


4
Дякую. У своєму питанні я посилався на цей текст неясно. Але я продемонстрував приклад, коли зміна діапазону індексу не змінила довжину і все одно скопіювала. Отже, якщо ви не хочете копію, вам доведеться міняти її по одному елементу.
Ктуту

21

Поведінка змінилася з Xcode 6 beta 3. Масиви більше не є посилальними типами і мають механізм копіювання-на-запис , тобто, як тільки ви зміните вміст масиву з тієї чи іншої змінної, масив буде скопійовано і лише одна копія буде змінена.


Стара відповідь:

Як зазначали інші, Свіфт намагається уникати копіювання масивів, якщо це можливо, в тому числі при зміні значень для окремих індексів за раз.

Якщо ви хочете бути впевнені, що змінна масиву (!) Є унікальною, тобто не ділиться з іншою змінною, ви можете викликати unshareметод. Це копіює масив, якщо він уже не має лише одного посилання. Звичайно, ви також можете викликати copyметод, який завжди буде робити копію, але віддаляти перевагу бажано, щоб переконатися, що жодна інша змінна не містить того самого масиву.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

хм, для мене цей unshare()метод невизначений.
Hlung

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 # та / або Swift, можна було б ретроактивно уникнути великої плутанини, пов’язаної з такими проблемами, завдяки використанню мов .і ->жетонів набагато ближче до використання C ++. Методи як агрегатів, так і посилальних типів можуть використовуватись як .для дії на змінну, на яку вони були викликані, так і ->для дії на значення (для композитів) або на ідентифіковану цим річ ​​(для еталонних типів). Проте жодна мова не розроблена таким чином.

У C # звичайною практикою методу модифікації змінної, на основі якої він викликається, є передача змінної як 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) i і 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
"розглядається як зміна довжини", я можу зрозуміти, що це буде скопійовано, якщо довжина буде змінена, але в поєднанні з цитатою вище, я думаю, це дійсно тривожна "особливість", і така, що, на мою думку, багато людей помиляться
Joel Berger

25
Те, що мова нова, не означає, що вона може містити кричущі внутрішні суперечності.
Гонки легкості на орбіті

Це було виправлено в бета-версії 3, varмасиви тепер повністю змінюються, а letмасиви повністю незмінні.
Паскаль

4

Рядки та масиви Delphi мали точно таку ж "особливість". Коли ви подивились на реалізацію, це мало сенс.

Кожна змінна є покажчиком на динамічну пам'ять. Ця пам’ять містить кількість посилань, за якою йдуть дані в масиві. Таким чином, ви можете легко змінити значення масиву, не копіюючи весь масив або не змінюючи жодних покажчиків. Якщо ви хочете змінити розмір масиву, вам доведеться виділити більше пам'яті. У цьому випадку поточна змінна буде вказувати на щойно виділену пам'ять. Але ви не можете легко відстежити всі інші змінні, які вказували на вихідний масив, тому залиште їх у спокої.

Звичайно, не складно буде зробити більш послідовне впровадження. Якщо ви хотіли, щоб усі змінні бачили зміну розміру, зробіть це: Кожна змінна є покажчиком на контейнер, що зберігається в динамічній пам'яті. Контейнер вміщує рівно дві речі, кількість посилань та вказівник на фактичні дані масиву. Дані масиву зберігаються в окремому блоці динамічної пам'яті. Зараз є лише один вказівник на дані масиву, тому ви можете легко змінити його розмір, і всі змінні побачать зміни.


4

Багато ранніх розробників Swift скаржилися на цю схильну до помилок семантику, і Кріс Латтнер написав, що семантика масиву була переглянута, щоб надати повноцінну семантику ( посилання розробника Apple для тих, хто має обліковий запис ). Нам доведеться почекати принаймні наступної бета-версії, щоб побачити, що це саме означає.


1
Нова поведінка масиву тепер доступна з SDK, що входить до складу iOS 8 / Xcode 6 Beta 3.
smileyborg 07

0

Для цього я використовую .copy ().

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 

1
Я отримую "Значення типу '[Int]' не має учасника" копіювати "", коли я запускаю ваш код
jreft56,

0

Чи змінилося щось у поведінці масивів у пізніших версіях Swift? Я просто запускаю ваш приклад:

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

І мої результати: [1, 42, 3] та [1, 2, 3]

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.