初期化(Initialization)について
クラス、列挙型、構造体のインスタンスを作成するプロセスを 初期化 と呼びます。
初期化では主に、以下の2点を行います。
- 格納プロパティへの値の割当
すべての格納プロパティには、インスタンス生成時に必ず値を与えなければなりません。
これにより、後の処理で値が「未設定」である状態を回避します。
- インスタンスに必要な機能のセットアップ
たとえば、プロパティの初期値に基づいた内部状態の調整や、外部リソースへの接続などを行います。
このプロセスを実装するために、イニシャライザ(initializer) を定義します。
イニシャライザは、実際のインスタンス作成・セットアップの処理が記述された特別なメソッドです。
Swiftでは、名前は必ず init() とし、引数を利用して初期化に必要な値を受け取ります。
格納プロパティの初期値とオプショナル
すべての格納プロパティは、インスタンス作成時に値が設定されなければならないため、必ず初期値を与えるか
イニシャライザで明示的に初期化する必要があります。
例:回答用紙(AnsSheet)のイメージ
回答用紙をインスタンス化する例として、以下のようなクラスを考えます。
-
回答が必須の場合(例:名前)
→name
プロパティは必ず値が必要。オプショナルにはできません。 -
回答が任意の場合(例:問題の回答)
→ 回答ができなかった場合「わからない」としてnil
を許容するには、オプショナル型にします。
class AnsSheet {
let name: String // 必須:名前は必ず設定(非オプショナル)
var answer: Int? // 任意:回答がない場合は nil を許容
init(studentName: String, answer: Int?) {
self.name = studentName
self.answer = answer
}
}
使用例
let student1 = AnsSheet(studentName: "Sato Taro", answer: nil) // 名前は必須、回答は nil を許容
名前がない場合はエラーとなるので、たとえば以下はコンパイルエラーです。
let student2 = AnsSheet(studentName: nil, answer: nil) // エラー:name は String 型(非オプショナル)
また、全インスタンスで同じ初期値を使いたい場合は、プロパティにデフォルト値を設定することも可能です。
class AnsSheet {
var answerPattern: Int = 1 // すべてのインスタンスにあらかじめ 1 を設定
init() {} // イニシャライザで特に値を与える必要なし
}
定数プロパティ(let)の初期化
定数プロパティは、インスタンス生成時に一度だけ値を設定できます。
一度設定した後は変更ができません。
class AnsSheet {
let name: String // 一度のみ初期化できる
init(studentName: String) {
self.name = studentName
}
}
let student1 = AnsSheet(studentName: "Sato Taro")
// student1.name = "Saito Hanako" // これはエラーになる
実務例として、ユーザの一意な識別子(id)など、生成時だけ決まり以降変更してほしくない値に用いられます。
class User {
let id: UUID
init() {
self.id = UUID()
}
}
2段階初期化と self
へのアクセスタイミング
Swift のイニシャライザは 2段階初期化 を行います。
この仕組みにより、フェーズ1 ではすべての格納プロパティに 安全に値を設定 し、
その後 フェーズ2 に移行して self
へのアクセスやカスタム設定 を行います。
-
フェーズ1(初期化段階)
→ すべての格納プロパティに値が与えられるまで self は完全には使えません。 -
フェーズ2(設定段階)
→ 全プロパティが初期化された後、self にアクセスできる状態となり、
メソッド呼び出しなどが可能になります。
また、クラス継承時はサブクラスのプロパティを先に初期化し(ボトムアップ方式)、
その後 super.init()
を呼び出してスーパークラス側の初期化を行う必要があります。
class SuperClass {
var number: Int
var number2: Int
init(number: Int, number2: Int) {
self.number = number
self.number2 = number2
}
func reSetupNumbers() {
self.number = 1
self.number2 = 2
}
}
class SubClass: SuperClass {
var subNumber: Int
var subNumber2: Int
init(subNumber: Int, subNumber2: Int) {
// フェーズ1:サブクラス側のプロパティ初期化
self.subNumber = subNumber
self.subNumber2 = subNumber2
// サブクラス側が初期化されたら、スーパークラスの初期化を呼び出す
super.init(number: 1, number2: 1)
// フェーズ2:全初期化完了後に self にアクセス可能
self.reSetupNumbers()
}
override func reSetupNumbers() {
super.reSetupNumbers()
self.subNumber = 1
self.subNumber2 = 2
}
}
イニシャライザの委譲
Swiftでは、複数のイニシャライザを定義 して、呼び出し側の要件に応じた初期化方法を提供できます。
同じクラス内で初期化処理が重複する場合、あるイニシャライザから別のイニシャライザを呼び出す「 委譲 」が使えます。
値型(構造体・列挙型)の場合
例えば、構造体 Rect
の例です。
struct Point {
var x = 0.0
var y = 0.0
}
struct Size {
var width: Double
var height: Double
}
struct Rect {
var origin = Point()
var size = Size(width: 0.0, height: 0.0)
// デフォルトイニシャライザ
init() {}
// 指定の原点とサイズを使う
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
// 中心点とサイズから原点を計算し初期化する:他のイニシャライザに委譲する
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
委譲を使うことで、重複する処理をまとめ、読み手にも意図が伝わりやすくなります。
参照型(クラス)の場合
クラスの場合は、継承関係があるため、サブクラスのイニシャライザは 必ずスーパークラスの指定イニシャライザ(designated initializer)を呼ぶ 必要があります。
enum Class {
case named(grade: Int, group: Int)
}
class AnsSheetFormat {
let name: String
let cls: Class
init(name: String, cls: Class) {
self.name = name
self.cls = cls
}
}
class Test20251212: AnsSheetFormat {
var ans1: String
var ans2: String
var ans3: String
// サブクラスの指定イニシャライザ:まず自クラスのプロパティを初期化し、次にスーパークラス側を初期化する
init(name: String, cls: Class, ans1: String, ans2: String, ans3: String) {
self.ans1 = ans1
self.ans2 = ans2
self.ans3 = ans3
super.init(name: name, cls: cls)
}
}
let student1 = Test20251212(
name: "Sato Taro",
cls: Class.named(grade: 1, group: 1),
ans1: "ans1", ans2: "ans2", ans3: "ans3"
)
サブクラスのプロパティにデフォルト値がある場合は、override init(..)
を用いてイニシャライザを再定義できます。
また、スーパークラスのイニシャライザを再定義(オーバーライド)する際は、必ず同じシグネチャで定義し、内部で super.init(..)
を呼び出す必要があります。
convenience(利便)イニシャライザ
指定イニシャライザは、インスタンスの初期化責任をすべて持ちます。
一方、convenience
イニシャライザ は「補助」的な役割を持ち、最終的には同じクラス内の指定イニシャライザに委譲します。
たとえば、よく使われる初期値を自動的に与えたい場合に利用できます。
class SuperClass {
var number: Int
// 指定イニシャライザ:初期化の正規ルート
init(number: Int) {
self.number = number
}
// 利便イニシャライザ:指定イニシャライザに委譲する補助的なルート
convenience init() {
self.init(number: 10)
}
}
let superClass1 = SuperClass() // convenience init を使用(number は 10 になる)
let superClass2 = SuperClass(number: 20) // 指定イニシャライザを直接使用
-
convenience
イニシャライザ は必ず同じクラスの別のイニシャライザ(最終的には指定イニシャライザ)に委譲しなければならない
→ コードの重複を避け、意図を明示するための仕組みです。 -
「補助」と呼ばれるのは、使用頻度ではなく 初期化の責任を持たない ことを明示する。
動的ディスパッチとの関係(補足)
初期化そのものは動的ディスパッチの話題とは直接関連しませんが、継承時にスーパークラスのメソッド(例:setup()
)を呼び出す場合、
そのメソッドがサブクラスでオーバーライドされていると、実行時にサブクラス版が呼ばれます。
これが「動的ディスパッチ」による挙動です。
例:トップアップ方式での危険性
class Parent {
init() {
print("Parent init")
setup() // → 動的ディスパッチによりサブクラス版が呼ばれる可能性がある
}
func setup() {
print("Parent setup")
}
}
class Child: Parent {
var name: String = "Taro"
override func setup() {
// 初期化前に子クラスのプロパティにアクセスすると未初期化の可能性がありエラーになる
print("Child setup: \(name)")
}
}
let child = Child()
このような問題を避けるために、Swiftは ボトムアップ初期化(サブクラスのプロパティを先に初期化してから super.init()
を呼ぶ方式)を採用しています。
これにより、スーパークラスのイニシャライザ内で安全に self
にアクセスできる状態が保証されます。
まとめ
初期化(Initialization)
インスタンスを作成する際、すべてのプロパティに値を与え、必要なセットアップを行う。
-
イニシャライザ(init)
インスタンスの初期状態を設定する特別なメソッド。必ず全ての格納プロパティに値を設定する。 -
定数プロパティ
インスタンス生成時に一度だけ値が設定され、その後は変更できない。必須の情報(例:名前、ID)に用いる。 -
2段階初期化
(1) すべての格納プロパティに値を設定(初期化フェーズ)
(2) self を使った追加設定(設定フェーズ)
を経て安全なインスタンスが生成される。 -
イニシャライザの委譲
同じクラス内で初期化処理を共通化するために、あるイニシャライザから別のイニシャライザを呼び出す(特に値型で有効)。 -
継承時のイニシャライザ
サブクラスでは、まず自分のプロパティを初期化し、その後 super.init(...) を呼んでスーパークラス側を初期化する必要がある。 -
convenience イニシャライザ
主たる初期化(指定イニシャライザ)を補助するためのイニシャライザ。最終的に同じクラス内の指定イニシャライザに委譲する。 -
動的ディスパッチ(補足)
メソッドの呼び出し時、実際のインスタンス(実体)の型に応じてオーバーライドされたメソッドが実行される仕組み。
初期化中に未初期化の状態で動的ディスパッチが起きると問題が生じるため、Swiftではボトムアップ初期化を採用して安全性を確保している。
このように、Swiftの初期化は単に値を割り当てる以上に、言語設計上の安全性や意図の明示、コードの共通化を図るための仕組みが随所に組み込まれています。