こんにちは!私の名前はKiryl Faminです。iOS開発者です。
今日は、Swift の初期化子のような単純なトピックを徹底的に調べたいと思います。一見単純なトピックですが、このトピックを完全に理解していないと、詳細を掘り下げることなくすぐに修正したいイライラするエラーが発生することがあります。
この記事では、初期化子に関連する以下の内容をすべて説明します。
構造体のメンバ単位の初期化子を保持しながらカスタム初期化子を定義する方法
クラスに初期化子を記述する必要が必ずしもない理由
指定された初期化子でsuper.init
を呼び出すことが必ずしも必要なわけではない理由
super.init
を呼び出す前にサブクラスのすべてのフィールドに値を設定する必要がある理由
サブクラスで最小限のオーバーライドですべての親初期化子にアクセスする方法
まさにrequired
初期化子が必要な場合
UIView.init()
は常にパラメータなしで呼び出されますが、 init(frame:)
とinit(coder:)
はオーバーライドされるのはなぜですか?
...などなど。でも、一歩ずつ進めていきましょう。
var
とlet
required
初期化子: ジェネリック、プロトコル、 Self()
、 final
UIView()
Apple のガイド「Swift プログラミング言語」(6) (ちなみに、初期化子については驚くほど詳細に説明されています) には次のように書かれています。
初期化は、クラス、構造体、または列挙体のインスタンスを使用できるように準備するプロセスです。このプロセスには、そのインスタンスに格納されている各プロパティの初期値の設定と、新しいインスタンスが使用可能になる前に必要なその他のセットアップや初期化の実行が含まれます。
この初期化プロセスを実装するには、イニシャライザを定義します。イニシャライザは、特定の型の新しいインスタンスを作成するために呼び出すことができる特別なメソッドのようなものです。Objective-C のイニシャライザとは異なり、Swift のイニシャライザは値を返しません。その主な役割は、型の新しいインスタンスが初めて使用される前に正しく初期化されるようにすることです。
まあ、ここに何か付け加える必要はないと思います。
まず、構造体の初期化子について説明します。継承がないので非常に簡単ですが、知っておく必要のあるルールがいくつかあります。
簡単な構造を書いてみましょう:
struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)
明示的に初期化子を宣言しなくても構造体を初期化できることに注目してください。これは、構造体がコンパイラによって生成されたメンバー単位の初期化子を受け取るために発生します。これは構造体に対してのみ機能します。
「リファクタリング」→「メンバーワイズ初期化子を生成」を選択すると、どのように見えるか確認できます。
init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }
署名から、すべてのパラメータに値を指定しないとコンパイル エラーが発生することが簡単にわかります。
let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call
ただし、必要な引数の数を減らしたい場合は、カスタム初期化子を定義できます。
init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅
isBlocked
が設定されていない場合、すべての構造体プロパティを初期化子で設定する必要があるため、コンパイル エラーが発生することに注意してください。
var
とlet
フィールドに明示的に値を入力する必要がない唯一のケースは、フィールドがオプション( ?
)変数( var
) である場合です。このような場合、フィールドはデフォルトでnil
になります。
struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅
ただし、この場合にメンバーワイズ初期化子を使用しようとすると、コンパイル エラーが発生します。
let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call
これは、カスタム初期化子を宣言すると、メンバー単位の初期化子が削除されるため発生します。明示的に定義することは可能ですが、自動的には利用できなくなります。
ただし、メンバーごとの初期化子を保持するための小さなトリックがあります。 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
構造の概要
var
フィールドはデフォルトでnil
になりますextension
でカスタム初期化子を定義します。クラスのプライマリ初期化子は、指定初期化子です。これは、次の 2 つの目的を果たします。
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) } }
super.init
呼び出す前に、すべてのフィールドに値を設定する必要があります。これは、スーパークラスの初期化子がサブクラスによってオーバーライドされたメソッドを呼び出し、値が未設定のサブクラスのプロパティにアクセスする可能性があるためです。
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) } }
したがって、 self.breed = breed
を設定していなかった場合、 Animal
初期化子がDog
クラスからオーバーライドされたgetInfo()
メソッドを呼び出すため、実行時エラーが発生します。このメソッドは、まだ設定されていないbreed
プロパティにアクセスしようとします。
構造体とは異なり、クラスは暗黙的なメンバー初期化子を受け取りません。初期化されていないプロパティがある場合、コンパイル エラーが発生します。
class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
class Animal { // ✅ var age: Int = 0 }
class Animal { // ✅ var age: Int? }
class Animal { } // ✅
クラスには、便利な初期化子も存在します。指定された初期化子とは異なり、これらはオブジェクトを最初から作成するのではなく、他の初期化子のロジックを再利用することで初期化プロセスを簡素化します。
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 } }
コンビニエンス初期化子は、指定初期化子または他のコンビニエンス初期化子のいずれかを呼び出すことができます。最終的には、指定初期化子が常に呼び出されます。
便利な初期化子は常に水平方向 (self.init) に配置され、指定された初期化子は垂直方向 (super.init) に配置されます。
サブクラスが新しいプロパティを宣言するとすぐに、スーパークラスのすべての便利な初期化子にアクセスできなくなります。
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
これは、スーパークラスの指定された初期化子をすべてオーバーライドすることで修正できます。
class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅
このように、次のサブクラスで便利な初期化子を使用するには、 2 つの初期化子をオーバーライドする必要があることが簡単にわかります。
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) // ✅
ただし、便利なオーバーライド初期化子を使用することでこれを回避できます。
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) // ✅
これで、各サブクラスには明示的に指定された初期化子が 2 つだけになりました。
便利なオーバーライド初期化子がsuper.init
ではなく、 self
指定された init を呼び出すことに注意してください。
このトリックは、Tjeerd in 't Veen 著の『 Swift in Depth』の第 5 章で詳しく説明されており、強くお勧めします。
super.init()
を呼び出します。サブクラスが新しいパラメータを導入しない場合、スーパークラスの初期化子のすべてが自動的に継承されることについてはすでに説明しました。
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) // ✅
ただし、もう 1 つ重要な点があります。スーパークラスに指定されたイニシャライザが 1 つだけあり、それがパラメーターなし (引数のないinit()
) である場合、サブクラスで明示的に宣言されたイニシャライザはsuper.init()
を呼び出す必要はありません。この場合、Swift コンパイラは、引数なしで利用可能なsuper.init()
への呼び出しを自動的に挿入します。
class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }
super.init()
が暗黙的に呼び出されるため、コードはコンパイルされます。これは、次の例のいくつかにとって重要です。
required
初期化子は、サブクラスが基本クラスと同じ初期化子を持つ必要があるすべての場合に使用されます。また、 super.init()
を呼び出す必要があります。以下は、 required
初期化子が必要な例です。
ジェネリック型でinit
を呼び出すには、それを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 }
このコードは、 Factory
Base
のサブクラスについて何も知らないため、コンパイルされません。この特定のケースでは、 Subclass
にはパラメータのないinit()
がありますが、新しいフィールドが導入されたと想像してください。
class Subclass: Base { let value: Int init(value: Int) { self.value = value } }
ここでは、空のinit
がなくなったため、 required
として宣言する必要があります。
class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()
Subclass
でrequired init
明示的に宣言しなかったにもかかわらず、コンパイラがそれを生成したことに注意してください。これについては、コンパイラ支援で説明しました。 required init
は自動的に継承され、 super.init()
と呼ばれます。
class Subclass: Base { required init() { super.init() } }
プロトコルで宣言されたすべての初期化子はrequired
です。
protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }
繰り返しますが、これは、コンパイラがサブクラスがプロトコル初期化子を実装していることを確認するために必要です。すでにご存知のとおり、これは常に行われるわけではありません。init init
required
でない場合、サブクラスはそれをオーバーライドする必要はなく、独自の初期化子を定義できます。
class IntValue: InitableObject { let value: Int init(value: Int) { self.value = value } } let InitableType: Initable.Type = IntValue.self let initable: Initable = InitableType.init()
もちろん、 Base.init()
はrequired
ないため、次のコードはコンパイルされません。
class InitableObject: Initable { required init() { } // ✅ } class IntValue: InitableObject { let value: Int required init() { self.value = 0 } init(value: Int) { self.value = value } }
静的メソッドでSelf()
初期化子を呼び出す場合にも同様の状況が発生します。
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 }
いつものように、問題は継承にあります。
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
required
の目的はサブクラスで初期化子の実装を強制することなので、当然、 final
キーワードを使用して継承を禁止すると、初期化子をrequired
としてマークする必要がなくなります。
protocol Initable { init() } final class InitableObject: Initable { } // ✅
protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }
init()
のみがある場合、サブクラスの初期化子で自動的に呼び出されます。Self()
で使用するために、サブクラスでの存在を保証するには、 required
初期化子が必要です。パラメータなしのUIView()
初期化子について簡単に説明します。これはドキュメントには記載されていませんが、不思議なことにあらゆる場所で使用されています。
その理由は、 UIView
がNSObject
を継承しており、NSObject にはパラメータのないinit()
があるためです。したがって、この初期化子はUIView
インターフェースで明示的に宣言されていませんが、それでも利用可能です。
@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()
ただし、内部的には、この初期化子は、コードで初期化されるときはinit(frame:)
を呼び出し、Interface Builder 経由で初期化されるときはinit(coder:)
を呼び出します。これは、 UIView
独自のNSObject.init()
実装を提供するため発生します。これは、 method_getImplementation
がNSObject.init()
とUIView.init()
に対して異なるアドレスを返すことから確認できます。
失敗可能な初期化とは、オプションの
final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }
生の値を持つ列挙型は、無料のinit?(rawValue:)
を取得します。
enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")
enum 用のカスタム init を作成することもできます。すべての enum init はself
を割り当てる必要があります。
enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }
Swift の初期化子の重要な側面をすべて説明しました。
初期化子では、すべてのフィールドに値を入力する必要があります。
オプションのvar
プロパティのデフォルトはnil
です。
構造体は、自由なメンバーワイズ初期化子を受け取ります。
カスタム初期化子が定義されると、メンバー単位の初期化子は消えます。
指定された初期化子は、すべてのフィールドに値が設定されていることを確認し、 super.init()
を呼び出します。
便利な初期化子は、指定された初期化子を呼び出すことによって初期化を簡素化します。
便利な初期化子は常に水平方向( self.init
) に配置され、指定された初期化子は垂直方向( super.init
) に配置されます。
サブクラスで新しいプロパティを宣言すると、便利な初期化子は使用できなくなります。
スーパークラスのコンビニエンス イニシャライザを復元するには、その指定されたイニシャライザをすべてオーバーライドする必要があります。
オーバーライドの数を最小限に抑えるには、便利なオーバーライド初期化子を使用できます。
サブクラスが新しいパラメータを導入しない場合、サブクラスはスーパークラスからすべての初期化子を自動的に継承します。
スーパークラスにパラメータなしのinit()
のみがある場合、サブクラスの初期化子で自動的に呼び出されます。
必須の初期化子は、ジェネリック、プロトコル、およびSelf()
で使用するためにサブクラスに存在することを保証します。
UIView.init()
UIView.init(frame:)
またはUIView.init(coder:)
いずれかを呼び出します。
失敗可能な初期化子はオプションを返します。
生の値を持つ列挙型は、無料のinit?(rawValue:)
を取得します。
すべての列挙型初期化子はself
を割り当てる必要があります。
この記事で何か役に立つ情報を見つけていただければ幸いです。不明な点や不正確な点がございましたら、Telegram: @kfamynまでお気軽にご連絡ください。無料でご説明いたします。