Swift struct boleh berkongsi objek boleh ubah selepas penetapan. Untuk memastikan salinan kekal berasingan, gunakan sifat jenis nilai atau sembunyikan storan class di sebalik copy-on-write. Panduan ini membina pengira baris jubin sintetik untuk pratonton desktop Mac dan memastikan bahawa penyuntingan satu salinan tidak mengubah salinan yang lain.

Mengapa menyalin struct masih mengubah yang asal?

Andaikan pratonton desktop mengira lebar sebaris jubin. Anda menyalin tetapannya untuk mencuba lebar jubin yang lain, dengan harapan pratonton asal kekal sama. Jangkaan itu tidak tercapai apabila kedua-dua nilai tetapan mengandungi rujukan kepada objek boleh ubah yang sama.

Swift menyalin nilai tersimpan struct apabila menetapkannya kepada pemboleh ubah lain. Bagi sifat class, nilai tersimpan itu ialah rujukan: objek yang dirujuk masih dikongsi. Pembungkusnya masih struct, tetapi tingkah laku yang boleh diperhatikannya boleh melibatkan perubahan yang dikongsi. Peraturan ini selaras dengan perbezaan yang didokumenkan oleh Swift antara jenis nilai dan jenis rujukan.

Contoh lengkap ini menunjukkan masalah tersebut. Contoh ini hanya menggunakan pustaka standard Swift, jadi tiada import diperlukan.

final class TileDimensions {
    var width: Int

    init(width: Int) {
        self.width = width
    }
}

struct SharedLayout {
    let dimensions: TileDimensions
}

let original = SharedLayout(
    dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60

assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60

Mengisytiharkan sifat atau pembungkus dengan let tidak membekukan sifat boleh ubah objek. Ia menghalang rujukan atau nilai struct itu daripada digantikan. Bab Structures and Classes dalam dokumentasi Swift membezakan secara jelas antara rujukan malar dengan keadaan objek yang tidak boleh diubah.

Meletakkan objek ini dalam tatasusunan juga mengekalkan identiti yang dikongsi. Salinan tatasusunan mengurus elemennya secara berasingan, tetapi elemen yang bernilai class itu sendiri ialah rujukan. Menggantikan elemen memberi kesan kepada satu tatasusunan; mengubah objek yang dirujuk oleh kedua-dua tatasusunan dapat dilihat melalui kedua-duanya. Apple menunjukkan perbezaan ini dalam dokumentasi Array.

Reka bentuk manakah yang memberikan nilai pengiraan secara berasingan?

Tentukan kontrak sebelum memilih pelaksanaan: selepas var edited = original, setiap suntingan yang disokong pada mana-mana susun atur mesti membiarkan lebar terhitung susun atur yang satu lagi tidak berubah. Pembaca jenis itu tidak sepatutnya perlu mengetahui sama ada storannya dikongsi secara dalaman.

Panduan keputusan berikut menggunakan kontrak itu pada model pratonton setempat. Ini ialah saranan reka bentuk yang berasaskan peraturan penyalinan yang didokumenkan.

Situasi Reka bentuk yang sesuai Syarat untuk mengekalkan kebebasan
Tetapan terdiri daripada nombor, rentetan atau komponen lain yang mempunyai semantik nilai Simpan nilai itu terus dalam struct Pastikan komponen bersarang juga memberikan nilai yang boleh diperhatikan secara berasingan
Storan berasaskan class sedia ada mesti dikekalkan Gunakan storan copy-on-write peribadi Setiap perubahan mesti mengasingkan keadaan boleh ubah yang dikongsi terlebih dahulu
Syot kilat berasingan hanya diperlukan pada operasi tertentu yang dinamakan Sediakan operasi penyalinan yang jelas Dokumentasikan bahawa penetapan biasa masih berkongsi objek melainkan pembungkus itu turut memastikan kebebasan
Beberapa bahagian program sengaja mengubah satu objek Kekalkan semantik rujukan Huraikan tingkah laku objek kongsi dalam API

Untuk tiga tetapan integer, sifat struct secara langsung ialah pilihan paling ringkas. Pelaksanaan berasaskan class di bawah berguna apabila menyesuaikan reka bentuk storan sedia ada atau memahami cara sempadan ini berfungsi; ia tidak menunjukkan kelebihan prestasi untuk pengiraan kecil ini.

Copy-on-write membolehkan dua nilai berkongsi storan sehingga salah satunya berubah. Apple menerangkan bahawa pembungkusan class dalam struct dan penggunaan copy-on-write ialah cara untuk menggabungkan storan yang diperuntukkan secara berasingan dengan semantik nilai dalam Explore Swift performance. Membungkus class sahaja tidak melaksanakan tingkah laku itu.

Kaedah copy() yang jelas sahaja juga masih meninggalkan jurang: pemanggil masih boleh menggunakan penetapan biasa tanpa memanggilnya. Jika penetapan sebahagian daripada kontrak nilai anda, laluan perubahan dalam pembungkus mesti memastikan kebebasan secara automatik.

Bagaimanakah copy-on-write dilaksanakan dan diperiksa?

Gunakan satu objek storan peribadi, bina objek itu di dalam pembungkus, dan dedahkan hanya nilai terhitung serta kaedah perubahan terkawal. Sejurus sebelum mengubah storan, panggil isKnownUniquelyReferenced. Jika keunikan tidak dapat dipastikan, gantikan storan itu dengan salinan berasingan. Apple mendokumentasikan urutan ini secara khusus untuk melaksanakan copy-on-write.

Pengiraan contoh ini menggunakan input ilustrasi: tiga jubin, setiap satunya selebar 40 mata, dengan jarak 10 mata antara jubin bersebelahan. Terdapat dua jarak, jadi lebar baris ialah 3 * 40 + 2 * 10 = 140 mata. Meningkatkan lebar jubin kepada 60 menghasilkan 200 mata.

Untuk mencuba program lengkap ini pada Mac, jalankan swift --version dalam Terminal terlebih dahulu untuk mengesahkan bahawa rantaian alat Swift tersedia, seperti yang disarankan dalam panduan Getting Started Swift. Simpan kod berikut sebagai Layout.swift, kemudian jalankan swift Layout.swift dari direktorinya. Program ini mengira dalam memori dan mencetak hasilnya.

struct TileLayout {
    private final class Storage {
        let columns: Int
        var tileWidth: Int
        var gap: Int

        init(columns: Int, tileWidth: Int, gap: Int) {
            self.columns = columns
            self.tileWidth = tileWidth
            self.gap = gap
        }

        func copy() -> Storage {
            Storage(
                columns: columns,
                tileWidth: tileWidth,
                gap: gap
            )
        }
    }

    private var storage: Storage

    init(columns: Int, tileWidth: Int, gap: Int) {
        precondition((1...1000).contains(columns))
        precondition((0...1000).contains(tileWidth))
        precondition((0...1000).contains(gap))
        storage = Storage(
            columns: columns,
            tileWidth: tileWidth,
            gap: gap
        )
    }

    var rowWidth: Int {
        storage.columns * storage.tileWidth
            + (storage.columns - 1) * storage.gap
    }

    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
    }

    mutating func setTileWidth(_ width: Int) {
        precondition((0...1000).contains(width))
        ensureUniqueStorage()
        storage.tileWidth = width
    }

    mutating func setGap(_ gap: Int) {
        precondition((0...1000).contains(gap))
        ensureUniqueStorage()
        storage.gap = gap
    }
}

var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original

edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)

original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)

let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)

print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220

Program ini tidak memerlukan import atau pakej luaran. Julat yang diterima ialah batasan contoh yang disengajakan: satu hingga 1,000 lajur, dan sifar hingga 1,000 mata bagi lebar dan jarak. Julat ini memastikan pengiraan integer terkawal. Argumen di luar julat menyebabkan prakondisi gagal; nilainya tidak dikepit secara senyap. Jika kemudian penyetel ini menerima teks yang boleh disunting daripada antara muka desktop, sahkan input itu sebelum memanggilnya dan sediakan cara untuk pengguna membetulkannya.

Pemeriksaan ini meliputi tiga keperluan yang boleh diperhatikan: menyunting susun atur yang disalin mengekalkan susun atur asal, menyunting susun atur asal mengekalkan salinannya, dan mengekalkan syot kilat malar mengekalkan hasilnya sepanjang suntingan kemudian. Pemeriksaan ini menguji tingkah laku tanpa memerlukan bilangan peruntukan tertentu. Swift tidak tersedia dalam persekitaran penyelidikan, jadi contoh ini diperiksa tetapi tidak dijalankan di sana; output yang ditunjukkan ialah hasil yang dijangka.

Apakah yang mesti kekal benar apabila pembungkus berkembang?

Kebebasan dalam contoh ini berpunca daripada keseluruhan sempadan APInya. Storan bersifat peribadi, tiada pemula menerima objek storan yang disimpan di luar, dan tiada pengambil memulangkannya. Kedua-dua kaedah perubahan melalui operasi pengasingan yang sama. Pengambil mengira hasil tanpa mengubah storan.

Apabila menambah ciri, telusuri setiap laluan baharu yang boleh mengubah keadaan yang boleh diperhatikan. Penyetel, subskrip atau panggil balik penyuntingan baharu memerlukan layanan yang sama seperti kaedah sedia ada. Pengambil yang memulangkan objek anak boleh ubah membolehkan pemanggil memintas pengasingan, walaupun pengambil itu sendiri baca sahaja.

Salinan storan juga mesti merangkumi semua keadaan boleh ubah yang berkaitan dengan nilai itu. Di sini, Storage.copy() menyalin tiga integer, dan itu mencukupi. Jika storan kemudian mengandungi anak bernilai class yang boleh ubah, penetapan anak itu kepada storan baharu akan mengekalkan satu lagi rujukan yang dikongsi. Salin anak itu dengan sewajarnya atau gantikannya dengan komponen yang sudah menyediakan semantik nilai. Tingkah laku elemen class yang didokumenkan untuk Array menggambarkan sempadan yang sama.

Gunakan urutan ini apabila melanjutkan pengira:

  1. Salin satu susun atur sebelum menguji setiap operasi penyuntingan baharu.
  2. Pastikan susun atur yang tidak disentuh mengekalkan hasil sebelumnya.
  3. Ulangi dengan peranan yang diterbalikkan.
  4. Kekalkan satu syot kilat, lakukan beberapa suntingan kemudian, dan periksanya semula.
  5. Periksa setiap sifat yang dipulangkan dan argumen pemula untuk mencari rujukan boleh ubah yang boleh terlepas daripada pembungkus.

Class storan final menghalang subkelas daripada menambah tingkah laku, tetapi tidak menjadikan sifatnya tidak boleh diubah. Begitu juga, menandakan operasi mutating membolehkannya mengemas kini struct; logik pengasinganlah yang mengekalkan kebebasan contoh ini.

Bagaimanakah objek Foundation patut disalin?

Jika model sedia ada menyimpan objek Foundation seperti NSMutableString, gunakan tingkah laku penyalinan yang didokumenkan bagi jenis konkrit itu apabila melaksanakan pengasingan storan. Penetapan Swift tidak memanggil kaedah penyalinan Foundation secara automatik.

Protokol Foundation NSCopying mentakrifkan penyalinan fungsian, manakala makna tepatnya bergantung pada class. Dokumentasinya juga membenarkan objek asal dipulangkan apabila kedua-dua class dan kandungannya tidak boleh diubah. Oleh itu, identiti objek sahaja bukan ujian universal untuk menentukan sama ada sesuatu salinan memenuhi kontrak nilai anda.

Apabila anda memerlukan hasil boleh ubah daripada jenis yang mempunyai bentuk boleh ubah dan tidak boleh ubah yang berbeza, rujuk NSMutableCopying. Apple mendokumentasikan mutableCopy(with:) dan kaedah kemudahan mutableCopy() untuk tujuan itu. Class tanpa perbezaan tersebut patut mengguna pakai NSCopying.

Bagi pengira jubin, sempadan praktikalnya mudah: objek Foundation yang diterima sebagai input tidak boleh kekal sebagai alias yang boleh diubah dari luar kepada tetapan yang kononnya berasingan. Ambil syot kilat berasingan yang sesuai semasa menerima input, lakukan pengasingan yang sewajarnya sebelum suntingan, dan pulangkan syot kilat nilai, bukannya objek boleh ubah dalaman. Sahkan cara class konkrit mengendalikan sebarang kandungan boleh ubah bersarang sebelum bergantung pada operasi penyalinannya.

Adakah copy-on-write menjadikan jenis ini Sendable?

Kebebasan nilai dan pematuhan kepada keperluan konkurensi memerlukan pertimbangan yang berasingan. Dokumentasi Sendable Apple mensyaratkan ahli tersimpan struct sendable jika struct itu mempunyai pematuhan sendable yang diperiksa. Class storan peribadi yang boleh ubah tidak memenuhi syarat itu semata-mata kerana jenis luarnya ialah struct.

Pemeriksaan keunikan juga bukan penyegerakan. Apple memberi amaran bahawa isKnownUniquelyReferenced boleh memulangkan true semasa akses serentak dan memerlukan penyegerakan yang sesuai untuk penggunaan sedemikian. Pemeriksaan ini mengira rujukan kuat; alias lemah atau tanpa pemilikan masih mungkin wujud. Oleh itu, memastikan storan tidak boleh dicapai dari luar pembungkus sama pentingnya dengan memeriksa keunikan. Lihat had fungsi pustaka standard yang didokumenkan.

Kekalkan contoh ini dalam satu konteks pengiraan segerak. Jika versi kemudian perlu menghantar tetapan merentas domain konkurensi, pilihan yang lebih mudah ialah struct yang mengandungi sifat nilai sendable, dengan pematuhan Sendable yang diperiksa. Menambah @unchecked Sendable memindahkan tanggungjawab ketepatan kepada pelaksana; ia tidak menyediakan penyelarasan yang tiada. Apple menerangkan keperluan ini dalam rujukan Sendable.

Untuk pengiraan desktop yang kecil, utamakan sifat nilai secara langsung apabila ia memenuhi keperluan. Apabila storan class boleh ubah mesti dikekalkan, kekalkan kontraknya melalui pemilikan peribadi, salinan storan yang lengkap, suntingan terkawal dan pemeriksaan kebebasan yang boleh diperhatikan. Hasilnya mudah dijangka: mengubah tetapan kerja pratonton membiarkan setiap nilai susun atur yang dikekalkan memiliki hasil pengiraannya sendiri.

Sumber