Swiftの初期化 | Failable Initializers

Swiftの初期化 | Failable Initializers

Failable Initializersです。これは「初期化が失敗する可能性がある(optionalsになる可能性がある)」ことを明示している初期化子のことです。Failable Initializersは全ての独自型(クラスや構造体、または列挙型)で定義できます。

このページでは、failable initializersとは何か?から始めて、その定義構文、具体的な使用例を各独自型で説明します。また、failable initializerの委譲とオーバーライドに関しても、具体例を交えながら詳しく説明します。最後に、implicitly unwrapped optionalsのインスタンスを生成する初期化子init!に関しても簡単に触れます。

Failable Initializersって何?

Optional型の値にするための初期化子

「Failable」は直訳すると「失敗可能」もしくは「失敗できる」という意味です。したがって、Failable Initializerは「失敗可能な初期化子」ですが、もうちょっと分かりやすく意訳すると

optional型の値を割り当てるための初期化子

になります。

Optionalsは「値がない」状態(nil)になる可能性があり、そういう意味で「(初期化の)失敗が可能」という名前が当てられているのではないかと思います。

うまい和訳が思いつかないので、このページでは英語のfailable initializersのまま使います。

Failable Initializersの定義構文

Failable initializerを定義する時には、通常の初期化子で使用するinitキーワードの直後にクエスチョンマーク?を付けます。したがって、定義構文としては

// Failable Initializersの定義構文
init?(parameters) {
    statements
}

という感じになります。公式マニュアルに注意書きがありますが、パラメータの型と名前が同じ2つの初期化子をfailableとnonfailableとして定義することは出来ません。後で試しにやってみます。

“NOTE

You cannot define a failable and a nonfailable initializer with the same parameter types and names.”

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

公式マニュアルでは、failableとの対比で敢えてnonfailableを使っていると思いますが、nonfailableというのは普通の初期化子のことです(not failableと書くと分かりやすいかもしれません)。

初期化の失敗にはreturn nil

Failable initializerを使うと、その型のoptionalsを生成しますが、実際に「初期化に失敗した」という状態にするにはどうすればいいでしょうか?Swiftのソースコード上で、「初期化に失敗した」というのを明示的に示すのには、return nilを使います。

「初期化子は値を返さないんじゃなかったのか?」という疑問が湧くと思いますが、その通りです。このreturn nilは、値を返すというよりは、「初期化を失敗した」という特別な意味で使われています。したがって、「初期化が成功した」という場合にはreturnキーワードは使えません。

Failable Initializersの具体例

では実際にfailable initializersの具体例を、それぞれの独自型毎に見ていきます。

構造体(Structures)の場合

構造体の具体例です。以下の構造体は平仮名構造体です。

// 平仮名構造体
struct Hiragana {
    let character: Character
    init?(character: Character) {
        switch character {
            case "あ"..."ん":
                self.character = character
            default:
                return nil
        }
    }
}

// インスタンス生成
let thisIsHiragana = Hiragana(character: "あ")
let thisIsKatakana = Hiragana(character: "ア")

// テスト用関数
func checkHiragana(string: Hiragana?) {
    if let hiragana = string {
        print("「\(hiragana.character)」は平仮名です")
    } else {
        print("これは平仮名ではありません")
    }
}

// 結果をチェック
checkHiragana(string: thisIsHiragana)
checkHiragana(string: thisIsKatakana)

公式マニュアルに比べると複雑なように見えますが、初期化子内部で条件分岐しているだけです。

条件分岐は、「平仮名」と「それ以外」で分けています。

switch character {
    case "あ"..."ん":
        self.character = character
    default:
        return nil
}

条件分岐switch文の所で使ったinterval matchingと同じです。平仮名"あ"..."ん"ではプロパティに値をセットし、それ以外の場合はreturn nilで初期化失敗をトリガーしています。範囲演算子...を使うと、文字列の範囲指定も出来るのが良いですね。

「初期化の失敗をトリガーする」というのは「初期化の失敗を引き起こす」ということです。公式マニュアルでは「initialization failure is triggered」という部分です。「トリガー」とは「trigger」をそのままカタカナにしただけですが、和訳を当てると「引き金を引く」とか「きっかけとなる」とか「(事を)起こす」というような意味があります。

この条件判定があることで、平仮名以外の文字をパラメータに与えると初期化に失敗します。実際にインスタンスを生成すると分かりますが、試しにカタカナを与えた場合はnilになっていることが分かります。

// テスト用関数
func checkHiragana(string: Hiragana?) {
    if let hiragana = string {
        print("「\(hiragana.character)」は平仮名です")
    } else {
        print("これは平仮名ではありません")
    }
}

// 結果をチェック
checkHiragana(string: thisIsHiragana)
//"「あ」は平仮名です"と表示

checkHiragana(string: thisIsKatakana)
//"これは平仮名ではありません"と表示

テストを簡単にするために、インスタンスのチェック部分を関数化してます。関数にしたのは、optional bindingnilかどうかの判定をする部分です。

1つ注意しないといけないのは、生成したインスタンスの型はHiraganaじゃなくて、optionalsのHiragana?ということです。関数のパラメータを見てもらうと分かると思いますが、

func checkHiragana(string: Hiragana?) {

のように、パラメータの型はHiragana?(optionals)となっています。クエスチョンマークがついていないとコンパイルエラーになります。

先程、failableとnonfailableで「同じ」初期化子を作ることができないと言いましたが、実際に試してみると

//error: invalid redeclaration of 'init(character:)'

こんな感じのコンパイルエラーが出て怒られます。

列挙型(Enumerations)の場合

次は列挙型です。列挙型の方がfailable initializersと親和性が高いような気がします。というのも、列挙型の場合、自身が保有しているcaseプロパティをswitchで条件分岐させるのが前提だからです。したがって、それらと適合しない場合は「初期化失敗」という条件分岐にすれば良いことになります。

先程平仮名の構造体だったので、今度はカタカナを列挙型にしてみます。

// カタカナ列挙型
enum Katakana {
    case asound, ksound, ssound, tsound
    
    init?(symbol: Character) {
        switch symbol {
        case "ア"..."オ":
            self = .asound
        case "カ"..."コ":
            self = .ksound
        case "サ"..."ソ":
            self = .ssound
        case "タ"..."ト":
            self = .tsound
        default:
            return nil
        }
    }
}

let asound = Katakana(symbol: "エ")
if asound != nil {
    print("カタカナです")
}

// または
if asound == Katakana.asound {
    print("ア行のカタカナです")
}

let unknown = Katakana(symbol: "B")
if unknown == nil {
    print("カタカナではありません")
}

手抜きでタ行までしかありませんが、やりたいことは表現できていると思います。構造体の場合とほとんど同じですが、列挙型本体に「ア行」とかの情報を入れることが出来ますので、上で実行しているように

// または
if asound == Katakana.Asound {
    print("ア行のカタカナです")
}

列挙型のcaseプロパティを使って条件判定をすることも可能です。

Raw Valuesを持つ列挙型の場合

列挙型で「Raw Valuesを使った初期化」という項目がありましたが、実はそこで紹介した初期化子はfailable initializersです。その定義構文は

init?(rawValue:)

という形で、パラメータrawValueは与えたraw valuesの型と同じです。

先程の例を、raw valuesを与えて書きなおしてみようかと思いましたが、範囲演算子で条件判定しているので、これを直接raw valuesにすることは出来ません。例えば、カタカナを1文字ずつ対応した値に代入することで実装することは出来ます。

// Character型なので1文字ずつ定義すれば実装できる
enum Katakana: Character {
    case A="ア", Ka="カ", ...
}

初期化失敗の伝搬(Propagation of Initialization Failure)

Failable initializersは、別のfailable initializerへ初期化を「横に委譲(delegate across)」することが出来ます。横への委譲は同じ型で行われるので、クラス限定という訳ではなく構造体や列挙型でも可能です(値型の初期化子の委譲を参照)。クラスは継承できるので、サブクラスのfailable initializerにおいて、スーパークラスのfailable initializerに初期化を委譲(上に委譲、delegate up)することが出来ます。

重要なのは

失敗(return nil)した時点で、直ちに初期化をストップする(以降の初期化は実行されない)

ということです。

Propagationは「伝達」とか「伝搬」という訳が当てられます。ここでの(私個人の)イメージは、一旦初期化が失敗すると、nilが全てのプロパティに「伝搬」していく、という感じです。間違っているかもしれませんが、この「伝搬」の部分を直感的に理解する他のイメージがわきませんでした。

クラスで見るfailable initializer委譲の具体例

早速クラスを使って、failable initializerを委譲してみます。

// 粒子
class Particle {
    var name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name

        print("Particleの初期化: \(self.name)")
    }
}

// 元素
class Element: Particle {
    var atomicNumber: Int
    init?(name: String, atomicNumber: Int) {
        if atomicNumber < 1 { return nil }
        self.atomicNumber = atomicNumber
        print("Elementの初期化: \(self.atomicNumber)")

        super.init(name: name)
    }

    func show() {
        print("名前:\(name)、原子番号:\(atomicNumber)")
    }
}

構造は公式マニュアルと全く一緒ですが、このパターンだと色々作れそうです。順番に詳しく見ていきます。後でインスタンスを作ってテストすることを考えて、print()文を初期化子の最後に挿入しています。

文字列が空の場合、失敗フラグを立てる

init?(name: String) {
    if name.isEmpty { return nil }
    self.name = name
}

String型が持つisEmptyプロパティを使って、空白文字かどうか判定し、もし文字列が空なら初期化失敗、という処理を実行しています。これは簡単ですね。

Swift 2.2以前では、この構文ではコンパイルエラーになっていました。以前は、クラス限定ですが、初期化失敗の前に全てのプロパティを一度初期化しなければなりませんでした。したがって、

init?(name: String) {
    self.name = name
    if name.isEmpty { return nil }
}

という順序でなければ、コンパイルが通らなかったようです。

派生先でもfailable initializer

サブクラスは「元素」を表すクラスで、元素を特徴づけるのは「原子番号(atomic number)」です。原子番号は1からスタートするので、0以下の場合は初期化を失敗しないとマズイです(ちなみに、原子番号1の元素は水素です)。したがって、

init?(name: String, atomicNumber: Int) {
    if atomicNumber < 1 { return nil }
    self.atomicNumber = atomicNumber
    super.init(name: name)
}

のように、原子番号が1未満の場合は、初期化を失敗させます。その後、自分自身のプロパティをセットした後に、スーパークラスの初期化子に対して初期化を委譲(上に委譲)しています。Failable initializersの場合も、クラスの初期化子の委譲で見た2段階初期化のルールが適用されますので、先ず自分自身のプロパティを初期化してから上に委譲、という順番で初期化します。

Failable initializerの挙動を確認

実際にインスタンスを作って、failable initializersの挙動を確認してみます。

// 名前も原子番号もちゃんと入力した場合
let helium = Element(name: "ヘリウム", atomicNumber: 2)
//結果
//Elementの初期化: 2
//Particleの初期化: ヘリウム

名前と原子番号をちゃんと入力すると、サブクラスの初期化子の最後に実行されているスーパークラスの初期化子super.init(name: name)から、print()関数が呼びだされていることが分かります。

では、試しに名前を空にしてみます。名前だけが無い場合は、スーパークラスの初期化子で「初期化失敗」フラグが立つはずなので、少なくともサブクラスの初期化は実行されるはずです。

// 名前だけを空白にしてみる
if let missingName = Element(name: "", atomicNumber: 2) {
    missingName.show()
} else {
    print("名前を入力して下さい")
}
//結果
//Elementの初期化: 2
//名前を入力して下さい

結果を見ると、確かにサブクラスでのprint()関数からの出力は確認出来ます。一方で、スーパークラスからの出力は無く、インスタンス自体がnilになった場合の出力("名前を入力して下さい")が表示されています。この場合は、予想通りですが、スーパークラスの初期化子で「失敗」判定されて、ElementインスタンスであるmissingNamenilになっていることが分かります。

一方で、原子番号を0にした場合は、サブクラスの初期化の先頭で失敗フラグが立ちますので、ParticleElementクラスに埋め込んだprint()関数からの出力はありません。

// 原子番号を0にした場合
if let missingAtomicNumber = Element(name: "炭素", atomicNumber: 0) {
    missingAtomicNumber.show()
} else {
    print("原子番号には1以上の整数を入力して下さい")
}
//結果
//原子番号には1以上の整数を入力して下さい

Failable initializerの委譲

公式マニュアルに注意書きがありますが、failable initializerはnonfailable initializerに対して初期化を委譲出来ます。逆に、nonfailable initializerはfailable initializerを呼び出すことが出来ません。これは実際に試してみると分かります。

// Failable/nonfailable initializersの委譲
struct Sum {
    var a: Int
    var b: Int

    // nonfailable initializer
    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    // failable initializer
    init?(c: Int) {
        if c == 0 { return nil }
        self.init(a: c, b: c) // failableからnonfailableの呼び出しは可能
    }

    // これはコンパイルエラー
    // nonfailableからfailable initializerを呼び出すことは出来ない
    init() {
        self.init(c: 1)
    }
}

2番目の初期化子はfailable initializerで、nonfailable initializerに初期化を委譲しています。3番目の初期化子は、nonfailable initializerで2番目のfailable initializerを呼び出して初期化を委譲しようとしていますが、これはコンパイルエラーになります。

//error: a non-failable initializer cannot delegate to failable initializer 'init(c:)' written with 'init?'

少し考えると自明ですが、3番目のケースがもし可能だとすると、一見nonfailable initializerな初期化子が、初期化を失敗するという事故に近い処理を起こす可能性があります。それを防ぐためには、nonfailable initializerからfailable initializerの呼び出しをあらかじめ防いでおかないといけません。

余り良い実装ではないですが、2番目のnonfailable initializerでreturn nilの行をコメントアウトしてもコンパイルは通ります。Failable initializerは、初期化が失敗する可能性がある初期化子、という定義なので、return nilがない(初期化に絶対失敗しない)ような実装も可能です。

Failable Initializerのオーバーライド

Failableをnonfailableとしてオーバーライドすることは可能(逆は不可)

サブクラスのfailable initializerも、普通の初期化子と同様にオーバーライドすることが出来ます。面白いのは、

スーパークラスのfailable initializerを、サブクラスでnonfailable initializerとしてオーバーライドする

ことが出来るということです。この場合、サブクラスの初期化は失敗することがありませんが、スーパークラスを初期化すると普通に失敗する可能性があります。

公式マニュアルに注意書きがありますが、上記のオーバーライドの逆のケースは成り立ちません。つまり、スーパークラスのnonfailalble initializerを、サブクラスでfailable initializerとしてオーバーライドすることは出来ません。

"NOTE

You can override a failable initializer with a nonfailable initializer but not the other way around.”

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

表にすると

スーパークラスの初期化子 サブクラスがfailable サブクラスがnonfailable
Failable
Nonfailable ×

このようになります。この関係はオーバーライドだけじゃなくて委譲でも成り立ちます。Nonfailableからfailableへのオーバーライド(または委譲)は出来ない、という例外だけ覚えておくと良いかもしれません。ただし、後で紹介しますが、委譲の場合は例外(forced unwrappingを使うケース)があります。

Failable initializerのオーバーライドの具体例

先程の粒子クラスを少し改造して、実際に試してみます。

// 粒子
class Particle {
    var name: String
    init() {
        name = "粒子"
    }

    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name

        print("Particleの初期化: \(self.name)")
    }
}

// Failable initializerをオーバーライド
class MasslessParticle: Particle {
    // Nonfailable initializerをオーバーライド
    override init() {
        // Failable initializerに初期化を委譲(forced unwrapping)
        super.init(name: "質量なし")!
    }

    // 1. 条件分岐内に直接初期化子
    override init(name: String) {
        // 空白文字の場合
        if name.isEmpty {
            super.init(name: "名前がありません")!
        } else {
            super.init(name: name)!
        }
    }
}

let particle = MasslessParticle(name: "")

順番に見ていきます。

単純なforced unwrapping

スーパークラスであるParticleクラスは、nonfailable initializerのinit()と、failable initializerであるinit?(name: String)を持っています。したがって、先程述べた条件から、オーバーライドした初期化子のパターンは、

  • (1) Nonfailableなoverride init()
  • (2) Nonfailableなoverride init(name: String)
  • (3) Failableなoverride init?(name: String)

という3通りが可能です。

クラスMasslessParticleの1番目の初期化子は、(1)の初期化子に該当します。

// Nonfailable initializerをオーバーライド
override init() {
    // Failable initializerに初期化を委譲(forced unwrapping)
    super.init(name: "質量なし")!
}

スーパークラスの初期化子を呼び出して「上に委譲」しています。この時、委譲先の初期化子はfailable initializerですから、普通だとコンパイルエラーです。先程述べたように、

逆に、nonfailable initializerはfailable initializerを呼び出すことが出来ません。

という制約があるからです。ここでは、パラメータnameに手で文字列を与えていますので、初期化が失敗することはありません。したがって、forced unwrappingで強制的にoptionalsを外しています。こうすることで、nonfailable initializerからfailable initializerへ初期化を委譲することが可能になります。

条件分岐で実行時エラーを回避

サブクラスの2番目の初期化子は少し複雑です。

// 1. 条件分岐内に直接初期化子
override init(name: String) {
    // 空白文字の場合
    if name.isEmpty {
        super.init(name: "名前がありません")!
    } else {
        super.init(name: name)!
    }
}

これは上記リストの(2)に該当します。もっと単純化すると、

// これではダメか?
override init(name: String) {
    super.init(name: name)!
}

と書けそうですが、これは問題があります(コンパイルは通ります)。というのも、もしパラメータに空白文字が入った場合、super.init(name: name)nilになりますが、forced unwrappingしているので、実行時エラーを起こします。したがって、最初に書いたような条件分岐で空白文字を回避する必要があります。

色々書き方があると思いますが、ローカル変数を使ったり

// 2.  ローカル変数
override init(name: String) {
    var parameter = ""
    // 空白文字の場合
    if name.isEmpty {
        parameter = "名前がありません"
    } else {
        parameter = name
    }
    super.init(name: parameter)!
}

三項演算子を使ってカッコよく書くことも出来ます。

// 3. 三項演算子
override init(name: String) {
    super.init(name: name.isEmpty ? "名前がありません" : name)!
}

公式マニュアルによれば、スーパークラスのfailable initializerをサブクラスでnonfailable initializerとしてオーバーライドした場合、forced unwrapping以外「上に委譲」する方法がないようです。

“Note that if you override a failable superclass initializer with a nonfailable subclass initializer, the only way to delegate up to the superclass initializer is to force-unwrap the result of the failable superclass initializer.”

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

init!って何?

オプショナル型では、implicitly unwrapped optionalsというちょっと特殊なoptionalsがありました。これはforced unwrapping無しで、普通の型のようなアクセスが可能でした。

初期化子では、

// Implicitly unwrapped optionalsなfailable initializer
init!() {
  statements
}

のようにfailable initializerを定義することで、インスタンスをimplicitly unwrapped optionalsとして生成することが出来ます。

普通のfailable initializersと比較すると、委譲の自由度は高めで、init?init!間相互の委譲が可能です。

// init!のテスト
class ClassA {
    var a: Int
    init?(a: Int) {
        if a < 0 { return nil }
        self.a = a
    }
}

class ClassB: ClassA {
    var b: String

    // init! --> init?
    init!(a: Int, b: String) {
        self.b = b

        super.init(a: a<0 ? 1 : a)
    }

    // init? --> init!
    convenience init?(b: String) {
        if b.isEmpty { return nil }
        self.init(a: 2, b: b)
    }
}

また、nonfailable initializerからinit!の呼び出しも可能なようですが、実行時エラーを起こす可能性があるので注意が必要かもしれません。

まとめ

  • Failable Initializersは独自型のoptionalsを生成する初期化子(初期化が失敗する可能性がある初期化子)
  • 「初期化が失敗した状態」=「その型(のインスタンス)に値がない状態」=nil
  • Failable initializerはinit?で定義する
  • 「初期化の失敗」を発行するにはreturn nil
  • 初期化に失敗した時点で初期化を終了する。これは、初期化を委譲しても関係なく、委譲先で終了することもある
  • Failable initializersは普通の初期化子(nonfailable initializers)に初期化を委譲可能(逆は不可能、ただしforced unwrappingは例外)
  • Failable initializersのオーバーライドも可能。Failableをnonfailableとしてオーバーライドすることは出来るが、その逆は出来ない
  • Implicitly unwrapped optionalsを作りたい時はinit!初期化子を使う