Perkenalan
Halo! Nama saya Kiryl Famin, dan saya seorang pengembang iOS.
Hari ini, saya ingin membahas secara mendalam topik sederhana seperti inisialisasi di Swift. Meskipun tampak sederhana, terkadang kurangnya pemahaman menyeluruh tentang topik ini menyebabkan kesalahan yang membuat frustrasi sehingga seseorang ingin segera memperbaikinya tanpa mempelajari detailnya.
Dalam artikel ini, kami akan membahas segala hal yang berhubungan dengan inisialisasi, termasuk:
Cara mempertahankan inisialisasi anggota struktur saat mendefinisikan yang khusus
Mengapa tidak selalu perlu menulis inisialisasi di kelas
Mengapa pemanggilan
super.init
tidak selalu diperlukan dalam inisialisasi yang ditunjukMengapa semua bidang subkelas harus diisi sebelum memanggil
super.init
Cara mengakses semua inisialisasi induk dengan penggantian minimal di subkelas
Kapan tepatnya inisialisasi
required
dibutuhkanMengapa
UIView.init()
selalu dipanggil tanpa parameter, tetapiinit(frame:)
daninit(coder:)
ditimpa
...dan masih banyak lagi. Namun mari kita bahas langkah demi langkah.
Daftar isi
Dasar-dasar
Struktur
- Inisialisasi berdasarkan anggota
- Opsional
-
var
vslet
- Mempertahankan inisialisasi anggota
Kelas
- Inisialisasi yang ditunjuk
- Inisialisasi kenyamanan
- Mempertahankan inisialisasi kenyamanan superkelas
- Meminimalkan jumlah penggantian
- Bantuan penyusun
- inisialisasi
required
: generik, protokol,Self()
,final
-
UIView()
tanpa parameter
Penghargaan terhormat
- Inisiasi yang gagal
- Enumerasi
Ringkasan
Tautan relevan
Dasar-dasar
Panduan Apple Bahasa Pemrograman Swift (6) (yang, omong-omong, sangat rinci untuk inisialisasi) menyatakan:
Inisialisasi adalah proses menyiapkan contoh kelas, struktur, atau enumerasi untuk digunakan. Proses ini melibatkan pengaturan nilai awal untuk setiap properti yang tersimpan pada contoh tersebut dan melakukan pengaturan atau inisialisasi lain yang diperlukan sebelum contoh baru siap digunakan.
Anda menerapkan proses inisialisasi ini dengan mendefinisikan inisialisasi , yang seperti metode khusus yang dapat dipanggil untuk membuat contoh baru dari tipe tertentu. Tidak seperti inisialisasi Objective-C, inisialisasi Swift tidak mengembalikan nilai. Peran utamanya adalah untuk memastikan bahwa contoh baru dari suatu tipe diinisialisasi dengan benar sebelum digunakan untuk pertama kalinya.
Baiklah, saya rasa saya tidak perlu menambahkan apa pun di sini.
Struktur
Mari kita mulai dengan membahas inisialisasi struktur. Ini cukup sederhana karena tidak ada pewarisan, tetapi masih ada beberapa aturan yang harus Anda ketahui.
Inisialisasi berdasarkan anggota
Mari kita tulis struktur sederhana:
struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)
Perhatikan bahwa kita dapat menginisialisasi struktur tanpa mendeklarasikan inisialisasi secara eksplisit. Hal ini terjadi karena struktur menerima inisialisasi berdasarkan anggota yang dihasilkan oleh kompiler. Ini hanya berfungsi untuk struktur .
Dengan memilih Refactor → Generate memberwise initializer , Anda dapat melihat tampilannya:
init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }
Dari tanda tangannya, mudah untuk melihat bahwa kegagalan dalam memberikan nilai untuk semua parameter akan mengakibatkan kesalahan kompilasi:
let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call
Namun, jika Anda ingin mengurangi jumlah argumen yang diperlukan, Anda dapat menentukan inisialisasi khusus:
init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅
Perhatikan bahwa jika isBlocked
tidak diisi, ini akan mengakibatkan kesalahan kompilasi karena semua properti struktur harus diisi dalam inisialisasi .
Opsional, var
vs let
Satu-satunya kasus di mana suatu kolom tidak perlu diisi secara eksplisit adalah ketika kolom tersebut merupakan variabel opsional ( ?
) ( var
). Dalam kasus seperti itu, kolom tersebut akan secara default bernilai nil
:
struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅
Namun, jika kita mencoba menggunakan inisialisasi memberwise dalam kasus ini, kita akan mendapatkan kesalahan kompilasi:
let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call
Mempertahankan inisialisasi anggota
Hal ini terjadi karena mendeklarasikan inisialisasi kustom akan menghapus inisialisasi berdasarkan anggota. Hal ini masih memungkinkan untuk mendefinisikannya secara eksplisit, tetapi tidak akan tersedia secara otomatis.
Namun, ada sedikit trik untuk mempertahankan inisialisasi anggota: nyatakan inisialisasi kustom dalam extension
.
struct BankAccount { let amount: Double var isBlocked: Bool? } extension BankAccount { init(amount: Double) { self.amount = amount } } let barclaysBankAccount = BankAccount(amount: 735) // ✅ let revolutBankAccount = BankAccount(amount: 812, isBlocked: false) // ✅ print(barclaysBankAccount.isBlocked) // nil print(barclaysBankAccount.isBlocked) // false
Ringkasan untuk struktur
- Semua bidang harus diisi dalam inisialisasi
- Bidang
var
opsional default kenil
- Struktur menerima inisialisasi anggota secara gratis
- Inisialisasi anggota akan hilang jika inisialisasi kustom dideklarasikan
- Untuk mempertahankan inisialisasi anggota struktur, tentukan yang khusus dalam
extension
Kelas
Inisialisasi yang ditunjuk
Inisialisasi utama untuk suatu kelas adalah inisialisasi yang ditetapkan . Inisialisasi ini memiliki dua tujuan:
- Memastikan semua bidang terisi
- Jika kelas diwarisi, ia memanggil inisialisasi superkelas
class Animal { var name: String init(name: String) { self.name = name } } class Dog: Animal { var breed: String var name: String init(breed: String, name: String) { self.breed = breed super.init(name: name) } }
Semua kolom harus diisi sebelum memanggil super.init
. Hal ini karena inisialisasi superkelas dapat memanggil metode yang digantikan oleh subkelas, yang dapat mengakses properti subkelas yang belum diisi.
class Animal { var age: Int init(age: Int) { self.age = age getInfo() } func getInfo() { print("Age: ", age) } } class Dog: Animal { var breed: String init(breed: String, age: Int) { self.breed = breed // imagine we haven't done this super.init(age: age) } override func getInfo() { print("Age: ", age, ", breed: ", breed) } }
Jadi, jika kita tidak menetapkan self.breed = breed
, kita akan mengalami kesalahan runtime karena inisialisasi Animal
akan memanggil metode getInfo()
yang diganti dari kelas Dog
. Metode ini mencoba mengakses properti breed
, yang belum diisi.
Tidak seperti struktur, kelas tidak menerima inisialisasi anggota secara implisit. Jika ada properti yang tidak diinisialisasi, kesalahan kompilasi akan terjadi:
class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
class Animal { // ✅ var age: Int = 0 }
class Animal { // ✅ var age: Int? }
class Animal { } // ✅
Inisialisasi kenyamanan
Kelas juga dapat memiliki inisialisasi praktis . Tidak seperti inisialisasi yang ditetapkan, kelas tidak membuat objek dari awal, tetapi menyederhanakan proses inisialisasi dengan menggunakan kembali logika inisialisasi lainnya.
class Rectangle { var width: Double var height: Double init(width: Double, height: Double) { self.width = width self.height = height } convenience init(side: Double) { self.init(width: side, height: side) // uses a designated initializer of self } }
Inisialisasi praktis dapat memanggil inisialisasi yang ditunjuk atau inisialisasi praktis lainnya. Pada akhirnya, inisialisasi yang ditunjuk akan selalu dipanggil.
Inisialisasi praktis selalu berada pada posisi horizontal (self.init), dan inisialisasi yang ditunjuk berada pada posisi vertikal (super.init).
Mempertahankan Inisialisasi Kenyamanan Superclass
Begitu subkelas mendeklarasikan properti baru, ia kehilangan akses ke semua inisialisasi praktis superkelas.
class Animal { var age: Int var name: String init(age: Int, name: String) { self.age = age self.name = name } convenience init(age: Int) { self.init(age: age, name: "Default") } convenience init(name: String) { self.init(age: 0, name: name) } } class Dog: Animal { var breed: String init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } let dog = Dog(age: 3) // ❌ Missing arguments for parameters 'breed', 'name' in call
Hal ini dapat diperbaiki dengan mengesampingkan semua inisialisasi yang ditetapkan pada superkelas .
class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅
Mudah dilihat bahwa, dengan cara ini, untuk menggunakan inisialisasi praktis di subkelas berikutnya, Anda perlu mengganti dua inisialisasi.
class GuideDog: Dog { var isTrained: Bool override init(age: Int, name: String) { self.isTrained = false super.init(age: age, name: name) } override init(age: Int, name: String, breed: String) { self.isTrained = false super.init(age: age, name: name, breed: breed) } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅
Meminimalkan jumlah penggantian
Namun, hal ini dapat dihindari dengan menggunakan inisialisasi pengesampingan praktis .
class Dog: Animal { var breed: String convenience override init(age: Int, name: String) { self.init(age: age, name: name, breed: "Mixed") // self, not super } init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } class GuideDog: Dog { var isTrained: Bool // override init(age: Int, name: String) { // self.isTrained = false // // super.init(age: age, name: name, breed: "Mixed") // } convenience override init(age: Int, name: String, breed: String) { self.init(age: age, name: name, breed: breed, isTrained: false) // self, not super } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅
Sekarang kita hanya memiliki 2 inisialisasi yang ditetapkan secara eksplisit dalam setiap subkelas.
Perhatikan bagaimana inisialisasi pengesampingan praktis memanggil init yang ditunjuk self
, bukannya super.init
.
Trik ini dijelaskan secara menyeluruh dalam Bab 5 dari Swift in Depth oleh Tjeerd in 't Veen, buku yang sangat saya rekomendasikan.
Ringkasan menengah
- Inisialisasi yang ditunjuk memastikan semua properti terisi dan memanggil
super.init()
. - Inisialisasi praktis menyederhanakan inisialisasi dengan memanggil inisialisasi yang ditunjuk.
- Inisialisasi praktis tidak lagi tersedia bagi subkelas jika mereka mendeklarasikan properti baru.
- Untuk mengembalikan inisialisasi praktis superkelas, semua inisialisasi yang ditunjuk harus ditimpa.
- Untuk meminimalisir jumlah penggantian, inisialisasi penggantian praktis dapat digunakan.
Bantuan penyusun
Kita telah membahas bahwa jika sebuah subkelas tidak memperkenalkan parameter baru, maka subkelas tersebut secara otomatis mewarisi semua inisialisasi superkelas.
class Base { let value: Int init() { value = 0 } init(value: Int) { self.value = value } } class Subclass: Base { } let subclass = Subclass() // ✅ let subclass = Subclass(value: 3) // ✅
Namun, ada poin penting lainnya: jika superkelas hanya memiliki satu inisialisasi yang ditetapkan dan tidak memiliki parameter ( init()
tanpa argumen), maka inisialisasi yang dideklarasikan secara eksplisit di subkelas tidak perlu memanggil super.init()
. Dalam kasus ini, kompiler Swift secara otomatis memasukkan panggilan ke super.init()
yang tersedia tanpa argumen.
class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }
Kode dikompilasi karena super.init()
dipanggil secara implisit. Hal ini penting untuk beberapa contoh berikut.
Diperlukan
Inisialisasi required
digunakan dalam semua kasus di mana subkelas harus memiliki inisialisasi yang sama dengan kelas dasar. Subkelas juga harus memanggil super.init()
. Berikut adalah contoh di mana inisialisasi required
diperlukan.
Obat Generik
Memanggil init
pada tipe generik hanya dimungkinkan dengan mendeklarasikannya sebagai required init
.
class Base { } class Subclass: Base { } struct Factory<T: Base> { func initInstance() -> T { // ❌ Constructing an object of class T() // type 'T' with a metatype value } // must use a 'required' initializer }
Kode ini tidak dapat dikompilasi karena Factory
tidak mengetahui apa pun tentang subkelas Base
. Meskipun dalam kasus khusus ini, Subclass
memiliki init()
tanpa parameter, bayangkan jika ia memperkenalkan kolom baru:
class Subclass: Base { let value: Int init(value: Int) { self.value = value } }
Di sini, tidak lagi memiliki init
yang kosong, jadi harus dideklarasikan sebagai required
.
class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()
Perhatikan bahwa meskipun kami tidak secara eksplisit mendeklarasikan required init
di Subclass
, kompiler membuatnya untuk kami. Hal ini dibahas dalam Compiler Assistance . required init
secara otomatis diwarisi dan disebut super.init()
.
class Subclass: Base { required init() { super.init() } }
Protokol
Semua inisialisasi yang dideklarasikan dalam protokol harus required
:
protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }
Sekali lagi, hal ini diperlukan agar kompiler memastikan bahwa subkelas mengimplementasikan inisialisasi protokol. Seperti yang telah kita ketahui, hal ini tidak selalu terjadi—jika init
tidak required
, subkelas tidak berkewajiban untuk menggantinya dan dapat menentukan inisialisasinya sendiri.
class IntValue: InitableObject { let value: Int init(value: Int) { self.value = value } } let InitableType: Initable.Type = IntValue.self let initable: Initable = InitableType.init()
Tentu saja, kode berikut tidak akan dikompilasi karena Base.init()
tidak required
.
class InitableObject: Initable { required init() { } // ✅ } class IntValue: InitableObject { let value: Int required init() { self.value = 0 } init(value: Int) { self.value = value } }
Diri sendiri()
Situasi serupa terjadi saat memanggil inisialisasi Self()
dalam metode statis.
class Base { let value: Int init(value: Int) { self.value = value } static func instantiate() -> Self { Self(value: 3) // ❌ Constructing an object of class type 'Self' } // with a metatype value must use a 'required' initializer }
Seperti biasa, masalahnya terletak pada pewarisan:
class Subclass: BaseClass { let anotherValue: Int init(anotherValue: Int) { self.anotherValue = anotherValue } } let subclass = Subclass.instantiate() // ❌
class BaseClass { let value: Int required init(value: Int) { // ✅ self.value = value } static func instantiate() -> Self { Self(value: 3) } }
Menyingkirkan required
: final
Karena tujuan dari required
adalah untuk memaksakan penerapan inisialisasi pada subkelas, tentu saja, pelarangan pewarisan menggunakan kata kunci final
menghilangkan keperluan untuk menandai inisialisasi sebagai required
.
protocol Initable { init() } final class InitableObject: Initable { } // ✅
protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }
Ringkasan menengah
- Jika suatu subkelas tidak memperkenalkan parameter baru, maka subkelas tersebut secara otomatis mewarisi semua inisialisasi dari superkelasnya.
- Jika superkelas hanya memiliki
init()
tanpa parameter, ia dipanggil secara otomatis dalam inisialisasi subkelas. - Inisialisasi
required
diperlukan untuk menjamin keberadaannya di subkelas untuk digunakan dalam generik, protokol, danSelf()
.
Tampilan UI()
Penyebutan singkat tentang inisialisasi UIView()
tanpa parameter, yang tidak dapat ditemukan dalam dokumentasi tetapi secara misterius digunakan di mana-mana.
Alasannya adalah karena UIView
mewarisi dari NSObject
, yang memiliki init()
tanpa parameter. Oleh karena itu , inisialisasi ini tidak dideklarasikan secara eksplisit dalam antarmuka UIView
, namun masih tersedia:
@available(iOS 2.0, *) @MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate { open class var layerClass: AnyClass { get } public init(frame: CGRect) public init?(coder: NSCoder) open var isUserInteractionEnabled: Bool // no init()
Namun, di balik layar, penginisialisasi ini memanggil init(frame:)
saat diinisialisasi dalam kode atau init(coder:)
saat diinisialisasi melalui Interface Builder. Hal ini terjadi karena UIView
menyediakan implementasinya sendiri dari NSObject.init()
, yang dapat dikonfirmasi oleh fakta bahwa method_getImplementation
mengembalikan alamat yang berbeda untuk NSObject.init()
dan UIView.init()
.
Penghargaan terhormat
Inisiasi yang dapat gagal
Inisiatif yang dapat gagal hanyalah yang mengembalikan opsi
final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }
Enum
Enum dengan nilai mentah mendapatkan init?(rawValue:)
gratis
enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")
Anda juga dapat membuat init khusus untuk enum. Semua init enum harus menetapkan self
.
enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }
Ringkasan akhir
Kami telah membahas semua aspek penting inisialisasi di Swift:
Dalam inisialisasi, semua bidang harus diisi.
Properti
var
opsional default kenil
.Struktur menerima inisialisasi anggota secara gratis.
Inisialisasi anggota akan hilang ketika inisialisasi kustom ditetapkan.
Inisialisasi yang ditunjuk memastikan semua bidang terisi dan memanggil
super.init()
.Inisialisasi praktis menyederhanakan inisialisasi dengan memanggil inisialisasi yang ditunjuk.
Inisialisasi praktis selalu berada pada posisi horizontal (
self.init
), dan inisialisasi yang ditunjuk berada pada posisi vertikal (super.init
).Inisialisasi praktis menjadi tidak tersedia bagi subkelas jika mereka mendeklarasikan properti baru.
Untuk mengembalikan inisialisasi praktis superkelas, semua inisialisasi yang ditunjuk harus ditimpa.
Untuk meminimalisir jumlah penggantian, inisialisasi penggantian praktis dapat digunakan.
Jika suatu subkelas tidak memperkenalkan parameter baru, maka subkelas tersebut secara otomatis mewarisi semua inisialisasi dari superkelasnya.
Jika superkelas hanya memiliki
init()
tanpa parameter, ia secara otomatis dipanggil dalam inisialisasi subkelas.Inisialisasi yang diperlukan memastikan keberadaannya di subkelas untuk digunakan dalam generik, protokol, dan
Self()
.UIView.init()
memanggilUIView.init(frame:)
atauUIView.init(coder:)
.Inisialisasi yang gagal mengembalikan suatu opsional.
Enum dengan nilai mentah mendapatkan
init?(rawValue:)
gratis.Semua inisialisasi enum harus menetapkan
self
.
Saya harap Anda menemukan sesuatu yang bermanfaat dalam artikel ini. Jika ada yang masih belum jelas atau Anda menemukan ketidakakuratan, jangan ragu untuk menghubungi saya untuk mendapatkan penjelasan gratis di Telegram: @kfamyn .
Tautan relevan
- Bahasa Pemrograman Swift (6) / Inisialisasi
- Swift in Depth oleh Tjeerd di 't Veen
- Telegram - @kfamyn