Apakah ada alasan bahwa penugasan array Swift tidak konsisten (baik referensi maupun salinan mendalam)?

216

Saya membaca dokumentasi dan saya terus-menerus menggelengkan kepala pada beberapa keputusan desain bahasa. Tetapi hal yang benar-benar membuat saya bingung adalah bagaimana array ditangani.

Saya bergegas ke taman bermain dan mencoba ini. Anda dapat mencobanya juga. Jadi contoh pertama:

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

Di sini adan bkeduanya [1, 42, 3], yang dapat saya terima. Array direferensikan - OK!

Sekarang lihat contoh ini:

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

cadalah [1, 2, 3, 42]TAPI dadalah [1, 2, 3]. Yaitu, dmelihat perubahan pada contoh terakhir tetapi tidak melihatnya dalam contoh ini. Dokumentasi mengatakan itu karena panjangnya berubah.

Sekarang, bagaimana dengan yang ini:

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

eadalah [4, 5, 3], yang keren. Sangat menyenangkan memiliki pengganti multi-indeks, tetapi fMASIH tidak melihat perubahan meskipun panjangnya tidak berubah.

Jadi untuk meringkasnya, referensi umum ke array melihat perubahan jika Anda mengubah 1 elemen, tetapi jika Anda mengubah beberapa elemen atau menambahkan item, salinan dibuat.

Ini sepertinya desain yang sangat buruk bagi saya. Apakah saya benar dalam memikirkan ini? Apakah ada alasan saya tidak melihat mengapa array harus bertindak seperti ini?

EDIT : Array telah berubah dan sekarang memiliki semantik nilai. Jauh lebih waras!

Cthutu
sumber
95
Sebagai catatan, saya tidak berpikir pertanyaan ini harus ditutup. Swift adalah bahasa baru, jadi akan ada pertanyaan seperti ini untuk sementara waktu seperti yang kita semua pelajari. Saya menemukan pertanyaan ini sangat menarik dan saya berharap bahwa seseorang akan memiliki kasus yang menarik di pertahanan.
Joel Berger
4
@ Joel Baik, tanyakan pada programmer, Stack Overflow adalah untuk masalah pemrograman tertentu yang tidak terbuka.
bjb568
21
@ bjb568: Tapi itu bukan pendapat. Pertanyaan ini harus dijawab dengan fakta. Jika beberapa pengembang Swift datang dan menjawab "Kami melakukannya seperti itu untuk X, Y, dan Z," maka itulah fakta yang sebenarnya. Anda mungkin tidak setuju dengan X, Y, dan Z, tetapi jika keputusan dibuat untuk X, Y, dan Z maka itu hanya fakta sejarah dari desain bahasa. Sama seperti ketika saya bertanya mengapa std::shared_ptrtidak memiliki versi non-atom, ada jawaban berdasarkan fakta, bukan pendapat (faktanya panitia mempertimbangkannya tetapi tidak menginginkannya karena berbagai alasan).
Cornstalks
7
@JasonMArcher: Hanya paragraf terakhir yang didasarkan pada pendapat (yang ya, mungkin harus dihapus). Judul sebenarnya dari pertanyaan (yang saya ambil sebagai pertanyaan aktual itu sendiri) dapat dijawab dengan fakta. Ada adalah alasan array dirancang untuk bekerja dengan cara mereka bekerja.
Cornstalks
7
Ya, seperti kata API-Beast, ini biasanya disebut "Copy-on-Half-Assed-Language-Design".
R. Martinho Fernandes

Jawaban:

109

Perhatikan bahwa semantik dan sintaks array diubah dalam versi Xcode beta 3 ( posting blog ), jadi pertanyaannya tidak berlaku lagi. Jawaban berikut berlaku untuk beta 2:


Itu untuk alasan kinerja. Pada dasarnya, mereka mencoba untuk menghindari menyalin array selama mereka dapat (dan mengklaim "kinerja seperti C"). Mengutip buku bahasa :

Untuk array, penyalinan hanya terjadi ketika Anda melakukan tindakan yang berpotensi mengubah panjang array. Ini termasuk menambahkan, memasukkan, atau menghapus item, atau menggunakan subskrip jarak untuk mengganti berbagai item dalam array.

Saya setuju bahwa ini agak membingungkan, tetapi setidaknya ada deskripsi yang jelas dan sederhana tentang cara kerjanya.

Bagian itu juga mencakup informasi tentang cara memastikan array secara unik direferensikan, cara memaksa-menyalin array, dan bagaimana memeriksa apakah dua array berbagi penyimpanan.

Lukas
sumber
61
Saya menemukan fakta bahwa Anda telah berhenti berbagi dan menyalin bendera merah besar dalam desain.
Cthutu
9
Ini benar. Seorang insinyur menjelaskan kepada saya bahwa untuk desain bahasa ini tidak diinginkan, dan merupakan sesuatu yang mereka harapkan untuk "diperbaiki" dalam pembaruan mendatang untuk Swift. Pilih dengan radar.
Erik Kerber
2
Itu hanya sesuatu seperti copy-on-write (COW) dalam manajemen memori proses anak Linux, kan? Mungkin kita bisa menyebutnya copy-on-length-alteration (COLA). Saya melihat ini sebagai desain positif.
justhalf
3
@ justhalf Saya bisa memprediksi banyak pemula yang bingung datang ke SO dan bertanya mengapa array mereka / tidak dibagikan (hanya dengan cara yang kurang jelas).
John Dvorak
11
@justhalf: SAP tetap pesimis di dunia modern, dan kedua, SAP adalah teknik implementasi saja, dan hal-hal COLA ini mengarah pada berbagi secara acak dan tidak berbagi.
Puppy
25

Dari dokumentasi resmi bahasa Swift :

Perhatikan bahwa array tidak disalin ketika Anda menetapkan nilai baru dengan sintaks subscript, karena menetapkan nilai tunggal dengan sintaks subscript tidak berpotensi mengubah panjang array. Namun, jika Anda menambahkan item baru ke array, Anda memodifikasi panjang array . Ini meminta Swift untuk membuat salinan array baru pada saat Anda menambahkan nilai baru. Selanjutnya, a adalah salinan terpisah dari array .....

Baca seluruh bagian Penugasan dan Salin Perilaku untuk Array dalam dokumentasi ini. Anda akan menemukan bahwa ketika Anda mengganti berbagai item dalam array maka array mengambil salinannya sendiri untuk semua item.

iPatel
sumber
4
Terima kasih. Saya menyebut teks itu dengan samar dalam pertanyaan saya. Tapi saya menunjukkan contoh di mana mengubah rentang subskrip tidak mengubah panjang dan masih disalin. Jadi, jika Anda tidak ingin salinan, Anda harus mengubahnya satu elemen pada satu waktu.
Cthutu
21

Perilaku telah berubah dengan Xcode 6 beta 3. Array bukan lagi tipe referensi dan memiliki mekanisme copy-on-write , artinya segera setelah Anda mengubah konten array dari satu atau variabel lain, array akan disalin dan hanya satu salinan akan diubah.


Jawaban lama:

Seperti yang telah ditunjukkan orang lain, Swift mencoba menghindari menyalin array jika memungkinkan, termasuk ketika mengubah nilai untuk indeks tunggal pada suatu waktu.

Jika Anda ingin memastikan bahwa variabel array (!) Unik, yaitu tidak dibagi dengan variabel lain, Anda dapat memanggil unsharemetode. Ini menyalin array kecuali sudah hanya memiliki satu referensi. Tentu saja Anda juga dapat memanggil copymetode, yang akan selalu membuat salinan, tetapi berhenti berbagi lebih disukai untuk memastikan tidak ada variabel lain yang bertahan pada array yang sama.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Pascal
sumber
hmm, bagi saya, unshare()metode itu tidak terdefinisi.
Hlung
1
@Hung Sudah dihapus dalam beta 3, saya sudah memperbarui jawaban saya.
Pascal
12

Perilaku ini sangat mirip dengan Array.Resizemetode di .NET. Untuk memahami apa yang terjadi, mungkin berguna untuk melihat sejarah .token di C, C ++, Java, C #, dan Swift.

Dalam C, struktur tidak lebih dari agregasi variabel. Menerapkannya .ke variabel tipe struktur akan mengakses variabel yang disimpan dalam struktur. Pointer ke objek tidak memiliki agregasi variabel, tetapi mengidentifikasi mereka. Jika seseorang memiliki pointer yang mengidentifikasi struktur, ->operator dapat digunakan untuk mengakses variabel yang disimpan dalam struktur yang diidentifikasi oleh pointer.

Dalam C ++, struktur dan kelas tidak hanya variabel agregat, tetapi juga dapat melampirkan kode padanya. Menggunakan .untuk memanggil suatu metode akan pada variabel meminta metode itu untuk bertindak atas isi dari variabel itu sendiri ; menggunakan ->pada variabel yang mengidentifikasi objek akan meminta metode itu untuk bertindak atas objek yang diidentifikasi oleh variabel.

Di Jawa, semua tipe variabel kustom hanya mengidentifikasi objek, dan memanggil metode pada variabel akan memberi tahu metode objek apa yang diidentifikasi oleh variabel. Variabel tidak dapat menampung segala jenis tipe data komposit secara langsung, juga tidak ada cara yang digunakan suatu metode untuk mengakses variabel yang diminta. Pembatasan ini, meskipun secara semantik membatasi, sangat menyederhanakan runtime, dan memfasilitasi validasi bytecode; penyederhanaan seperti itu mengurangi overhead sumber daya Jawa pada saat pasar peka terhadap masalah-masalah seperti itu, dan dengan demikian membantunya mendapatkan daya tarik di pasar. Mereka juga berarti bahwa tidak perlu token yang setara dengan yang .digunakan dalam C atau C ++. Meskipun Java bisa digunakan ->dengan cara yang sama seperti C dan C ++, pembuatnya memilih untuk menggunakan karakter tunggal. karena itu tidak diperlukan untuk tujuan lain.

Dalam C # dan bahasa .NET lainnya, variabel dapat mengidentifikasi objek atau memegang tipe data komposit secara langsung. Ketika digunakan pada variabel tipe data komposit, .bertindak atas isi variabel; ketika digunakan pada variabel tipe referensi, .bertindak atas objek yang diidentifikasioleh itu. Untuk beberapa jenis operasi, perbedaan semantik tidak terlalu penting, tetapi untuk yang lain itu. Situasi paling problematis adalah situasi di mana metode tipe data komposit yang akan memodifikasi variabel yang digunakan, dipanggil pada variabel read-only. Jika suatu upaya dilakukan untuk memanggil metode pada nilai atau variabel read-only, kompiler umumnya akan menyalin variabel, membiarkan metode bertindak atas hal itu, dan membuang variabel. Ini umumnya aman dengan metode yang hanya membaca variabel, tetapi tidak aman dengan metode yang menulisnya. Sayangnya,. Belum memiliki sarana untuk menunjukkan metode mana yang dapat digunakan dengan aman dengan substitusi tersebut dan yang tidak.

Di Swift, metode pada agregat dapat secara tegas menunjukkan apakah mereka akan memodifikasi variabel yang mereka gunakan, dan kompiler akan melarang penggunaan metode bermutasi pada variabel read-only (daripada meminta mereka memalsukan salinan sementara dari variabel yang kemudian akan dibuang). Karena perbedaan ini, menggunakan .token untuk memanggil metode yang memodifikasi variabel yang mereka gunakan jauh lebih aman di Swift daripada di .NET. Sayangnya, fakta bahwa .token yang sama digunakan untuk tujuan itu untuk bertindak pada objek eksternal yang diidentifikasi oleh variabel berarti kemungkinan untuk kebingungan tetap ada.

Jika punya mesin waktu dan kembali ke penciptaan C # dan / atau Swift, salah satu surut bisa menghindari banyak kebingungan seputar isu-isu tersebut dengan memiliki bahasa menggunakan .dan ->token dalam mode lebih dekat dengan C ++ penggunaan. Metode agregat dan tipe referensi dapat digunakan .untuk bertindak berdasarkan variabel yang mereka gunakan , dan ->untuk bertindak berdasarkan nilai (untuk komposit) atau hal yang diidentifikasi dengan demikian (untuk jenis referensi). Namun, tidak ada bahasa yang dirancang seperti itu.

Dalam C #, praktik normal untuk metode untuk memodifikasi variabel yang dipanggil adalah untuk lulus variabel sebagai refparameter ke metode. Dengan demikian panggilan Array.Resize(ref someArray, 23);saat someArraymengidentifikasi array 20 elemen akan menyebabkan someArraymengidentifikasi array baru dari 23 elemen, tanpa mempengaruhi array asli. Penggunaan refmemperjelas bahwa metode harus diharapkan untuk memodifikasi variabel yang diminta. Dalam banyak kasus, menguntungkan untuk dapat memodifikasi variabel tanpa harus menggunakan metode statis; Alamat swift artinya dengan menggunakan .sintaks. Kerugiannya adalah tidak ada kejelasan tentang metode apa yang berlaku pada variabel dan metode apa yang berlaku pada nilai.

supercat
sumber
5

Bagi saya ini lebih masuk akal jika Anda pertama kali mengganti konstanta Anda dengan variabel:

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

Baris pertama tidak perlu mengubah ukuran a. Secara khusus, tidak perlu melakukan alokasi memori apa pun. Terlepas dari nilai i, ini adalah operasi yang ringan. Jika Anda membayangkan bahwa di bawah kap aadalah sebuah pointer, itu bisa menjadi pointer konstan.

Baris kedua mungkin jauh lebih rumit. Tergantung pada nilai idan j, Anda mungkin perlu melakukan manajemen memori. Jika Anda membayangkan itu eadalah pointer yang menunjuk ke isi array, Anda tidak bisa lagi berasumsi bahwa itu adalah pointer konstan; Anda mungkin perlu mengalokasikan blok memori baru, menyalin data dari blok memori lama ke blok memori baru, dan mengubah pointer.

Tampaknya para perancang bahasa telah berusaha menjaga (1) seringan mungkin. Karena (2) mungkin melibatkan penyalinan, mereka telah menggunakan solusi yang selalu bertindak seolah-olah Anda melakukan penyalinan.

Ini rumit, tetapi saya senang mereka tidak membuatnya lebih rumit dengan misalnya kasus khusus seperti "jika dalam (2) i dan j adalah konstanta waktu kompilasi dan kompiler dapat menyimpulkan bahwa ukuran e tidak akan untuk mengubah, maka kita tidak menyalin " .


Akhirnya, berdasarkan pemahaman saya tentang prinsip-prinsip desain bahasa Swift, saya pikir aturan umum adalah ini:

  • Gunakan konstanta ( let) selalu di mana-mana secara default, dan tidak akan ada kejutan besar.
  • Gunakan variabel ( var) hanya jika itu benar-benar diperlukan, dan sangat bervariasi dalam kasus-kasus tersebut, karena akan ada kejutan [di sini: salinan implisit aneh array dalam beberapa tetapi tidak semua situasi].
Jukka Suomela
sumber
5

Apa yang saya temukan adalah: Array akan menjadi salinan yang bisa diubah dari yang direferensikan jika dan hanya jika operasi memiliki potensi untuk mengubah panjang array . Dalam contoh terakhir Anda, f[0..2]pengindeksan dengan banyak, operasi memiliki potensi untuk mengubah panjangnya (mungkin duplikat tidak diizinkan), sehingga disalin.

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
Kumar KL
sumber
8
"diperlakukan sebagai panjang telah berubah" Saya dapat memahami bahwa itu akan disalin jika panjangnya diubah, tetapi dalam kombinasi dengan kutipan di atas, saya pikir ini adalah "fitur" yang sangat mengkhawatirkan dan yang saya pikir banyak orang akan salah
Joel Berger
25
Hanya karena sebuah bahasa baru bukan berarti tidak apa-apa untuk itu mengandung kontradiksi internal yang mencolok.
Lightness Races in Orbit
Ini telah "diperbaiki" dalam beta 3, vararray sekarang sepenuhnya bisa berubah dan letarray benar-benar tidak dapat diubah.
Pascal
4

String dan array Delphi memiliki "fitur" yang sama persis. Ketika Anda melihat implementasinya, itu masuk akal.

Setiap variabel adalah penunjuk ke memori dinamis. Memori itu berisi jumlah referensi yang diikuti oleh data dalam array. Jadi Anda dapat dengan mudah mengubah nilai dalam array tanpa menyalin seluruh array atau mengubah pointer apa pun. Jika Anda ingin mengubah ukuran array, Anda harus mengalokasikan lebih banyak memori. Dalam hal ini, variabel saat ini akan menunjuk ke memori yang baru dialokasikan. Tetapi Anda tidak dapat dengan mudah melacak semua variabel lain yang menunjuk ke array asli, jadi biarkan saja.

Tentu saja, tidak akan sulit untuk membuat implementasi yang lebih konsisten. Jika Anda ingin semua variabel melihat ukurannya, lakukan ini: Setiap variabel adalah penunjuk ke wadah yang disimpan dalam memori dinamis. Wadah menampung tepat dua hal, penghitungan referensi dan penunjuk ke data array aktual. Data array disimpan dalam blok memori dinamis yang terpisah. Sekarang hanya ada satu pointer ke data array, sehingga Anda dapat dengan mudah mengubah ukurannya, dan semua variabel akan melihat perubahannya.

Ide Dagang Philip
sumber
4

Banyak pengguna awal Swift mengeluh tentang semantik array rawan kesalahan ini dan Chris Lattner telah menulis bahwa semantik array telah direvisi untuk memberikan semantik nilai penuh ( tautan Pengembang Apple untuk mereka yang memiliki akun ). Kami harus menunggu setidaknya untuk beta berikutnya untuk melihat apa artinya ini sebenarnya.

Gael
sumber
1
Perilaku Array baru sekarang tersedia pada SDK yang disertakan dengan iOS 8 / Xcode 6 Beta 3.
smileyborg
0

Saya menggunakan .copy () untuk ini.

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
sumber
1
Saya mendapatkan "Nilai tipe '[Int]' tidak memiliki anggota 'salin'" ketika saya menjalankan kode Anda
jreft56
0

Apakah ada perubahan perilaku array di versi Swift yang lebih baru? Saya baru saja menjalankan contoh Anda:

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

Dan hasil saya adalah [1, 42, 3] dan [1, 2, 3]

jreft56
sumber