導入 こんにちは!私の名前は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 つだけになりました。 ではなく、 指定された init を呼び出すことに注意してください。 便利なオーバーライド初期化子が super.init self このトリックは、Tjeerd in 't Veen 著の『 の第 5 章で詳しく説明されており、強くお勧めします。 Swift in Depth』 中間要約 、すべてのプロパティが設定されていることを確認して、 を呼び出します。 指定された初期化子は 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 つだけあり、それがパラメーターなし (引数のない ) である場合、サブクラスで明示的に宣言されたイニシャライザは を呼び出す 。この場合、Swift コンパイラは、引数なしで利用可能な への呼び出し 。 init() super.init() 必要はありません 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 にはパラメータのない があるためです。 、この初期化子は インターフェースで明示的に宣言されていませんが、それでも利用可能です。 UIView 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() ただし、内部的には、この初期化子は、コードで初期化されるときは を呼び出し、Interface Builder 経由で初期化されるときは を呼び出します。これは、 独自の 実装を提供するため発生します。これは、 が と に対して異なるアドレスを返すことから確認できます。 init(frame:) 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 関連リンク Swiftプログラミング言語 (6) / 初期化 Tjeerd in 't VeenによるSwift in Depth テレグラム - @kfamyn