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 pendekPenghitungan tautan otomatis