Daftar tangkapan cepat: apa perbedaan antara tautan yang lemah, kuat dan tidak dimiliki?


Joseph Wright, The Prisoner - Ilustrasi dari Penangkapan yang Kuat

Daftar nilai "yang ditangkap" ada di depan daftar parameter penutupan dan dapat "menangkap" nilai dari cakupan dengan tiga cara berbeda: menggunakan tautan "kuat", "lemah" atau "tidak dimiliki". Kita sering menggunakannya, terutama untuk menghindari siklus referensi yang kuat ("siklus referensi kuat" alias "mempertahankan siklus").
Mungkin sulit bagi pengembang pemula untuk memutuskan metode mana yang akan digunakan, sehingga Anda dapat menghabiskan banyak waktu untuk memilih antara "kuat" dan "lemah" atau antara "lemah" dan "tidak dimiliki", tetapi seiring waktu, Anda akan menyadari bahwa pilihan yang tepat - hanya satu.

Pertama, buat kelas sederhana:

class Singer { func playSong() { print("Shake it off!") } } 

Kemudian kami menulis fungsi yang membuat instance dari kelas Singer dan mengembalikan penutupan yang memanggil metode playSong () dari kelas Singer :

 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

Akhirnya, kita dapat memanggil sing () di mana saja untuk mendapatkan hasil bermain playSong ()

 let singFunction = sing() singFunction() 


Akibatnya, baris "Shake it off!" Akan ditampilkan.

Penangkapan yang kuat


Kecuali Anda secara eksplisit menentukan metode penangkapan, Swift menggunakan tangkapan "kuat". Ini berarti bahwa penutupan menangkap nilai-nilai eksternal yang digunakan dan tidak akan pernah membebaskannya.

Mari kita lihat kembali fungsi sing ()

 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

Konstanta taylor didefinisikan di dalam suatu fungsi, jadi dalam keadaan normal, tempatnya akan dibebaskan setelah fungsi tersebut selesai bekerja. Namun, konstanta ini digunakan di dalam penutupan, yang berarti bahwa Swift akan secara otomatis memastikan keberadaannya selama penutupan itu sendiri ada, bahkan setelah fungsi berakhir.
Ini adalah penangkapan "kuat" dalam aksi. Jika Swift mengizinkan taylor untuk dibebaskan, maka memanggil penutupan akan menjadi tidak aman - metode taylor.playSong () tidak lagi valid.

Tangkapan "Lemah" (penangkapan lemah)


Swift memungkinkan kita membuat " daftar tangkap " untuk menentukan bagaimana nilai yang digunakan ditangkap. Alternatif penangkapan "kuat" adalah "lemah" dan penerapannya menyebabkan konsekuensi berikut:

1. Nilai-nilai yang ditangkap "Lemah" tidak dipegang oleh penutupan dan dengan demikian nilai-nilai itu dapat dilepaskan dan ditetapkan ke nol .

2. Sebagai konsekuensi dari paragraf pertama, nilai yang ditangkap dengan "lemah" di Swift selalu opsional .
Kami memodifikasi contoh kami menggunakan tangkapan "lemah" dan segera melihat perbedaannya.

 func sing() -> () -> Void { let taylor = Singer() let singing = { [weak taylor] in taylor?.playSong() return } return singing } 

[lemah taylor] - ini adalah " daftar tangkap " kami, bagian khusus dari sintaksis penutupan di mana kami memberikan instruksi tentang bagaimana nilai harus ditangkap. Di sini kita mengatakan bahwa taylor harus ditangkap "dengan lemah", jadi kita perlu menggunakan taylor? .PlaySong () - sekarang ini opsional , karena dapat disetel ke nol kapan saja.

Jika sekarang Anda menjalankan kode ini, Anda akan melihat bahwa memanggil singFunction () tidak lagi menghasilkan pesan. Alasan untuk ini adalah bahwa taylor hanya ada di dalam sing () , dan penutupan yang dikembalikan oleh fungsi ini tidak menahan taylor "sangat" di dalam dirinya sendiri.

Sekarang coba ubah taylor? .PlaySong () ke taylor! .PlaySong () . Ini akan menyebabkan pembongkaran paksa taylor di dalam penutupan, dan, karenanya, terjadi kesalahan fatal (membongkar isi yang mengandung nihil )

Tangkapan "Tanpa pemilik" (penangkapan tidak dimiliki)


Alternatif penangkapan "lemah" adalah "tanpa pemilik".

 func sing() -> () -> Void { let taylor = Singer() let singing = { [unowned taylor] in taylor.playSong() return } return singing } 

Kode ini akan berakhir secara abnormal dengan cara yang sama dengan opsi opsional yang ditampilkan di atas - taylor yang tidak dikenal mengatakan: "Saya tahu pasti bahwa taylor akan ada selama durasi penutupan, jadi saya tidak perlu menyimpannya di memori." Bahkan, taylor akan segera dirilis dan kode ini akan mogok.

Jadi gunakan yang tidak dimiliki dengan sangat hati-hati.

Masalah umum


Ada empat masalah yang dihadapi pengembang saat menggunakan penangkapan nilai dalam penutupan:

1. Kesulitan dengan lokasi daftar tangkap dalam kasus ketika penutupan mengambil parameter


Ini adalah masalah umum yang mungkin Anda temui pada awal studi penutupan, tetapi, untungnya, Swift akan membantu kami dalam kasus ini.

Saat menggunakan daftar tangkap dan parameter penutupan bersama-sama, daftar tangkap datang dalam tanda kurung siku, lalu parameter penutupan, lalu dalam kata kunci, menandai awal “badan” penutupan.

 writeToLog { [weak self] user, message in self?.addToLog("\(user) triggered event: \(message)") } 

Mencoba memasukkan daftar tangkap setelah parameter penutupan akan menghasilkan kesalahan kompilasi.

2. Munculnya siklus tautan yang kuat, yang mengarah ke kebocoran memori


Ketika entitas A memiliki entitas B, dan sebaliknya, Anda memiliki situasi yang disebut "mempertahankan siklus".

Sebagai contoh, perhatikan kodenya:

 class House { var ownerDetails: (() -> Void)? func printDetails() { print("This is a great house.") } deinit { print("I'm being demolished!") } } 

Kami mendefinisikan kelas House , yang berisi satu properti (penutupan), satu metode, dan de-initializer yang akan menampilkan pesan ketika instance kelas dihancurkan.

Sekarang buat kelas Pemilik mirip dengan yang sebelumnya, kecuali bahwa properti penutupannya berisi informasi tentang rumah.

 class Owner { var houseDetails: (() -> Void)? func printDetails() { print("I own a house.") } deinit { print("I'm dying!") } } 

Sekarang buat instance dari kelas-kelas ini di dalam blok do . Kami tidak membutuhkan blok penangkap, tetapi menggunakan blok do akan menghancurkan instances setelah}

 print("Creating a house and an owner") do { let house = House() let owner = Owner() } print("Done") 

Akibatnya, pesan akan ditampilkan: "Membuat rumah dan pemilik", "Aku sekarat!", "Aku sedang dihancurkan!", Lalu "Selesai" - semuanya berfungsi sebagaimana mestinya.

Sekarang buat satu loop tautan yang kuat.

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = owner.printDetails owner.houseDetails = house.printDetails } print("Done") 

Sekarang pesan "Membuat rumah dan pemilik" akan muncul, lalu "Selesai". Deinitializers tidak akan dipanggil.

Ini terjadi sebagai akibat dari kenyataan bahwa rumah memiliki properti yang menunjuk ke pemilik, dan pemilik memiliki properti yang menunjukkan rumah. Oleh karena itu, tidak ada dari mereka yang dapat dilepaskan dengan aman. Dalam situasi nyata, ini menyebabkan kebocoran memori, yang menyebabkan kinerja buruk dan bahkan crash aplikasi.

Untuk memperbaiki situasi, kita perlu membuat penutupan baru dan menggunakan tangkapan "lemah" dalam satu atau dua kasus, seperti ini:

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = { [weak owner] in owner?.printDetails() } owner.houseDetails = { [weak house] in house?.printDetails() } } print("Done") 

Tidak perlu mendeklarasikan kedua nilai yang ditangkap, itu cukup untuk melakukannya di satu tempat - ini akan memungkinkan Swift untuk menghancurkan kedua kelas jika perlu.

Dalam proyek-proyek nyata, situasi siklus yang sangat jelas dari tautan yang kuat seperti itu jarang muncul, tetapi ini lebih berbicara tentang pentingnya menggunakan tangkapan "lemah" dengan pengembangan yang kompeten.

3. Penggunaan tautan yang kuat secara tidak sengaja, biasanya saat mengambil beberapa nilai


Swift menggunakan pegangan yang kuat secara default, yang dapat menyebabkan perilaku yang tidak terduga.
Pertimbangkan kode berikut:

 func sing() -> () -> Void { let taylor = Singer() let adele = Singer() let singing = { [unowned taylor, adele] in taylor.playSong() adele.playSong() return } return singing } 

Sekarang kami memiliki dua nilai yang ditangkap oleh penutupan, dan kami menggunakan keduanya dengan cara yang sama. Namun, hanya taylor yang ditangkap sebagai tidak dimiliki - adele ditangkap dengan kuat karena kata kunci yang tidak dimiliki harus digunakan untuk setiap nilai yang diambil.

Jika Anda melakukan ini dengan sengaja, maka semuanya baik-baik saja, tetapi jika Anda ingin kedua nilai ditangkap " tidak dimiliki ", Anda perlu yang berikut ini:

 [unowned taylor, unowned adele] 

4. Salin penutupan dan bagikan nilai yang diambil


Kasus terakhir yang ditemukan oleh pengembang adalah bagaimana kesalahan disalin karena data yang mereka ambil tersedia untuk semua salinan kesalahan.
Pertimbangkan contoh penutupan sederhana yang menangkap bilangan variabel integerOfLinesLogged yang dideklarasikan di luar penutupan, sehingga kami dapat meningkatkan nilainya dan mencetaknya setiap kali penutupan dipanggil:

 var numberOfLinesLogged = 0 let logger1 = { numberOfLinesLogged += 1 print("Lines logged: \(numberOfLinesLogged)") } logger1() 

Ini akan menampilkan pesan "Baris yang dicatat: 1".
Sekarang kita akan membuat salinan penutupan yang akan membagikan nilai yang ditangkap bersama dengan penutupan pertama. Jadi, jika kita menyebut penutupan asli atau salinannya, kita akan melihat nilai variabel yang tumbuh.

 let logger2 = logger1 logger2() logger1() logger2() 

Ini akan mencetak pesan “Baris yang dicatat: 1” ... “Baris yang dicatat: 4" karena logger1 dan logger2 menunjuk ke variabel numberOfLinesLogged yang ditangkap.

Kapan menggunakan tangkapan "kuat", "lemah" dan "tidak memiliki pemilik"


Sekarang kita mengerti bagaimana semuanya bekerja, mari kita coba meringkas:

1. Jika Anda yakin bahwa nilai yang ditangkap tidak akan pernah menjadi nol saat melakukan penutupan, Anda dapat menggunakan "pengambilan yang tidak dimiliki" . Ini adalah situasi yang jarang terjadi di mana menggunakan tangkapan "lemah" dapat menyebabkan kesulitan tambahan, bahkan ketika menggunakan pelindung membiarkan nilai yang ditangkap secara lemah di dalam penutupan.

2. Jika Anda memiliki kasus siklus tautan yang kuat (entitas A memiliki entitas B, dan entitas B memiliki entitas A), maka dalam salah satu kasus Anda perlu menggunakan "penangkap lemah" . Adalah perlu untuk mempertimbangkan yang mana dari dua entitas yang akan dibebaskan terlebih dahulu, jadi jika view controller A mewakili view controller B, maka view controller B dapat berisi tautan "lemah" kembali ke "A".

3. Jika kemungkinan siklus tautan kuat dikecualikan, Anda dapat menggunakan tangkapan "kuat" ( " tangkapan kuat" ). Misalnya, menjalankan animasi tidak memblokir diri di dalam penutup yang berisi animasi, sehingga Anda dapat menggunakan ikatan yang kuat.

4. Jika Anda tidak yakin, mulailah dengan ikatan "lemah" dan ubah hanya jika perlu.

Opsional - Panduan Swift Resmi:
Sirkuit pendek
Penghitungan tautan otomatis

Source: https://habr.com/ru/post/id444336/


All Articles