Swiftの初期化 | Required Initializersとデフォルトプロパティ値のカスタマイズ

Swiftの初期化 | Required Initializers

初期化子の最後はrequired initializersです。Requireというのは「要求する」「必要とする」「命じる」等の訳が当てられますから、required initializersというのは「(実装するのが)必要な、または要求される初期化子」というニュアンスでしょうか。また、クロージャを使った、デフォルトプロパティ値のカスタマイズに関しても、具体例を使って簡単に説明します。

Required Initializers

requiredキーワードを使って初期化子を定義すると、サブクラス(subclass)で同じパラメータを持った初期化子を実装しなければコンパイルエラーになります。

// Required initializerのテスト
class Super {
    var property: Int
    required init(property: Int) {
        self.property = property
    }
}

class Sub: Super {
    var subProperty: Int
    init() {
        self.subProperty = 10
        super.init(property: 20)
    }
}

//コンパイルエラー
//error: 'required' initializer 'init(property:)' must be provided by subclass of 'Super'

今、サブクラスでrequired init(property: Int)を実装することが必須なので、コンパイルエラーが出ています。

この場合

// サブクラスでrequired initializer
required init(property: Int) {
    self.subProperty = property
    super.init(property: property)
}

のように、サブクラスで同じ初期化子を実装する必要があります。重要なのは、

サブクラスでも必ずrequiredキーワードが必要

だということです。

サブクラスでもrequiredキーワードが必要だということは、このrequired連鎖はサブクラスのサブクラスでも続きます。

つまり、サブクラスを継承した孫サブクラス(サブクラスを派生したクラス)でも、同じ初期化子を定義しないとコンパイルエラーになります。さらに、そこにもrequiredキーワードが必要ですから、そこから派生したクラスにも・・・、という具合に連鎖が続きます。

また、reuquired initializerの場合、override(オーバーライド)キーワードは必要ありません。そういう意味では、

requiredキーワードは、実装するのが必須なオーバーライド

と言えるかもしれません。

公式マニュアルに注意書きがありますが、もしrequired initializerが暗黙に継承されると、サブクラスで明示的に実装する必要はありません。これはautomatic initializer inheritanceの話ではないかと思いましたが、特にそれに関する言及はありませんでした。

“NOTE

You do not have to provide an explicit implementation of a required initializer if you can satisfy the requirement with an inherited initializer.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

例えば、上記の例ですと、

class Sub: Super {
    var subProperty: Int = 0
}

という感じでプロパティ(property)にデフォルト値を与えると、明示的にrequired initializerを定義しなくてもコンパイルが通るようになります。

クロージャまたは関数でデフォルトプロパティ値のカスタマイズ

これは初期化子というよりは、プロパティの初期値をどう与えるか?という話です。これまでプロパティに値をセットする場合は、

class SomeClass {
    var property = 5
}

という感じで、値で直接初期化していました。「もうちょっと複雑な計算処理を施したい!」とか「単純な処理なんだけどカスタマイズ機能を付けたい」というケースがあるかもしれません。こういう時はクロージャ(closures)やグローバル関数(global functions)の出番です。

クロージャやグローバル関数経由でプロパティを初期化する場合、インスタンスが作られた際に、クロージャ等の戻り値がプロパティのデフォルト値として割り当てられます。具体例を使って、グローバル関数の場合と、クロージャの場合について見ていきます。

グローバル関数を使った具体例

グローバル関数の場合は簡単です。

// グローバル関数
func sum(a: Int, b: Int) -> Int {
    return a + b
}

struct SomeStruct {
    // グローバル関数でプロパティを初期化
    let property = sum(a:2, b:3)
}

let structure = SomeStruct()
print(structure.property)
//"5"と表示

上記の例では、2つの整数の和を返すグローバル関数sum(a:b:)を使って、構造体SomeStructのプロパティpropertyに初期値を与えています。

クロージャを使った具体例

クロージャを使って、プロパティにデフォルト値を与える場合の定義構文は、

class ClassName {
    let property: type = {
        //何かしらの処理
        return value
    }()
}

という感じになります。戻り値valueの型は、typeになります。

1つ重要なのは、クロージャの最後が空のカッコ()で終わっている点です。これを挿入することで、Swiftに「このクロージャをすぐ実行しろ」と言っています。ですから、もしカッコ()を省くと、クロージャ自体をプロパティに割り当てることになります。その場合、上記のproperty関数型(function types)のプロパティになってしまって、意図した実装ではなくなるので注意が必要です。

先程の構造体を流用して、クロージャを使った簡単なプロパティの初期化を実装してみます。

struct SomeStruct {
    // クロージャでプロパティを初期化
    let constant: Int = {
        let array = [5,2,4,1]
        var sum = 0
        for item in array {
            sum += item
        }
        return sum
    }()
}

let structure = SomeStruct()
print(structure.constant)
//"12"と表示

定義構文で示したクロージャの実装をほぼそのまま書いただけなので、難しくはないと思います。先程も説明しましたが、もし最後のカッコを外した場合(つまりクロージャ自体をプロパティとして使いたい場合)、constantは関数型になりますので、使用する時は

structure.constant()

という感じで呼び出さないといけません。

公式マニュアルに注意書きがありますが、クロージャが実行される時点では、インスタンスが保有するその他のプロパティは初期化されていません。つまり、クロージャ内部では、初期値を与えたいプロパティも含め、その他のプロパティへアクセスすることが出来ません。これは、プロパティにデフォルト値を与えていようが関係ありません。ということは、selfプロパティも使えませんし、インスタンスメソッド(instance methods)も使えません。

“NOTE

If you use a closure to initialize a property, remember that the rest of the instance has not yet been initialized at the point that the closure is executed. This means that you cannot access any other property values from within your closure, even if those properties have default values. You also cannot use the implicit self property, or call any of the instance’s methods.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

試しに、プロパティを別に作ってそれを参照しようとすると、

// 別のプロパティを呼び出してみる
....
var someProperty = 10
let constant: Int = {
    let array = [5,2,4,1]
    var sum = 0
    for item in array {
        sum += item
    }
    return sum + someProperty
}()
....
//error: instance member 'someProperty' cannot be used on type 'SomeStruct'

怒られました。エラーメッセージを見る限り、こういうクロージャ形式は型プロパティのような扱いに近い感じがします。

クロージャ内部で型プロパティは問題なく使用できます。おそらく型メソッドも使えますが、テストはしていません。

公式マニュアルでは、クロージャ内部の変数等を「一時的な変数」と強調しているのは、こういう理由があるからですね。

まとめ

  • requiredキーワード指定によって、サブクラスで強制的に「同じ」初期化子を実装可能
  • サブクラスのrequired initializersには、overrideキーワードは不要
  • デフォルトプロパティ値をカスタマイズしたい場合は、グローバル関数かクロージャを使う