書いて覚えるSwift入門

第10回例外を避ける

唯一変わったのは、そのすべて

「唯一変わったのは、そのすべて⁠⁠。iPhone 6sのキャッチコピーですが、むしろそれはSwiftにこそふさわしい一言ではないかというぐらいSwiftは大きく変わりました。前回はそれを広く浅く紹介したのですが、今回からはそれぞれの変化を深く見ていきましょう。

Type?

Swiftの最大の特長は何かと問われたら、筆者はOptional型の多用だと答えます。本連載を最初から追いかけてくださっている読者の皆さんは納得していただけると思いますが、そうではない読者のために、ここで一度おさらいしておきましょう。

次のようなDictionaryがあったとします。

var supportedLanguages = [
    "C" : 1,
    "ObjectiveC" : 2,
]

次はなんとprintするでしょうか?

print(supportedLanguages["C"])

1ではなく、Optional(1)ですね。では次は?

print(supportedLanguages["Swift"])

nilとなります。

今度はOptional(1)ではなく1となります。この挙動を、型に着目して追っていきましょう。まずsupportedLanguagesの型は[String:Int]です。つまりStringを添字にすると、対応するIntが返ってくるデータ型なのですが、その中にないStringを添字には何を返したらよいでしょう? クラッシュするか「何もない」を何らかの形で返すかのどちらかということになります。Swiftが採用したのは後者でした。この「何もない」のがnilで、nilか値を返す」のがOptional型です。つまりsupportedLanguages[k]の型は、IntではなくOptional<Int>つまりInt?ということになります。

ところでSwiftには、enumがあります。Optional型をenumで表現するとどうなるでしょうか? こんな感じでしょうか。

enum Optional<T> {
  case Nil
  case Some(T)
}

Swiftの実装は、まさにそのようになっています。[String, Int]が実はDictionary<String,Int>の構文糖衣であるように、Int?というのはOptional<Int>の構文糖衣に過ぎないのです。

There's more than one way to fail

以上を踏まえて、次を見てみましょう。

var language = "C"
if let i = supportedLanguages[language] {
    print(i)
} else {
    print("Swift is not supported");
}

Optional(1)ではなく、1と表示されます。iの型はInt?ではなくIntで、ifに続く{}の中では100%例外なくiはIntであることが保証されている一方、elseに続く{}の中ではsupported Languages[language]nilだったことが100%例外なく保証されているわけです。これがSwiftにおけるエラー処理の基本でした。Optional(と型変数)導入により、静的な型でもDictionaryのような動的に扱いたい型の扱いが動的言語なみに楽になったのです。

しかし、実際には「うまくいかない」だけではうまくいかないケースは少なくありません。⁠何がどううまくいかなかった」かによって、処理を変えたいケースも多いのです。たとえば「軽い」エラーならデフォルト値を代わりに使って続行し、⁠重い」エラーならプログラムを終了する。そういった場合、どうしたらよいのでしょう?

SwiftのOptionalがenumで実装されていることを知っていれば、次のようなSuperOptionalを定義してしまえばその問題は解決しそうです。

enum SuperOptional<E,T> {
    case Error(E)
    case Some(T)
}

func handleSuperOptional<E,T>(so:SuperOptio
nal<E,T>) {
    switch(so) {
    case let .Some(i):
        print(i)
    case let .Error(s):
        print("Error:\(s)")
    }
}

var so:SuperOptional<String, Int> =
.Some(42)
handleSuperOptional(so)
so = .Error("Not a number")
handleSuperOptional(so)

ところが、Swift 1では型変数を複数持つ総称型enumはサポートしていなかったのです図1⁠。

図1 総称型enumの動作
図1 総称型enumの動作

見てのとおりSwift 2では期待どおり動いていますが、Swift 1.xではコンパイラがクラッシュしてしまいます。

Swift 1におけるOptionalenumで実装されていましたが、Swift 2におけるtrycatchもenumによって実現されています。

give it a try

というわけでSwift 2のtry catchを実際に使ってみましょう。ここでは例題として、⁠クラッシュしない配列」を実装してみます。

SwiftのArrayの要素に範囲外の添字を与えると、問答無用でクラッシュします。

var ary = [0,1,2,3]
ary[4] // ここでクラッシュ

これはDictionaryとは異なる振る舞いです。

var dict = [0:0, 1:1, 2:2, 3:3]
dict[4] // nil

問答無用でクラッシュする代わりに、何らかのエラーを返すにはどのようにしたらよいでしょうか?

Swift 2では、まずどんなエラーを返すかを定義します。

enum ArrayError : ErrorType {
    case RangeError
}

見てのとおり、エラーはErrorType型を継承したenumです。次に、エラーを起こしうるメソッドを次のように定義します。

extension Array {
    func valueAtIndex(i:Int) throws ->
Element {
        if self.count <= i {
            throw ArrayError.RangeError
        }
        return self[i]
    }
}

通常のfuncと異なる点は2つ。1つは->の前にthrowsというキーワードが追加されていること、もう1つは範囲外であることを検出したうえで、その場合はthrow ArrayError.Range Errorしていること。

あとはこれを使うだけ。

var ary = [0,1,2,3]
do {
    var v:Int
    v = try ary.valueAtIndex(0)
    v = try ary.valueAtIndex(4)
} catch {
    print("Array out of range")
}

たしかに今度はクラッシュせず、⁠Array out of range⁠と表示されるようになりました図2⁠。ここでコードを見てみましょう。まず、実行ブロックがtryではなくdoで始まっています。そしてtryary.valueAtIndex()の前についています。tryを取り除くとどうなりましたか?

図2 trycatchのエラー
図2 trycatchのエラー

Java(Script)と同様、catchは特定のエラーだけを捕まえることもできます。たとえば次のようにコードを書き換えてみましょう。

enum ArrayError : ErrorType {
    case OutOfBounds
    case NegativeBounds
}

extension Array {
    func valueAtIndex(i:Int) throws ->
Element {
        if i < 0 {
            throw ArrayError.NegativeBounds
        }
        if self.count <= i {
            throw ArrayError.OutOfBounds
        }
        return self[i]
    }
}

var ary = [0,1,2,3]
do {
    var v:Int
    v = try ary.valueAtIndex(0)
    v = try ary.valueAtIndex(-1)
} catch ArrayError.NegativeBounds {
    print("Array Index must be zero or
lager")
} catch ArrayError.OutOfBounds {
    print("Array Index too large")
} catch {
    print("Unknown Error")
}

例外を例外扱いしないSwift

さらにSwiftならではの特長として、catchでまとめて捕まえるのではなく、if letguardで捕まえることもできます。

if let v = try? ary.valueAtIndex(4) {
    print(v)
} else {
    print("Array out of range")
}

もしくは

guard let v = try? ary.valueAtIndex(3) else
{
    print("Array out of range")
}

まとめると、次のとおりとなります。

  • TypeErrorを継承したエラー型を定義
  • エラーを起こしうるfuncには->の前にthrowsをつける(エラーを起こしたら定義したエラーをthrow
  • エラーを起こしうる関数/メソッドはtryする(エラーはcatchだけではなくif letguardで使うこともできる)

このようにSwift 2のtry catch機構は、Java(Script⁠⁠?のそれと比べると少し面倒ですが、例外ではなく飽くまで一般的なデータ型であるenumの自然な拡張として実現されている点が実に特長的です。構文の視点で見るとdo catch「例外処理」ですが、型の視点で見ると飽くまでで普通の処理。Swift 2に合わせて改定された[The Swift Programming Language]でも、⁠例外」⁠exception)という言葉を避けて飽くまで「エラー処理」⁠Error Handling)としているのもそのためでしょう。

次号は

「なるべく一般的かつ包括的に⁠⁠。それがさらに反映されているのが、Protocol Oriented Programmingという新たなスローガンでしょう。

次回は、いよいよこのProtocol Oriented Programmingを取り上げます。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧