Swift 2.0 の Error Handling について考えてみる

カジュアル Swift プログラミング

Swift 2.0 に Error Handling が新設されたと知って、どんな場面で使うのがいいのかなって思っていたんですけど、NSFileManager で実際に使われているのを見たら感じるところがあったので、それについて書き綴ってみることにしました。


Swift 2.0 の Error Handling ってどんな機会に使うんだろうと思いながら過ごしていたら、NSFileManager の contentsOfDirectoryAtPath:メソッド で縁があったので、そこから感じたことを記してみることにしました。

Error Handling

Swift 2.0 の Error Handling というのは、エラーの状況に応じて適切な回復手段を提供するための仕組みで、これまでの真偽値やオプショナルを使った方式のように、成功したか失敗したかだけでは物足りない場面をカバーできるもののようです。

また、NSError を使った Cocoa フレームワークのエラー処理を自然に扱えるようにデザインされたものという位置づけもあるようです。

Cocoa の Error Handling

NSFileManager の contentsOfDirectoryAtPath: に見るエラー送出の形は、今までは引数を使って NSErrorPointer でやりとりしていた NSError を引数から独立させ、正常系とは別のエラー系としてそのまま throw するという形になっているようです。

そのため、このメソッドを使ったときには try を使ってエラーの場合の処理を書くことが求められます。

let manager = NSFileManager.defaultManager()

do {

	let contents = try manager.contentsOfDirectoryAtPath(path)


}
catch let error as NSError {

}

こうすることで、ファイルからコンテンツを取得するときにエラーがあったとしても、それから続く行を飛ばして catch で指定したブロックの方が実行されます。

今までのような『事前に NSErrorPointer を用意しないといけない』みたいな具体的なコードが不要になるのも嬉しいですし、そもそもの『引数に渡した NSError の様子を見て、必要に応じて処理を飛ばさないといけない』みたいな気遣いが不要になるのもかなり嬉しいところです。

Handling しないことも許されている?

ちなみに今の Swift では catch ブロックで何も書かないことも許されています。

let manager = NSFileManager.defaultManager()

do {

	let contents = try manager.contentsOfDirectoryAtPath(path)


}
catch {

	// ここで処理を何も書かなくても OK の様子
}

このときは、暗黙的に ErrorType 型の error 変数でエラーを受け取ることになるのですけど、それを使った処理をなにもしなくても、コンパイルエラーになることはないようです。

安全性を重視する Swift にしては珍しいなと感じますけど、つまり逆に考えれば Error Handling ではその程度の重みのエラーを扱うものというイメージでいいのかもしれません。

Objective-C などでもあった例外であれば『キャッチしないと強制終了に行き着くエラー』みたいなもっと重たい意味が込められていると思うので、そんなところにも見据える先の違いが窺えるように思えました。

エラーが発生し得る場所

そしてこの Error Handling で興味深いのが、エラーが発生する可能性のあるところに try が記載されているというところです。

let contents = try manager.contentsOfDirectoryAtPath(path)

Objective-C の例外機構では @try-@catch で括って『そのなかのどこかで』例外が発生したときに @catch ブロックで処理をするようになってましたけど、Swift では『try が頭に書かれている関数やメソッドでエラーが発生するかもしれない』ことが分かります。


つまり try というキーワードが、ここで『エラーを想定する!』という、プログラマー自身の意思表示になるわけですね。

そんな、エラーを返すかもしれない関数やメソッドには throws キーワードが指定されているので、もしもそこで意思表示がされていないときには、コンパイラが丁寧に『エラーが発生するかもしれないことを忘れてないか』と教えてくれます。

func contentsOfDirectoryAtPath(path: String) throws -> [String]

そんなところにも、Swift の安全性を大事にする考え方、つまりは『例外を処理するための仕組み』ではなく『例外を生まないための仕組み』という位置づけが窺えるように思えました。

Swift では列挙型でエラーを扱う

Cocoa の NSError は特例として、Swift では基本的に列挙型をエラー型として扱います。

enum Failure : ErrorType {

	case NotExists
	case NotPermitted(String)
}

func action() throws {

}

これによって、どんなエラーが想定されるかがグループ化されて分かるため、エラーが起きたときの判断がかなりしやすくなります。また、Swift の列挙型は値を添えられるため、エラーによっては付加情報を添えることも簡単です。


このようにして用意したエラーは、実際の処理の中で do-catch で簡単に扱えます。

do {

	try action()


}
catch Failure.NotExists {

}
catch Failure.NotPermitted(let reason) {

}

ここでは、起こり得るエラーの数だけ catch を連ねて書いて行くことになるようです。このとき let を使って値付き列挙子から値を取りだすことも簡単にできます。

どんなエラーが発生する可能性があるかは把握しにくい

ここまで見てきて思ったのは、関数やメソッドがエラーを返すかもしれないことは判っても、どんなエラーが返されるかまでは、表面からは分からないところでした。

表面的には throws というキーワードしか現れないので、実装が見れればまだいいですが、そうでなければ善意で提供されるドキュメントでしか、どんなエラーが返されるかが分からないのが少し不安なところです。

最後に後始末をしたいとき

Objective-C の例外処理では @finally という、正常でもエラーでも、最後に必ず実行したい処理を書くという場所がありました。

Swift の場合は defer 文を使ってそれを実現します。


たとえばですけど FileStream というファイルの内容を読むクラスがあったとして、それが最後に close メソッドでファイルを閉じないといけなかったとします。ちなみに、イニシャライザにパスを渡してファイルを開き、開けなかったときはエラーを投げるようになっていたとします。

そんなときには、ファイルを開いた直後にでも defer 文で『ファイルを閉じる』処理を記載しておくことで do のスコープを抜けたときに、それを忘れずに実行してくれます。

do {

	let stream = try FileStream(path)

	defer {
	
		stream.close()
	}

	try action()


}
catch {

}

最後に処理したい内容を defer で『先に』書かないといけないのがちょっと気持ち悪いかなと最初は感じたんですけど、関連するもののすぐ下で『最後に必ずこれをする』ということが併記できるので、この書き方になれてしまえば、むしろ好ましそうに思えます。


そして、この defer の挙動で興味深いのが、この中で指定した内容は、正常系でも異常系でも do のブロックを抜けた直後、つまり catch のブロックを実行する直前に呼び出されるところです。

もう少し具体的にいうと『do ブロック内で最後に実行される』というところです。


つまり do ブロック内なので、そのブロックで定義した変数を操作できます。

もしも最終処理を書くための仕組みが、たとえば finally みたいな別のブロックで用意されていたとしたら、次のように、後始末をしたい変数は do ブロックの外に用意しなければいけなかったかもしれません。

// もしも Swift が do-catch-finally だったら、の参考例です。

var stream:FileStream? = nil

do {

	stream = try FileStream(path)

	try action()


}
catch {

}
finally {

	// こんな書き方ではなくてよかったな、というお話。
	stream?.close()
}

エラーをさらに先へ伝えたいとき

さて、ここまででおおよその Error Handling の使い方が窺えてきましたが、その場でエラー処理をしないための仕組みも用意されています。

ある関数 main が、エラーを発生する可能性のある関数 action を呼び出したとき、そこで発生したエラーを関数 main を呼び出した側に伝えたいときは、関数 main 自体にも throws をつけます。

func main() throws {

	try action()
}

内部で呼び出した関数のエラーを catch で処理しなければ、そのエラーが自身を通して呼び出し元へ伝えられます。このとき、エラーをキャッチする必要性がないので do-catch は不要です。

もちろん、そのときにエラーのいくつかを処理して解消したり、新しいエラーを追加で throw したりといったことも可能なようです。

エラーが起こるはずがないとき

エラーが起こるはずがないのにエラー処理を書くのはナンセンス、かもしれません。

ある関数を呼び出すときに、一般的にはエラーが起こる可能性はあっても、このアプリに限ってはエラーが起こるはずがないときは try! を使うことで catch するコードを省略できます。

try! action()

こうすることで、エラー処理を書くことなく、エラーを発生させるかもしれない関数を呼び出すことが可能です。このときも、エラーをキャッチする必要性がないので do-catch は不要です。


こうしたときに、なんらかの事情でエラーが発生したときには try! の行で即座に落ちてくれるので、安全にエラーを検知することができます。

もしも絶対に catch を書かないといけなかったとしたら『闇雲にすべてのエラーをキャッチしてもみ消す』みたいなことが往々に行われてしまいがちになりそうで、この try! という構文があってとても嬉しいです。

デバッグで心が折れそうになる場面のひとつに『どこでおかしくなったか分からない』があると思うので、発生したエラーに対してどう対処したらいいか分からないうちは、とりあえず積極的に try! を使って行きたいように思います。

積極的に『落としていく』ことについて

この辺りの考え方は人によって大きく分かれるところみたいですけど、自分的には積極的に落としていきたいところのように感じます。

安易にエラー処理を書くよりは強制的に落としてしまった方がいい。そうやってコードを書いていった方が、自己責任で下手にリカバリーするよりも、ひいては全体的に見たときに安全に動くものが作れるように思います。

そうなるためには『落とすことそのものは気持ちの悪いこと』と認識しつつ、かつ『落とすことそのものを受け入れる』気持ちの在り方が大切なようにも思えました。

特徴から考えられる利点

そんなことを加味しながら Error Handling の使いどころを考えてみると、コンセプトの通りですが、さまざまなエラー要因が考えられる中で、その要因をプログラマーに提供し、それを以ってエラーからの復帰を図るという場面になりそうに思えます。

オプショナルとの比較

Swift 1.2 までは、正しく値が取れたか否かを表現する一般的な方法として Optional<T> を使う方法がありました。

この最大の特徴は『値が存在しない』ことを表現できることですけれど、それが『なぜ存在しないのか』までは表現できませんでした。

func action<T>() -> T? {

}

// 値が取れたか、取れなかったか。
if let value = action() {

}
else {

	// 取れなかった理由は知らない。
}

そんな感じで語ってしまうと、簡素すぎて頼りない印象を持たれてしまうかもしれませんけど、明示的に値を取り出す必要のある Optional<T> は、値があるかどうかをその場で判断しないといけないため、想定外を排除できる大事な仕組みのひとつでした。

どちらかというとオプショナルは『値があるかないか』が大事であって、値がなかったときにエラーにするかは、プログラマーの判断に委ねる、その判断を強要できる、ものだったように感じます。


つまり、オプショナルは『そのとき値がなかっただけで、それがエラーになるとは限らない』、そんな表現方法とも言えます。

逆に考えれば、Error Handling では名前のとおり、はっきりとエラーであったことを伝えることが目的のものとも言えるのでしょう。

さらに言うと、オプショナルが『その値は取れなかったから、あとは各自で判断してね』という主張が込められているのに対して、Error Handling では『値の取得を試みたけど、処理の途中でエラーが起こったから値が取れなかった』ということを伝えるための仕組みとも言えそうです。

いわゆる Either 型との比較

それを補うために、きっと世の中では Either 型という『任意の成功値か、任意の失敗値を取る型』が作られ、広く利用されてきたのだと思います。

enum Result<T> {

	case Success(T)
	case Failure(NSError)
}

// 正常系とエラー系が両方とも戻り値で返される。
func action() -> Result<String> {

}

Swift 1.2 までは上記の書き方だとエラーになってしまうため Success(Box<T> ) という形で記載する必要があって無理を感じるところでしたけど、Swift 2.0 からは普通に書けるようになってこの書き方も有りなのかなと思いましたが、それと合わせて Error Handling という仕組みが登場しました。

enum Failure : ErrorType {

	case NotExists
	case NotPermitted(String)
}

// 正常系とエラー系が明確に分離されている。
func action() throws -> T {

}


Error Handling と比較して、いわゆる Either 型の欠点を敢えて探すとすれば『どちらが正常でどちらが異常なのかが表現上のものだけになる』というところのように思います。

そんな観点では Error Handling であれば、正常な戻り値とエラーとを別系統で明確に分離できていて、エラーを確実に判断できるというメリットがあるように思います。さらに Swift ではエラーを ErrorType な列挙型で表現することで、エラーがより具体的に表現されるという利点もあります。


とはいえ、先ほどの Either 型の例で挙げた Result が Failure を返したからといって、それが必ずしもエラーとして特別扱いするほどのものではない可能性もあります。

あくまでも自然な流れな中で、成功する場合もあれば、失敗する場合もある、そんなときにはいわゆる Either 型も悪くはないように思えました。

もし Either 型が、流れの中では正常系も異常系も対等に扱える場面と、エラー系は分離して特別扱いしたい場面と、そんな両方の場面が想定できる場合は、たとえば succeeded メソッドとかを作ってエラーだったらそれを投げるメソッドとかを作ってみるのも良いかもわかりません。

enum Result<T> {

	case Success(T)
	case Failure(NSError)
	
	func succeeded() throws {
	
		switch self {
		
		case .Success:
			return
			
		case .Failure(let error):
			throw error
	}
}

// 正常系とエラー系が両方とも戻り値で返される。
func action() -> Result<String> {

}

// 戻り値の列挙型の succeeded を呼んでエラー系は Error Handling 機構に変換する。
do {

	try action().succeeded()
}
catch {

}

なにもここまでしなくても BOOL を返す succeeded プロパティで十分のようにも思えますが、その場合だと succeeded が false を示したときに .Failure 列挙子からエラーを取り出すための仕組みが必要になってきたりするため、それも判断材料にしたいときには Result<T> に対して素直に switch 文を使うことになると思います。

そんなとき、switch を使って正常系と異常系をいっしょくたに判定するコードよりは Error Handling を使って分離したほうがわかりやすくなる、かもしれません。

Either 型のように二者択一の場合はそれほど問題にならないようにも思いますが。

例外処理との比較

それと try-catch と聞くと例外処理が思い起こされますが、Swift 2.0 の Error Handling では、そこまでの状況は想定していないことが見受けられます。


たとえば『数値を 0 で割ること』を考えてみます。

let y = x / 0

数値を 0 で割られたら答えが無いため、ここで処理を継続することはできなくなります。

このとき『じゃあ 0 で割ろうとしたときにどうするか』を考える必要に迫られますけど、この考えを迫るための手段として Error Handling を使うことは「ない」のかなと感じます。

enum OperationError : ErrorType {

	case DivideByZero
}

func divide(a:Double, b:Double) throws -> Double {

	guard b != 0 else {
	
		// b が 0 以外ではなかった場合、エラーを送出する。
		throw OperationError.DivideByZero
	}

	return a / b
}

こういう場面では『強制的に落とす』というのが、自分の中での Swift の安全な書き方とも言えるように感じました。

func divide(a:Double, b:Double) throws -> Double {

	guard b != 0 else {
	
		// b が 0 以外ではんなかった場合、強制終了する。
		fatalError("DivideByZero")
	}

	// そもそもここでちゃんと落ちるので guard 自体が不要かもしれない。
	return a / b
}

// もちろん、使う前に回避します。
if b != 0 {

	let y = divide(a, b)
	
else {

}	

自分の中でどうしてこういう使い分けが生まれてくるのかを考えてみたとき、もしかすると、先に考えておくべきなのか、実行時のその時々にならないとわからないのか、そこで違いが生まれてくるように思いました。

数値が 0 で割れないという出来事は普遍的なことなので、そういう場面が考えられるなら最初から考えておかないといけません。

それに対して、ファイルを読み込もうとしたらダメだったみたいな、当日の環境に左右されることは、そのときになって初めて判断できる出来事です。そうしたときに適切な対処をしたければ、失敗した理由を伝えて、プログラムによる判断を仰ぐしか方法がありません。

Error Handling のコンセプトのひとつとして掲げられている事柄に『エラーの内容を伝えて、状況をリカバリする』というのがありますが、まさにこれが今回の温度差、つまりは使いどころの判断材料なのかもしれません。


ちなみにここを Optional で表現しても、弱々しくて済し崩しになりそうな印象でした。

きっと 0 で割ると例外的な結果が得られるということの重みと、エラーかどうかは各自で判断してねという主張の重みとのバランスが取れていないところからくる、温度差なのかもしれません。

func divide(a:Double, b:Double) -> Double? {

	if b != 0 {
	
		return a / b
	}
	else {
	
		return nil
	}
}

例外

Swift では『それがどうにもならない』ということを表現するのに fatalError 関数が使えます。

これにテキストを添えて実行すると、そこでプログラムが強制終了して、その理由がテキストで出力されるというものですけど、想定できないケースはもちろん、事前にその値を取るはずがない、ここへ来るまでに取り除けるというようなときには、とりあえず fatalError を使うのがいちばん安全なのかなと自分は感じています。


想定できないケースというのは、たとえば値を受け取ったときに、それが nil のはずがなかったのに、なんらかの理由で nil になっていた、みたいなところでしょうか。

// getValue 関数が nil を返す可能性があるとき
let value:String = getValue()

このような場面は Swift ではオプショナルという仕組みのおかげで、たぶんないと思うのですけど、同じような場面で例えば『ある要素が配列内のどこにあるか』を取るときに『事前にその配列内にぜったいに入れた要素を探そうとしたときに、見つからなかった』というものもあると思います。

array.append(element)

let index:Int = array.indexOf(element)!

配列の indexOf メソッドは、オプショナルな Int? を返しますけど、ぜったいに配列内にあるもので探したなら普通は見つからないことは有り得ないので、ここでは『値がなかったときは fatalError』と同等の ! で処理してしまうのが、適切なように感じます。


念のためちゃんとチェックするという考え方もあると思いますが、有り得ない場合を考えるにしては冗長で、ここのコードを理解しようとしたときに「読む」必要に迫られます。

そもそも、今回みたいな「ない」はずの値が見つかったときに、そこから正常系へリカバリーを図ったり、適切なエラー処理をして正常系に戻すとなると、どんな場合を想定してコードを書いているのか、そもそもそこまでの努力に見合った働きをするコードになるか疑問に思えてきます。

array.append(element)

if let index = array.indexOf(element) {

	// 普通に考えれば、ほぼ間違いなくこのブロックが処理されます。
}
else {

	// ここでなんらかの復帰を図るための処理を書いたとしたら、そもそもそのコードが必要なのか、考える価値はありそうです。
	// ここで fatalError を使って具体的な理由を出力するのも良さそうですが、少し冗長にも思えます。それなら if 文を使わずコメント文を添えれば十分かもしれません。
}

まとめ

これまでの話ででてきた Swift のエラー処理的な機能について、印象を整理してみると次のようになりそうな気がします。

手段 意思
Optional<T> 単純に『有るか無いか』を提示する
Error Handling ここで起こり得るエラーを伝えて対処を迫る
fatalError 問題を起こす場面を事前に必ず摘み取らせる

これらの意思を伝える相手は『それを使うプログラマー』です。

これらを返す機能を使ったときには、そういう意図で伝えられていることを加味して組むと良い感じになりそうですし、これらを返す機能を作るときにも、こういったイメージで使いどころを判断すると良い感じになりそうに思えます。

例外との距離感

ちなみに Objective-C の例外は『対処できない問題が発生したときに、外の誰かに委任する』という感じに見えます。相手に対処を求めるというより、責任を放棄するといった方がしっくりくるように思えました。

そんな放任的な例外処理とは違って Error Handling は、その場面で『想定できるケース』を事前に提示して、それに対する回復策を取ってもらうことが目的のように感じます。


そういう考え方のときには、エラーでたとえば Failure.Unknown というようなケースを取るより、有りえないことが起こったなら fatalError で落とすくらいの気持ちで使うのが良いのかもしれません。

もっとも、そうしてしまうと今度はリカバリーできる機会が失われるので、提供側では普通に Failure.Unknown という選択肢を用意して、それをキャッチした側が必要に応じて fatalError するという方が、適切なこともあったりしそうです。


そんな風にして、エラーを知らせるためだけでなく、エラーを受け取った相手がどんな風にそれを処理してくれるかを、ひとつ先だけ、つまりいちばん近い呼び出し元のことだけを考えて使ってみるとちょうど良いかもしれませんね。

そこがちょうど、例外 (Exception) か、エラー (NSErrorPointer) か、どちらが適切になるかのちょうど良い距離感なのかもしれません。

Error Handling の使い所

最後に、これまでのおさらいになりますが、そんなことを踏まえて使い所を考えてみると、使いどころのイメージがなんとなく見えてくるように思います。

Error Handling は、次のような状況下で使うのに向いているように思えます。

  1. 目的に対して、成功と失敗が明確なとき
  2. 正しく使えば成功することが期待されるとき
  3. 失敗したときに幾つかの理由が考えられるとき
  4. 原因に応じて正常系への復帰の道を与えたいとき

長々と書くなら、目的を達成できるかが、実行する環境やタイミングで異なるときで、そのとき、どんな理由で目的が達成できないかを(いくつか)想定できて、達成できなかったときに、どの理由で出来なかったか特定しにくくて、でも本来であればその目的が達成できて、もし目的が達成できなかったときには処理の流れに大きな方向転換が求められて、代替の処理を実行する(リカバリの)機会を与える余地をつくる必要があるか、そんな機会を与えるときに、詳細な情報を提示する必要があるか。

そんなところが Error Handling が適切かどうかの判断材料になるのかなって、今のところは感じています。

まとめ

そういう風に考えてみると、目的から見て処理が成功したときと失敗したときが明らかで、失敗したときにアプリを操作しているユーザーに状況を伝えて『どうするか』を訪ねるくらいの、なんらかの方向転換を迫られるくらいの出来事にこそ Error Handling は向いているのかもしれません。

逆に言うと、値が取得できたか出来なかったかみたいなそれ自体が必ずしも失敗とは言えない場合や、理由もプログラマーの範疇だけで解決できる事柄であれば Error Handling は使わないでも済みそうにも思えます。

オプショナル

たとえば、配列の中から値を検索したときにそれが見つからなかったとして、それが失敗かどうかは、機能を提供する側からは分かりません。こういうときはオプショナルで十分です。

例外

また、割る数に 0 が渡ってくる可能性があるときに、方向転換を余儀なくされることは確かでしょうけど『割る数に 0 が渡されたけど、どうする?』なんてユーザーに聞くものでもないでしょう。そもそも割り算で失敗するとしたら、原因を想像するのも簡単です。そういったときにわざわざプログラマーに Error Handling で異常系統の処理に切り替えてエラーの詳細を伝えるというのは、いささか大げさなように感じました。

Either

それと似た考え方で、たとえば Web API からデータを取得するようなときなら、さまざまな理由でデータの取得に失敗する可能性が普通にありますし、そもそも Web だと取得できないことも自然にありがちなので、その想定程度によっては、Either 型を使って正常と異常とを対等に扱うくらいでも、ちょうど良いように思えました。

または、目的が「Web API から応答をもらう」なので「OK でも Error でも目的は達成された」、つまり正常という捉え方でも良いかもしれません。そういう考え方のときには「応答がない」ことをエラーで表現するのが適切かもしれません。

もしかすると後者の方が、考え方としては適切なのかもしれません。

Error Handling

逆に、コンパイラみたいな『コンパイルする』という大きな目的の中で、コンパイルに失敗したときに『失敗した』しか教えてもらえなかったとしたら、もうどうにもなりません。失敗した原因も、状況や揃えられたデータによって様々なので、その原因を呼び出し元に伝えてあげれば、目的を達成するための方法を模索してくれる可能性が出てきます。

コンパイラくらいの規模でなくても、ファイル読み込みや JSON 等のパーサーなど、こんな規模感のときにも Error Handling が活躍してくれそうです。