読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(11) - Initialization

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Initialization(初期化)

クラス、構造体、列挙体はイニシャライザを記述することで各プロパティに初期値を与えることが出来ます。

クラスに関しては各リソースを解放するためにDeinitializerを記述することが出来ますが、今回そこまでいけるかどうかわかりません。

Setting Initial Values for Stored Properties

クラスと構造体に関してはプロパティに必ず初期値を与える必要があります。

初期値を与えるにはイニシャライザを記述するか、デフォルト値を設定します。どちらであってもプロパティオブザーバは呼び出されません。

// イニシャライザを記述する例
struct Fahrenheit {
    var temperature: Double

    init() {
        temperature = 32.0
    }
}

var f = Fahrenheit()
println("The default temperature is \(f.temperature)° Fahrenheit")
// prints "The default temperature is 32.0° Fahrenheit"

// デフォルト値を与える例
struct Fahrenheit {
    var temperature = 32.0
}

デフォルト値とイニシャライザで同じ値による初期化を行うとデフォルト値が優先されるそうです。(原文:If a property always takes the same initial value, provide a default value rather than setting a value within an initializer.)

Customizing Initialization

イニシャライザには引数を設定することが出来ます。引数の型が同じでも外部引数名を設定するとオーバーロードになります。

これファンクションでも出来るのかな?それなら普通に便利なんだけど。

struct Celsius {
    var temperatureInCelsius: Double = 0.0

    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

イニシャライザに関しては外部引数名をわざわざ設定しなくても自動で同名のものが提供されるようです。

外部引数名が設定されているファンクションは必ずその引数名を書く必要があります。イニシャライザも例外ではなく、省略出来ません。

struct Color {
    let red = 0.0, green = 0.0, blue = 0.0

    init(red: Double, green: Double, blue: Double) {
        self.red = red
        self.green = green
        self.blue = blue
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

// 外部引数名が自動で設定されるので、下の記述方法だとコンパイルエラーになる
let veryGreen = Color(0.0, 1.0, 0.0)

「プロパティは必ず初期化しなければならない」と書きましたが、Optionに関しては例外です。と言うか、宣言した時点でデフォルト値としてnilが設定される、と言ったほうがいいのかもしれません。

class SurveyQuestion {
    var text: String
    var response: String?

    init(text: String) {
        self.text = text
    }

    func ask() {
        println(text)
    }
}

let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

また、letで宣言したプロパティであってもイニシャライザでの初期化が可能です。

class SurveyQuestion {
    let text: String
    var response: String?

    init(text: String) {
        self.text = text
    }

    func ask() {
        println(text)
    }
}

let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

Default Initializers

すべてのプロパティに初期値が設定されている場合はイニシャライザを省略できます。

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}

var item = ShoppingListItem()

構造体に関しては上記のようにデフォルト値をそのまま使うことも出来ますし、Memberwise Initializerによって別途初期値を与えることも出来ます。

struct Size {
    var width = 0.0, height = 0.0
}

let twoByTwo = Size(width: 2.0, height: 2.0)

Initializer Delegation for Value Types

Initializer Delegation(イニシャライザの委譲)なんて仰々しい名前がついていますが、イニシャライザからは別のイニシャライザのオーバーロードを呼ぶことが出来ると言うだけです。

セクション名の「for Value Types」ですが、単に値型だと継承が出来ないってだけです。まぁとりあえず例を見てみましょう。

struct Size {
    var width = 0.0, height = 0.0
}

struct Point {
    var x = 0.0, y = 0.0
}

この二つの構造体をプロパティに持つRectと言う構造体があったとします。

struct Rect {
    var origin = Point()
    var size = Size()

    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)
    }
}

何の引数も渡されなければ、SizeとPointの初期値をそのまま使います。originとsizeが渡されてきた場合にはそのまま値をセットします。centerとsizeが渡されてきた場合は、一旦計算してからself.init(origin, size)を呼び出しています。

// 何も渡さない

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

// originとsizeを渡す

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

// centerとsizeを渡す

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

まぁ、そんなに難しい概念ではないですね。別の方法としてExtentionを使ってイニシャライザの定義そのものを拡張する方法が紹介されています。

Class Inheritance and Initialization

何らかのクラスを継承しているクラスでは、二種類のイニシャライザが用意されています。ドキュメントいわく「Designated Initializer(指定イニシャライザ?)」と「Convenience Initializer(利便イニシャライザ?)」だそうです。

Designated Initializerは、従来の(他言語での)サブクラスでのイニシャライザとほぼ同じです。何らかの方法ですべてのプロパティを初期化し、親クラスがイニシャライザを持っていれば、サブクラス側で最低でも一つはそれを呼び出さないといけません。

Convenience Initializerの方は、サブクラスでのみ適用されるイニシャライザです。ここで親クラスのイニシャライザを呼ぶことは出来ません。かわりに自クラスのDesignated Initializerを呼ぶ必要があります。

サブクラスでの初期化についてのルールをまとめるとこんな感じです。

  1. すべてのプロパティが初期化されている。(親クラスに初期化を委譲するのはアリ)
  2. Designated Initializerでは必ず親クラスのイニシャライザを呼び出す。
  3. Convenience Initializerでは最終的に必ず自クラスのDesignated Initializerを呼び出す。(他のConvenience Initializerを経由するのはOK)
  4. 初期化の第一段階(後述)が終わるまでは自身のインスタンスメソッドを呼び出したり、インスタンスプロパティの値を取得してはならない。

また、クラスの初期化には二段階あります。第一段階では呼び出したサブクラス→親クラス→…→基底クラスへと向かっていきますが、第二段階では基底クラス→親クラス→…→呼び出したサブクラスへと向かっていきます。

具体的な動作は以下の通りです。

  • 第一段階
    1. Designated InitializerもしくはConvenience Initializerが呼び出される。
    2. インスタンスを生成するためのメモリを確保する。(この段階ではまだ初期化されていない)
    3. Designated Initializerによってプロパティが初期化されることを確認する。この時にプロパティ用のメモリを確保しておく。
    4. Designated Initializerから親クラスのイニシャライザが呼び出され、プロパティが初期化されることを確認する。
    5. 基底クラスに至るまで上記の動作を繰り返す。
    6. 基底クラスのイニシャライザまで到達し、すべてのプロパティが初期化されるだろうと言う事が確認できた段階で初めてメモリにインスタンスが割り当てられる。
  • 第二段階
    1. 基底クラスからサブクラスに向かって実際にプロパティの値を初期化していく。イニシャライザはこの時selfキーワードを使用することで自身のプロパティやメソッドにアクセス出来る。
    2. 最後に、Convenience Initializerによる初期化が行われる。

なんだか複雑な話ですが、「Designated Initializerでは親クラスのイニシャライザを呼ぶ」「Convenience Initializerでは自クラスのイニシャライザを呼ぶ」だけ覚えておけば十分です。

基本的にサブクラスは親クラスのイニシャライザを継承していないので、ちゃんと自分でオーバーライドしてやる必要があります。これはDesignated / Convenience Initializer共通の性質です。が、二つの特例があります。

一つは「サブクラスで定義されたプロパティにすべてデフォルト値が与えられている」場合です。このケースであればイニシャライザを記述しなくとも親クラスのDesignated Initializerがすべて自動で継承されます。

もう一つは「サブクラスが親クラスの全てのDesignated Initializerを継承している」場合です。この場合は親クラスのConvenience Initializerをすべて自動で継承します。

要は「親クラスのプロパティがすべて初期化されることが保証されている」と「サブクラスで定義されたプロパティがすべて初期化されることが保証されている」の二条件が満たされていれば、わざわざイニシャライザをオーバーライドする必要がないわけです。

長くなってきてしまいました。そろそろ実例を見ていきましょう。こんな基底クラスを作成します。

class Food {
    var name: String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

Convenience Initializerを定義する場合は接頭語としてconvenienceをつけます。そのまんまですね。

このクラスは二通りの方法でインスタンスを取得することが出来ます。

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

まぁここまでは問題ないですね。じゃあ継承してみましょう。

class RecipeIngredient: Food {
    var quantity: Int

    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }

    convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

見ての通りconvenienceでない方のイニシャライザはちゃんと親クラスのDesignated Initializerを呼び出しています。

このRecipeIngredientと言うサブクラスには三つの初期化方法があります。「サブクラスが親クラスの全てのDesignated Initializerを継承している」と言う条件を満たしているので、FoodのConvenience Initializerが呼び出せるようになっているためです。

// FoodのConvenience Initializer
let oneMysteryItem = RecipeIngredient()

// RecipeIngredientのConvenience Initializer
let oneBacon = RecipeIngredient(name: "Bacon")

// RecipeIngredientのDesignated Initializer
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

oneMysteryItemが所持しているプロパティの値はnameが[Unnamed]でquantityが1です。処理の流れを追っていくとこんな感じです。

Foodのconvenience init()を呼び出す
↓
self.init(name: "[Unnamed]")
この時のselfはRecipeIngredientである
↓
RecipeIngredientのconvenience init(name: String)が呼び出される
↓
self.init(name: String, quantity: Int)が呼び出される
この時のselfは(省略)
↓
RecipeIngredientのinit(name: String, quantity: Int)が呼び出される
↓
quantityが初期化される
super.init(name: name)が呼び出される
この時のsuperはFoodである
↓
Foodのinit(name: String)が呼び出される
↓
nameが初期化される

ね?ちゃんとプロパティが全部初期化されているでしょう?これ、中々気持ち悪いですね。(褒め言葉

(※実際にこんな動きをしているわけではありません。あくまでイメージです。環境持ってる人はぜひ試してみてください。ただまぁ、親クラスでのselfがサブクラスになることは恐らくないです。そこからサブクラスのメソッドやプロパティが呼べてしまうことになるので…。何かしらゴニョゴニョやってるんだと思います。)

さらにRecipeIngredientを継承したサブクラスを作ってみます。

class ShoppingListItem: RecipeIngredient {
    var purchased = false

    var description: String {
    var output = "\(quantity) x \(name.lowercaseString)"
        output += purchased ? " ?" : " ?"
        return output
    }
}

新しくpurchasedとdescriptionと言うプロパティが追加されました。前者はデフォルト値が、後者はgetのみが定義されています。

Computed Propertyは結局何らかのプロパティの値(もしくは固定値)を返すことになるので初期化の必要がありません。なので「サブクラスで定義されたプロパティにすべてデフォルト値が与えられている」と言う条件を満たしています。これにより親クラスのDesignated Initializerは自動で継承されます。そうなると「サブクラスが親クラスの全てのDesignated Initializerを継承している」と言う条件も満たすことになります。よって親クラスのConvenience Initializerもすべて自動で継承されます。

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]

breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true

for item in breakfastList {
    println(item.description)
}
// 1 x orange juice ?
// 1 x bacon ?
// 6 x eggs ?

Setting a Default Property Value with a Closure or Function

色々と見てきましたが、結局のところプロパティの初期化にはイニシャライザを使うよりデフォルト値を与えた方が色々と楽です。

でもワンライナーでデフォルト値を書くにも限界があります。そんな時はクロージャを使ってデフォルト値を生成しましょう。

class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }()
}

上記のSomeClassのインスタンスを取得しようとするとsomePropertyに定義したクロージャが実行され、デフォルト値が生成されます。

struct Checkerboard {
    let boardColors: Bool[] = {
        var temporaryBoard = Bool[]()
        var isBlack = false
        
        for i in 1...10 {
            for j in 1...10 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            
            isBlack = !isBlack
        }
        
        return temporaryBoard
    }()

    func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
        return boardColors[(row * 10) + column]
    }
}

let board = Checkerboard()
println(board.squareIsBlackAtRow(0, column: 1))
// prints "true"
println(board.squareIsBlackAtRow(9, column: 9))
// prints "false"