paint-brush
Swift init()、一度きり@kfamyn
新しい歴史

Swift init()、一度きり

Kiryl Famin19m2025/03/21
Read on Terminal Reader

長すぎる; 読むには

この記事では、Swift 初期化子に不可欠なすべての内容 (メンバー単位、指定、利便性、利便性オーバーライド、必須ユースケース、パラメーターなしの UIView() 初期化子、コンパイラー支援、失敗可能な init、enum init など) について説明します。
featured image - Swift init()、一度きり
Kiryl Famin HackerNoon profile picture
0-item
1-item

導入

こんにちは!私の名前はKiryl Faminです。iOS開発者です。


今日は、Swift の初期化子のような単純なトピックを徹底的に調べたいと思います。一見単純なトピックですが、このトピックを完全に理解していないと、詳細を掘り下げることなくすぐに修正したいイライラするエラーが発生することがあります。


この記事では、初期化子に関連する以下の内容をすべて説明します。


  • 構造体のメンバ単位の初期化子を保持しながらカスタム初期化子を定義する方法

  • クラスに初期化子を記述する必要が必ずしもない理由

  • 指定された初期化子でsuper.initを呼び出すことが必ずしも必要なわけではない理由

  • super.initを呼び出す前にサブクラスのすべてのフィールドに値を設定する必要がある理由

  • サブクラスで最小限のオーバーライドですべての親初期化子にアクセスする方法

  • まさにrequired初期化子が必要な場合

  • UIView.init()は常にパラメータなしで呼び出されますが、 init(frame:)init(coder:)はオーバーライドされるのはなぜですか?


...などなど。でも、一歩ずつ進めていきましょう。

目次

基礎

構造

  • メンバー単位の初期化子
  • オプション
  • varlet
  • メンバーごとの初期化子を保持する

クラス

  • 指定された初期化子
  • 便利な初期化子
  • スーパークラスの便利な初期化子を保持する
  • オーバーライドの数を最小限に抑える
  • コンパイラ支援
  • 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)


明示的に初期化子を宣言しなくても構造体を初期化できることに注目してください。これは、構造体がコンパイラによって生成されたメンバー単位の初期化子を受け取るために発生します。これは構造体に対してのみ機能します。


「リファクタリング」→「メンバーワイズ初期化子を生成」を選択すると、どのように見えるか確認できます。


Xcode でメンバーごとの初期化子を生成する


 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が設定されていない場合、すべての構造体プロパティを初期化子で設定する必要があるため、コンパイル エラーが発生することに注意してください。

オプション、 varlet

フィールドに明示的に値を入力する必要がない唯一のケースは、フィールドがオプション( ? )変数( 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 つの目的を果たします。

  1. すべてのフィールドが入力されていることを確認します
  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 


現在の初期化子階層では、明示的に宣言されている init は 1 つだけです。


これは、スーパークラスの指定された初期化子をすべてオーバーライドすることで修正できます。

 class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅ 


便利な初期化子が復活しましたが、2つの初期化子が明示的に宣言されています


このように、次のサブクラスで便利な初期化子を使用するには、 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) // ✅ 


便利な初期化子が復活しましたが、GuideDogは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(age:, name:)が回復され、GuideDogの初期化子は2つだけが明示的に指定されます。


これで、各サブクラスには明示的に指定された初期化子が 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()


Subclassrequired 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()初期化子について簡単に説明します。これはドキュメントには記載されていませんが、不思議なことにあらゆる場所で使用されています。


その理由は、 UIViewNSObjectを継承しており、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_getImplementationNSObject.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までお気軽にご連絡ください。無料でご説明いたします。

関連リンク

  1. Swiftプログラミング言語 (6) / 初期化
  2. Tjeerd in 't VeenによるSwift in Depth
  3. テレグラム - @kfamyn