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 ditunjuk
Mengapa semua bidang subkelas harus diisi sebelum memanggil super.init
Cara mengakses semua inisialisasi induk dengan penggantian minimal di subkelas
Kapan tepatnya inisialisasi required
dibutuhkan
Mengapa UIView.init()
selalu dipanggil tanpa parameter, tetapi init(frame:)
dan init(coder:)
ditimpa
...dan masih banyak lagi. Namun mari kita bahas langkah demi langkah.
var
vs let
required
: generik, protokol, Self()
, final
UIView()
tanpa parameterPanduan 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.
Mari kita mulai dengan membahas inisialisasi struktur. Ini cukup sederhana karena tidak ada pewarisan, tetapi masih ada beberapa aturan yang harus Anda ketahui.
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 .
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
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
var
opsional default ke nil
extension
Inisialisasi utama untuk suatu kelas adalah inisialisasi yang ditetapkan . Inisialisasi ini memiliki dua tujuan:
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 { } // ✅
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).
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) // ✅
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.
super.init()
.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.
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.
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() } }
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 } }
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) } }
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) { } // ✅ }
init()
tanpa parameter, ia dipanggil secara otomatis dalam inisialisasi subkelas.required
diperlukan untuk menjamin keberadaannya di subkelas untuk digunakan dalam generik, protokol, dan Self()
. 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()
.
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 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 } }
Kami telah membahas semua aspek penting inisialisasi di Swift:
Dalam inisialisasi, semua bidang harus diisi.
Properti var
opsional default ke nil
.
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()
memanggil UIView.init(frame:)
atau UIView.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 .