Kesalahan Kebanyakan Pengembang Swift Tidak Tahu Mereka Membuatnya
Diterbitkan: 2022-03-11Berasal dari latar belakang Objective-C, pada awalnya, saya merasa Swift menahan saya. Swift tidak mengizinkan saya untuk membuat kemajuan karena sifatnya yang diketik dengan kuat, yang terkadang membuat saya marah.
Tidak seperti Objective-C, Swift memberlakukan banyak persyaratan pada waktu kompilasi. Hal-hal yang santai di Objective-C, seperti tipe id
dan konversi implisit, tidak ada di Swift. Bahkan jika Anda memiliki Int
dan Double
, dan Anda ingin menambahkannya, Anda harus mengonversinya menjadi satu jenis secara eksplisit.
Juga, opsional adalah bagian mendasar dari bahasa, dan meskipun itu adalah konsep yang sederhana, perlu waktu untuk membiasakannya.
Pada awalnya, Anda mungkin ingin memaksa membuka semuanya, tetapi itu pada akhirnya akan menyebabkan crash. Saat Anda berkenalan dengan bahasa tersebut, Anda mulai menyukai bagaimana Anda hampir tidak memiliki kesalahan runtime karena banyak kesalahan tertangkap pada waktu kompilasi.
Sebagian besar pemrogram Swift memiliki pengalaman signifikan sebelumnya dengan Objective-C, yang, antara lain, mungkin mengarahkan mereka untuk menulis kode Swift menggunakan praktik yang sama yang mereka kenal dalam bahasa lain. Dan itu dapat menyebabkan beberapa kesalahan buruk.
Dalam artikel ini, kami menguraikan kesalahan paling umum yang dilakukan pengembang Swift dan cara untuk menghindarinya.
1. Opsi Pembukaan Paksa
Sebuah variabel dari tipe opsional (misalnya String?
) mungkin atau mungkin tidak memiliki nilai. Ketika mereka tidak memiliki nilai, mereka sama dengan nil
. Untuk mendapatkan nilai opsional, pertama-tama Anda harus membuka bungkusnya , dan itu dapat dibuat dengan dua cara berbeda.
Salah satu caranya adalah optional binding menggunakan if let
atau guard let
, yaitu:
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
Kedua adalah memaksa membuka menggunakan !
operator, atau menggunakan tipe opsional yang terbuka secara implisit (mis String!
). Jika opsional nil
, memaksa membuka bungkus akan menyebabkan kesalahan runtime dan menghentikan aplikasi. Lebih lanjut, mencoba mengakses nilai opsional yang tidak dibungkus secara implisit akan menyebabkan hal yang sama.
Kami terkadang memiliki variabel yang tidak dapat (atau tidak ingin) kami inisialisasi di penginisialisasi kelas/struktur. Jadi, kita harus mendeklarasikannya sebagai opsional. Dalam beberapa kasus, kami menganggap mereka tidak akan nil
di bagian tertentu dari kode kami, jadi kami memaksa membukanya atau mendeklarasikannya sebagai opsional yang tidak dibungkus secara implisit karena itu lebih mudah daripada harus melakukan pengikatan opsional setiap saat. Ini harus dilakukan dengan hati-hati.
Ini mirip dengan bekerja dengan IBOutlet
s, yang merupakan variabel yang mereferensikan objek di nib atau storyboard. Mereka tidak akan diinisialisasi pada inisialisasi objek induk (biasanya pengontrol tampilan atau UIView
khusus), tetapi kami dapat yakin mereka tidak akan nil
ketika viewDidLoad
(dalam pengontrol tampilan) atau awakeFromNib
(dalam tampilan) dipanggil, sehingga kami dapat mengaksesnya dengan aman.
Secara umum, praktik terbaik adalah menghindari pemaksaan membuka bungkus dan menggunakan opsional yang tidak dibungkus secara implisit. Selalu pertimbangkan opsional bisa nil
dan tangani dengan tepat baik menggunakan pengikatan opsional, atau periksa apakah itu bukan nil
sebelum memaksa membuka bungkus, atau mengakses variabel jika ada opsi yang tidak terbungkus secara implisit.
2. Tidak Mengetahui Jebakan dari Siklus Referensi yang Kuat
Siklus referensi yang kuat ada ketika sepasang objek menyimpan referensi yang kuat satu sama lain. Ini bukan sesuatu yang baru bagi Swift, karena Objective-C memiliki masalah yang sama, dan pengembang Objective-C yang berpengalaman diharapkan untuk mengelola ini dengan benar. Penting untuk memperhatikan referensi kuat dan referensi apa. Dokumentasi Swift memiliki bagian yang didedikasikan untuk topik ini.
Sangat penting untuk mengelola referensi Anda saat menggunakan penutupan. Secara default, penutupan (atau blok), menyimpan referensi yang kuat untuk setiap objek yang direferensikan di dalamnya. Jika salah satu dari objek ini memiliki referensi yang kuat untuk penutupan itu sendiri, kami memiliki siklus referensi yang kuat. Penting untuk menggunakan daftar pengambilan untuk mengelola dengan benar bagaimana referensi Anda diambil.
Jika ada kemungkinan bahwa instance yang ditangkap oleh blok akan dibatalkan alokasinya sebelum blok dipanggil, Anda harus menangkapnya sebagai referensi yang lemah , yang akan opsional karena bisa nil
. Sekarang, jika Anda yakin instance yang diambil tidak akan dibatalkan alokasinya selama masa pemblokiran, Anda dapat menangkapnya sebagai referensi yang tidak dimiliki . Keuntungan menggunakan yang unowned
daripada yang weak
adalah bahwa referensi tidak akan menjadi opsional dan Anda dapat menggunakan nilainya secara langsung tanpa perlu membukanya.
Dalam contoh berikut, yang dapat Anda jalankan di Xcode Playground, kelas Container
memiliki larik dan penutup opsional yang dipanggil setiap kali lariknya berubah (menggunakan pengamat properti untuk melakukannya). Kelas Whatever
memiliki instance Container
, dan dalam penginisialisasinya, ia menetapkan penutupan ke arrayDidChange
dan penutupan ini merujuk self
, sehingga menciptakan hubungan yang kuat antara instance Whatever
dan penutupan.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
Jika Anda menjalankan contoh ini, Anda akan melihat bahwa deinit whatever
tidak pernah dicetak, yang berarti instance kita w
tidak terdeallocated dari memori. Untuk memperbaikinya, kita harus menggunakan daftar tangkapan untuk tidak menangkap self
dengan kuat:
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
Dalam hal ini, kita dapat menggunakan unowned
, karena self
tidak akan pernah nil
selama masa penutupan.
Ini adalah praktik yang baik untuk hampir selalu menggunakan daftar tangkapan untuk menghindari siklus referensi, yang akan mengurangi kebocoran memori, dan pada akhirnya kode yang lebih aman.
3. Menggunakan self
di mana-mana
Tidak seperti di Objective-C, dengan Swift, kita tidak diharuskan menggunakan self
untuk mengakses properti kelas atau struct di dalam suatu metode. Kita hanya diharuskan melakukannya secara tertutup karena perlu menangkap self
. Menggunakan self
di tempat yang tidak diperlukan bukanlah kesalahan, itu berfungsi dengan baik, dan tidak akan ada kesalahan dan tidak ada peringatan. Namun, mengapa menulis lebih banyak kode daripada yang seharusnya? Juga, penting untuk menjaga kode Anda tetap konsisten.
4. Tidak Mengetahui Tipe Tipe Anda
Swift menggunakan tipe nilai dan tipe referensi . Selain itu, contoh tipe nilai menunjukkan perilaku yang sedikit berbeda dari contoh tipe referensi. Tidak mengetahui kategori apa yang cocok untuk setiap instance Anda akan menyebabkan harapan yang salah pada perilaku kode.
Dalam sebagian besar bahasa berorientasi objek, saat kita membuat instance kelas dan menyebarkannya ke instance lain dan sebagai argumen ke metode, kita berharap instance ini sama di mana-mana. Itu berarti perubahan apa pun akan tercermin di mana-mana, karena sebenarnya, yang kami miliki hanyalah sekumpulan referensi ke data yang sama persis. Objek yang menunjukkan perilaku ini adalah tipe referensi, dan di Swift, semua tipe yang dideklarasikan sebagai class
adalah tipe referensi.
Selanjutnya, kita memiliki tipe nilai yang dideklarasikan menggunakan struct
atau enum
. Tipe nilai disalin saat ditetapkan ke variabel atau diteruskan sebagai argumen ke fungsi atau metode. Jika Anda mengubah sesuatu dalam contoh yang disalin, yang asli tidak akan diubah. Jenis nilai tidak dapat diubah . Jika Anda menetapkan nilai baru ke properti instance dari tipe nilai, seperti CGPoint
atau CGSize
, instance baru dibuat dengan perubahan. Itu sebabnya kita bisa menggunakan pengamat properti pada larik (seperti pada contoh di atas di kelas Container
) untuk memberi tahu kita tentang perubahan. Apa yang sebenarnya terjadi, adalah bahwa array baru dibuat dengan perubahan; itu ditugaskan ke properti, dan kemudian didSet
dipanggil.

Jadi, jika Anda tidak tahu objek yang Anda hadapi adalah referensi atau tipe nilai, harapan Anda tentang apa yang akan dilakukan kode Anda, mungkin sepenuhnya salah.
5. Tidak Menggunakan Potensi Penuh Enum
Ketika kita berbicara tentang enum, kita biasanya memikirkan C enum dasar, yang hanya merupakan daftar konstanta terkait yang merupakan bilangan bulat di bawahnya. Di Swift, enum jauh lebih kuat. Misalnya, Anda dapat melampirkan nilai ke setiap kasus enumerasi. Enum juga memiliki metode dan properti read-only/computed yang dapat digunakan untuk memperkaya setiap kasus dengan lebih banyak informasi dan detail.
Dokumentasi resmi tentang enum sangat intuitif, dan dokumentasi penanganan kesalahan menyajikan beberapa kasus penggunaan untuk kekuatan ekstra enum di Swift. Juga, periksa mengikuti eksplorasi ekstensif enum di Swift untuk mempelajari hampir semua hal yang dapat Anda lakukan dengan mereka.
6. Tidak Menggunakan Fitur Fungsional
Pustaka Standar Swift menyediakan banyak metode yang mendasar dalam pemrograman fungsional dan memungkinkan kita melakukan banyak hal hanya dengan satu baris kode, seperti memetakan, mengurangi, dan memfilter, antara lain.
Mari kita periksa beberapa contoh.
Katakanlah, Anda harus menghitung ketinggian tampilan tabel. Mengingat Anda memiliki subkelas UITableViewCell
seperti berikut:
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
Pertimbangkan, kami memiliki sebuah array dari contoh model modelArray
; kita dapat menghitung ketinggian tampilan tabel dengan satu baris kode:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
map
akan menampilkan array CGFloat
, yang berisi tinggi setiap sel, dan reduce
akan menambahkannya.
Jika Anda ingin menghapus elemen dari array, Anda mungkin akan melakukan hal berikut:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
Contoh ini tidak terlihat elegan, juga tidak terlalu efisien karena kita memanggil indexOf
untuk setiap item. Perhatikan contoh berikut:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
Sekarang, kodenya lebih efisien, tetapi dapat ditingkatkan lebih lanjut dengan menggunakan filter
:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
Contoh berikutnya mengilustrasikan bagaimana Anda dapat menghapus semua subview dari UIView
yang memenuhi kriteria tertentu, seperti bingkai yang memotong persegi panjang tertentu. Anda dapat menggunakan sesuatu seperti:
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
Namun, kita harus berhati-hati, karena Anda mungkin tergoda untuk menyambungkan beberapa panggilan ke metode ini untuk membuat pemfilteran dan transformasi yang bagus, yang mungkin berakhir dengan satu baris kode spageti yang tidak terbaca.
7. Tetap Berada di Zona Nyaman dan Tidak Mencoba Pemrograman Berorientasi Protokol
Swift diklaim sebagai bahasa pemrograman berorientasi protokol pertama, seperti yang disebutkan dalam Pemrograman Berorientasi Protokol WWDC di sesi Swift. Pada dasarnya, itu berarti kita dapat memodelkan program kita di sekitar protokol dan menambahkan perilaku ke tipe hanya dengan menyesuaikan diri dengan protokol dan memperluasnya. Misalnya, jika kita memiliki protokol Shape
, kita dapat memperluas CollectionType
(yang disesuaikan dengan tipe seperti Array
, Set
, Dictionary
), dan menambahkan metode ke dalamnya yang menghitung total area yang dihitung untuk persimpangan.
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
Pernyataan where Generator.Element: Shape
adalah batasan yang menyatakan metode dalam ekstensi hanya akan tersedia dalam contoh tipe yang sesuai dengan CollectionType
, yang berisi elemen tipe yang sesuai dengan Shape
. Misalnya, metode ini dapat dipanggil pada instance Array<Shape>
, tetapi tidak pada instance Array<String>
. Jika kita memiliki kelas Polygon
yang sesuai dengan protokol Shape
, maka metode tersebut akan tersedia untuk instance Array<Polygon>
juga.
Dengan ekstensi protokol, Anda dapat memberikan implementasi default ke metode yang dideklarasikan dalam protokol, yang kemudian akan tersedia di semua tipe yang sesuai dengan protokol itu tanpa harus membuat perubahan apa pun pada tipe tersebut (kelas, struct, atau enum). Ini dilakukan secara ekstensif di seluruh Pustaka Standar Swift, misalnya, map
dan reduce
didefinisikan dalam ekstensi CollectionType
, dan implementasi yang sama ini dibagikan oleh tipe seperti Array
dan Dictionary
tanpa kode tambahan apa pun.
Perilaku ini mirip dengan mixin dari bahasa lain, seperti Ruby atau Python. Dengan hanya menyesuaikan diri dengan protokol dengan implementasi metode default, Anda menambahkan fungsionalitas ke tipe Anda.
Pemrograman berorientasi protokol mungkin terlihat cukup canggung dan tidak terlalu berguna pada pandangan pertama, yang dapat membuat Anda mengabaikannya dan bahkan tidak mencobanya. Posting ini memberikan pemahaman yang baik tentang penggunaan pemrograman berorientasi protokol dalam aplikasi nyata.
Seperti yang Kita Pelajari, Swift Bukanlah Bahasa Mainan
Swift awalnya diterima dengan banyak skeptisisme; orang tampaknya berpikir bahwa Apple akan mengganti Objective-C dengan bahasa mainan untuk anak-anak atau dengan sesuatu untuk non-programmer. Namun, Swift telah terbukti menjadi bahasa yang serius dan kuat yang membuat pemrograman menjadi sangat menyenangkan. Karena diketik dengan kuat, sulit untuk membuat kesalahan, dan karena itu, sulit untuk membuat daftar kesalahan yang dapat Anda buat dengan bahasa tersebut.
Ketika Anda terbiasa dengan Swift dan kembali ke Objective-C, Anda akan melihat perbedaannya. Anda akan kehilangan fitur bagus yang ditawarkan Swift dan harus menulis kode yang membosankan di Objective-C untuk mencapai efek yang sama. Di lain waktu, Anda akan menghadapi kesalahan runtime yang akan ditangkap Swift selama kompilasi. Ini adalah peningkatan yang bagus untuk pemrogram Apple, dan masih banyak lagi yang akan datang seiring bahasanya matang.