イニシャライザ

イニシャライザはinitという名の特別なメソッドで、構造体とクラスで初期化のために使用します。Objective-Cのイニシャライザは生成したインスタンスを返す必要がありますが、Swiftの場合は何も返しません。

構造体とクラスのイニシャライザの書き方は基本的に同じですが、それぞれで気をつけるところがあるのでそれらについて以下で説明します。

構造体


/* パーソン */
struct Person {
    var name: String    // 名前
    var age: Int        // 年齢
    // イニシャライザ
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}
let person = Person(name:"タロウ", age: 18)

イニシャライザは上の様に関数と違い、initの前にfuncは不要です。また、引数の内部名は自動的に外部名となり(関数で内部名に#をつけたのと同じ)、インスタンスの生成時に引数のラベルをつける必要があります。

構造体の場合は、イニシャライザを記述しないと、プロパティ値を引数にとるイニシャライザ(Memberwise Initialization:メンバワイズイニシャライザといいます。)を自動的に生成してくれます。


/* パーソン */
struct Person {
    var name: String    // 名前
    var age: Int        // 年齢
}
let person = Person(name:"タロウ", age: 18)

引数のラベルをつけたくない場合は、次のように、外部名に明示的に_(下線)を指定します。


/* パーソン */
struct Person {
    var name: String    // 名前
    var age: Int        // 年齢
    // イニシャライザ
    init(_ name: String, _ age: Int) {
        self.name = name
        self.age = age
    }
}
let person = Person("タロウ", 18)

イニシャライザの重要な役割はプロパティの初期化ですが、プロパティの初期値を宣言時に全て与えている場合に限り、自前でイニシャライザを用意しなくても引数をとらないイニシャライザ(これをデフォルトイニシャライザと言います)を自動的に生成してくれます。この場合でもメンバワイズイニシャライザも使用できます。


/* パーソン */
struct Person {
    var name: String = ""   // 名前
    var age: Int = 0        // 年齢
}
var taro = Person()
taro.name = "タロウ"
taro.age = 18
var hanako = Person(name:"ハナコ", 16)

プロパティの宣言時に初期値を与えない場合は、必ずイニシャライザで初期値を与える必要があります。


/* パーソン */
struct Person {
    var name: String
    var age: Int
    init() {
        name = ""
        age = 0
    }
}

オプショナル型の場合は、宣言時やイニシャライザで初期値を設定しないとnilになります。


/* パーソン */
struct Person {
    var name: String?
    var age: Int
    init() {
        age = 0
    }
}
var person = Person()
println(person.name)        // nil

自前のイニシャライザを用意する場合は、デフォルトニイシャライザやメンバワイズイニシャライザは自動的に生成されないので必要に応じて全て記述する必要があります。


/* パーソン */
struct Person {
    var name: String = ""
    var age: Int = 0
    
    // デフォルトイニシャライザ
    init() {}
    // メンバワイズイニシャライザ
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    // カスタムイニシャライザ
    init(name: String, birthday: NSDate) {
        // 生年月日から年齢を算出
        let components = NSCalendar.currentCalendar().components(
            .CalendarUnitYear,
            fromDate: birthday,
            toDate: NSDate.date(), options: .allZeros)
        self.init(name: name, age: components.year)
    }
}

let formatter = NSDateFormatter()
formatter.dateFormat = "Y/M/d"
let birthday = formatter.dateFromString("1965/10/20")
let person = Person(name: "タロウ", birthday: birthday!)

上の例ではコードの重複を避けるため、3つ目のイニシャライザからメンバワイズイニシャライザを呼び出しています。但しこのようにイニシャライザを呼び出すことができるのは、別のイニシャライザの中からのみです。それ以外のメソッドからイニシャライザを呼び出すことはできません。

イニシャライザの中で別のイニシャライザを呼び出す場合、initの呼び出しによってプロパティの初期化が終わるまでselfを使用することはできません。次の様にするとコンパイルエラーになります。


/* パーソン */
struct Person {
    :
    // メンバワイズイニシャライザ
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    // カスタムイニシャライザ
    init(name: String, birthday: NSDate) {
        self.name = name                        // initの呼び出し前にselfを使うとエラー
        let age = self.calculateAge(birthday)   // 同上
        self.init(name: name, age: age)         // initのselfはOK
    }
    // 年齢計算
    func calculateAge(birthday: NSDate) -> Int {
        let components = NSCalendar.currentCalendar().components(
            .CalendarUnitYear,
            fromDate: birthday,
            toDate: NSDate.date(), options: .allZeros)
        return components.year
    }
}

次の様に、年齢計算メソッドを静的メソッドにして、別のイニシャライザの呼び出しの前にselfを使わない様にするとエラーになりません。


/* パーソン */
struct Person {
    :
    // カスタムイニシャライザ
    init(name: String, birthday: NSDate) {
        let age = Pserson.calculateAge(birthday)
        self.init(name: name, age: age)
    }
    // 年齢計算
    static func calculateAge(birthday: NSDate) -> Int {
        :
    }
}

また、次の様に年齢計算メソッドをイニシャライザの中でネストしたメソッドとして使う方法もあります。この場合、年齢計算メソッドはこのイニシャライザの中からしか呼び出せません。


/* パーソン */
struct Person {
    :
    // カスタムイニシャライザ
    init(name: String, birthday: NSDate) {
         // 年齢計算
        func calculateAge(birthday: NSDate) -> Int {
            let components = NSCalendar.currentCalendar().components(
                .CalendarUnitYear,
                fromDate: birthday,
                toDate: NSDate.date(), options: .allZeros)
            return components.year
        }
        let age = calculateAge(birthday)
        self.init(name: name, age: age)
    }
    :
}

 

クラス

デフォルトイニシャライザ

クラスの場合もデフォルトイニシャライザが用意されています。プロパティの宣言時に初期値を与えているか、オプショナル型の場合に限り、引数をとらないイニシャライザが自動生成されます。


class Person {
    var name: String?
    var age: Int = 0
}
var person = Person()    // name:nil,  age:0
person.name = "タロウ"
person.age = 18

但し、構造体と異なりメンバワイズイニシャライザは自動生成されません。必要に応じて自分で記述する必要があります。


class Person {
    var name: String?
    var age: Int = 0
}
// 次の呼び出しはエラーになる
var person = Person(name: "タロウ", age: 0)

指定イニシャライザとコンビニエンスイニシャライザ

クラスのイニシャライザは、指定イニシャライザコンビニエンスイニシャライザに大別されます。
指定イニシャライザはそのクラスの中心となるイニシャライザで、通常は全てのプロパティの初期化を行うような処理を担います。一方、コンビニエンスイニシャライザは補佐的なイニシャライザで、呼び出し方を簡便にしたり、別の引数を与えて初期化を行うような場合に使用します。クラスには最低1つ以上の指定イニシャライザが必要です。またコンビニエンスイニシャライザからは最終的に指定イニシャライザを呼び出す様にします。

デフォルトイニシャライザは指定イニシャライザです。

コンビニエンスイラシャライザはinitの前に、convenienceをつけて宣言します。


/* パーソン */
class Person {
    var name: String
    var age: Int
    // 指定イニシャライザ
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    // コンビニエンスイニシャライザ
    convenience init(name: String, birthday: NSDate) {
        // 生年月日から年齢を算出
        let components = NSCalendar.currentCalendar().components(
            .CalendarUnitYear,
            fromDate: birthday,
            toDate: NSDate.date(), options: .allZeros)
        // 指定イニシャライザの呼び出し
        self.init(name: name, age: components.year)
    }
}

指定イニシャライザとコンビニエンスイニシャライザ間の関係は、以下のルールに従う必要があります。

ルール1
サブクラスの指定イニシャライザは、直近の親クラスの指定イニシャライザを呼ばなくてはならない。
ルール2
コンビニエンスイニシャライザは、同じクラスの他のイニシャライザを呼ばなくてはならない。
ルール3
コンビニエンスイニシャライザは、最終的に(呼び出すニイシャライザのどこかで)指定イニシャライザを呼ばなくてはならない。

つまり、最終的に継承ツリーの中の全てのクラスの指定イニシャライザが呼ばれる必要があります。

init_delegate.png

2フェーズの初期化

クラスの初期化は、2フェーズのプロセスで構成されます。

2フェーズの構成にすることで、値が未設定のプロパティにアクセスしたり、設定した値が他のイニシャライザの呼び出しによって別の値に上書きされることを防ぐことができます。

コンパイラは次のセーフティチェックを行います。

セーフティチェック1
指定イニシャライザがスーバークラスのイニシャライザを呼ぶ前に、そのクラス自身のプロパティを全てに初期値を与えているか?
セーフティチェック2
指定イニシャライザの中で、継承したプロパティに値を設定する前にスーパークラスのイニシャライザを呼んでいるか?(スーパークラスのイニシャライザを後から呼ぶと、その前に設定したプロパティが上書きされてしまう)
セーフティチェック3
コンビニエンスイニシャライザの中でプロパティの設定をする前に、そのクラスの別のイニシャライザを呼んでいるか?(イニシャライザを後から呼ぶと、その前に設定したプロパティがそのクラスの指定イニシャライザによって上書きされてしまう)
セーフティチェック4
フェーズ1が終わる前に、インスタンスメソッド、インスタンスプロパティ、selfへのアクセスをしていないか?(初期値の設定が済んでいないのでフェーズ1が終わるまでselfにアクセスできない)

フェーズ1とフェーズ2はそれぞれ次のような流れになります。

フェーズ1

  • クラスの指定イニシャライザ、又はコンビニエンスイニシャライザが呼ばれる。
  • インスタンス用のメモリが確保される。この時点ではまだメモリは初期化されていない。
  • 指定イニシャライザの呼び出し完了時点でそのクラスの保持型プロパティの初期値が全て与えられる。インスタンスの初期化が完了する。
  • スーパークラスのプロパティに初期値を与えるため、スーパークラスの指定イニシャライザ呼び出す。
  • 継承ツリーの最初のクラスに達するまで指定イニシャライザの呼び出しが繰り返される。
  • ベースクラスの指定イニシャライザの実行が終わると全てのプロパティに初期値が与えられたことになり、ここまででフェーズ1が完了する。
  • フェーズ2

  • ベースクラスの指定イニシャライザの呼び出しが終わり、継承ツリーを下る過程で、各クラスのイニシャライザの中で必要に応じて追加の処理が行われる。この時点では、selfへのアクセスも可能となる。
  • 最終的に、継承ツリーの最下部のクラスのコンビニエンスイニシャライザの呼び出しまで戻り、最終的な処理が行われ、全てのイニシャライザの呼び出しが完了する。
  • init_delegate2.png

    イニシャライザの自動継承

    ある条件の元では、スーパークラスのイニシャライザが自動的にサブクラスに継承されます。それは次の場合です。

    条件1
    サブクラスで指定イニシャライザを定義しない場合、スーパークラスの指定イシニャライザがそのまま継承される。
    条件2
    条件1による自動継承、或いは独自イニシャライザの提供に関わらず、スーパークラスの指定イニシャライザを全てサブクラスで定義すると、スーパークラスのコンビニエンスイニシャライザは自動的に全て継承される。これは、スーパークラスに無い新たなコンビニエンスイニシャライザを別途追加したとしても同様。

    イニシャライザの継承

    ここまでの説明を踏まえてサンプルコードを以下に示します。

    まずベースクラスです。ここではレンタル商品のクラスをベースクラスとします。レンタル商品クラスは商品名、価格、レンタル期間をプロパティに持ちます。
    そして、2つの指定イニシャライザと1つのコンビニエンスイニシャライザを持ちます。指定イニシャライザの1つは中身が空ですが、プロパティは全て初期値を与えているかオプショナル型なので、initの中で初期値を与えなくてもエラーになりません。
    コンビニエンスイニシャライザからは指定イニシャライザを呼び出しています。(ルール2

    
    /* レンタル商品 */
    class RentalGoods {
        var name: String?   // 品目名
        var price: Int = 0  // 価格
        var term: (startDate:NSDate, days:Int)?     // レンタル期間(開始日, 日数)
        // 指定イニシャライザ
        init() {}
        // 指定イニシャライザ
        init(name: String, price: Int, term: (NSDate, Int)) {
            self.name = name
            self.price = price
            self.term = term
        }
        // コンビニエンスイニシャライザ
        convenience init(name: String, price: Int, days: Int) {
            // 今日を開始日とする
            self.init(name:name, price:price, term:(NSDate.date(), days))
        }
        // 内容出力
        func printDescription() {
            let dateFormatter = NSDateFormatter()
            dateFormatter.dateFormat = "yyyy/MM/dd"
            print("商品名:\(name!)\t価格:\(price)円\tレンタル期間:\(dateFormatter.stringFromDate(term!.startDate))から\(term!.days)日間")
        }
    }
    

    次に、レンタル商品クラスを継承して、インターネットでレンタルできるネットレンタル商品(デジタル商品)を表すクラスをつくったとします。ここでは、名前をNetRentalGoodsとします。
    新たなプロパティとして、容量を表すsizeプロパティと、容量の単位を表すunitプロパティを追加します。

    引数を取らないイニシャライザはスーパークラスのイニシャライザをオーバーライドするので、overrideを指定します。sizeプロパティは宣言時に初期値を与えていないので、スーバークラスのイニシャライザを呼び出す前に設定する必要があります。(フェーズ1)
    unitプロパティは宣言時に初期値を与えているのでイニシャライザの中で設定する必要はありません。

    また、指定イニシャライザの中で、スーパークラスの指定イニシャライザを呼び出しています。(ルール1)

    そして、コンビニエンスイニシャライザからは、自身の指定イニシャライザを呼び出しています。(ルール3)

    
    /* ネットレンタル商品 */
    class NetRentalGoods: RentalGoods {
        var size: Float                 // 容量(MByte)
        var unit: String = "MByte"      // 単位
        // 指定イニシャライザ
        override init() {
            self.size = 0
            super.init()
        }
        // 指定イニシャライザ
        init(name: String, price: Int, term: (NSDate, Int), size: Float) {
            self.size = size
            super.init(name: name, price: price, term: term)
        }
        // コンビニエンスイニシャライザ
        convenience init(name: String, price: Int, days: Int, size: Float) {
            self.init(name: name, price: price, term: (NSDate.date(), days), size:size)
        }
        // 内容出力
        override func printDescription() {
            super.printDescription()
            let s = String(format: "%.2f", size) + unit
            print("容量:\(s)")
        }
    }
    

    最後に、ネットレンタル商品クラスを継承して、映画レンタル商品クラスを作成します。

    新たなプロパティとして、監督(producer)、主演俳優(leadingActors)、上映時間(screenTime)を追加しました。ここではプロパティの宣言時に初期値を与えず、指定イニシャライザの中で設定しています。ここでも上の場合と同じ酔うに、自クラスのプロパティの初期値設定は、スーパークラスのイニシャライザの呼び出し(super.init(〜))の前に行っています。

    さらに、指定イニシャライザの中で価格を変更したり別のメソッドを呼び出しています。このように、スーパークラスがもつプロパティに値を設定したり、他のメソッドを呼び出す場合は、必ずスーパークラスのイニシャライザの呼び出しが終わった後(フェーズ2)で行う必要があります。上で説明したように、フェーズ1が完了するまでインスタンスの内容が適切に初期化されていないためです。また、設定の後にイニシャライザ呼び出すと設定した値が上書きされてしまいます。

    フェーズ2であってもスーパークラスの定数を上書きすることはできません。

    下の例ではネットレンタル商品クラスの指定イニシャライザをオーバーライドしてコンビニエンスイニシャライザを定義しています。このように、スーバークラスの指定イニシャライザをコンビニエンスイニシャライザとしてオーバーライドすることも可能です。その場合は、override convenienceと宣言します。

    
    /* 映画ネットレンタル商品 */
    class MovieRentalGoods: NetRentalGoods {
        var producer: String           // 監督
        var leadingActors: String      // 主演俳優
        var screenTime: Int            // 上映時間
        // 指定イニシャライザ
        override init() {
            // フェーズ1
            self.producer = ""
            self.leadingActors = ""
            self.screenTime = 0
            super.init()
            // フェーズ2
            self.price = 500
            self.adjustSizeUnit()
        }
        // 指定イニシャライザ
        init(name: String, price: Int, term: (NSDate, Int), size: Float, producer: String, leadingActors: String, screenTime: Int) {
            // フェーズ1
            self.producer = producer
            self.leadingActors = leadingActors
            self.screenTime = screenTime
            super.init(name: name, price: price, term: term, size: size)
            // フェーズ2
            self.adjustSizeUnit()
        }
        // コンビニエンスイニシャライザ
        override convenience init(name: String, price: Int, term: (NSDate, Int), size: Float) {
            self.init(name: name, price: price, term: term, size: size, producer: "", leadingActors: "", screenTime: 0)
        }
        // サイズの単位の変更
        func adjustSizeUnit() {
            if (self.size > 1024) {
                self.size /= 1024
                self.unit = "GByte"
            }
        }
        // 内容出力
        override func printDescription() {
            super.printDescription()
            print("監督:\(producer)\t主演俳優:\(leadingActors)\t上映時間:\(screenTime)分")
        }
    }
    
    let movie = MovieRentalGoods(name: "ローマの休日", price: 400, days: 2, size: 3600)
    movie.producer = "ウィリアム・ワイラー"
    movie.leadingActors = "オードリーヘップバーン, グレゴリーベック"
    movie.screenTime = 118
    movie.printDescription()
    /*
    商品名:ローマの休日	価格:400円	レンタル期間:2014/09/02から2日間
    容量:3.52GByte
    監督:ウィリアム・ワイラー	主演俳優:オードリーヘップバーン, グレゴリーベック	上映時間:118分
    */
    

    上の映画ネットレンタル商品クラスでは、スーパークラスの2つの指定イニシャライザをオーバーライドしているので、スーバークラスのコンビニエンスイニシャライザも自動的に継承され呼び出すことができています。(イニシャライザの自動継承の条件2)
    もし、ネットレンタル商品クラスのコンビニエンスイニシャライザを再定義する場合、ルール2の制約により、再定義したイニシャライザからスーパークラスのコンビニエンスイニシャライザを呼び出すことはできません。その場合、映画ネットレンタル商品クラスの指定イニシャライザを呼び出す必要があります。またこの場合、overrideの指定はできません。

    init_delegate3.png

    必須イニシャライザ

    イニシャライザの前にrequiredをつけて宣言した場合、そのクラスを継承するサブクラスは必ずそのイニシャライザを実装する必要があります。但し、そのイニシャライザが自動的に継承される場合は明示的に実装する必要はありません。
    サブクラスで実装する場合、そのイニシャライザの前にもrequiredをつけて宣言する必要があります。その場合、overrideは不要です。

    
    /* ラベル */
    class Label {
        var title: String   // タイトル
        var width: Int      // 幅
        var height: Int     // 高さ
        // 指定イニシャライザ
        required init(title: String, width: Int, height: Int) {
            self.title = title
            self.width = width
            self.height = height
        }
    }
    
    /* ボタン(これはエラー) */
    class Button: Label {
        var pushed: Bool    // 押された?
        // 指定イニシャライザ
        init(title: String, width: Int, height: Int, pushed: Bool) {
            // 必須イニシャライザの実装が無いのでコンパイルエラー
            super.init(title, width, height)
        }
    }
    
    // ↓ならOK
    /* ボタン */
    class Button: Label {
        var pushed: Bool    // 押された?
        // 指定イニシャライザ
        required init(title: String, width: Int, height: Int) {
            self.pushed = false
            super.init(title: title, width: width, height: height)
        }
        // コンビニエンスイニシャライザ
        convenience init(title: String, width: Int, height: Int, pushed: Bool) {
            self.init(title: title, width: width, height: height)
            self.pushed = pushed
        }
    }
    
    

    関数オブジェクトによるプロパティの初期化

    次の例の様に、プロパティの宣言時に関数の呼び出し結果を使うことができます。squaresプロパティに無名関数オブジェクトを設定し最後に()をつけて呼び出しています。()がないと、関数の戻り値ではなく、関数オブジェクトそのものを割り当てることになり型が合わないためコンパイルエラーが発生します。

    
    /* 
      ビンゴカード 
      5 x 5 のマス目にランダムに割り当てられた1〜25の数値を持つ
    */
    struct BingoCard {
        // 各マス目の番号配列(関数オブジェクトを呼び出して初期化)
        let squares: [Int] = {
            var numbers = [Int]()
            for i in 1...25 {
                numbers.append(i)
            }
            var array = [Int]()
            while !numbers.isEmpty {
                let idx = Int(arc4random()) % numbers.count
                array.append(numbers.removeAtIndex(idx))
            }
            return array
        }()
        // 指定マス目の番号を返す
        subscript(index: Int) -> Int {
            return squares[index]
        }
        // 全ての番号を出力
        func printNumbers() {
            for var idx=0; idx<squares.count; idx++ {
                print("\(self[idx])\t", , terminator: ""))
                if (idx + 1) % 5 == 0 {
                    print("")
                }
            }
        }
    }
    
    let card = BingoCard()  // ビンゴカードのインスタンスを生成
    card.printNumbers()     // 番号を出力
    /* 出力結果例
    14	22	10	6	11
    1	20	9	5	2
    19	23	4	21	18
    3	15	7	8	17
    25	12	24	13	16
    */
    

    プロパティの宣言に使用する関数オブジェクトの呼び出し中はまだインスタンスの初期化が完了していないため、関数オブジェクトの中で他のプロパティにアクセスしたり、selfを使うことはできません。