2016/08/23 追記:Swift3.0でのErrorプロトコルについて(SE-0112)
2016/10/12 編集:@yusugaさんよりコメントをいただきPerfectの例についての記述を修正
2017/09/30 追記:Swift4についての記述を追加
はじめに
これまで、エラーハンドリングをしっかりしていなかったので改めて調べてみました。
備忘録として残し、「処理失敗したらとりあえずfalse
返す」みたいなことは卒業します。
また、Swift3.0も近づいてきているということもあり、これまでのSwift2.2のエラーハンドリングを振り返り、最後にSwift3.0で変わった点を記していきます。
Swiftの公式リファレンスを基にしていますが、自分なりにまとめた内容なので、語弊や間違った解釈があるかもしれません。そのような点があればご指摘ください
そもそもエラーハンドリングとは?
ググってみると「エラーが起きた時の対応処理」というような解釈がたくさん出てきますが、個人的にしっくりきたのは以下の文脈での使われ方でした。
ソフトウェアの中で発生するエラーは多岐にわたる。エラーには、代替策をとれば問題なく先へ進めるものや、プログラム自身が問題で修復できるものもある一方で、別のコンピュータへ役割を引き継いだり、人間の介入を待たなくてはならないものまで多種多様である。
~(中略)~
これらの**エラーに適切に対処すること(エラーハンドリング)**は非常に重要である。
引用:IPA ISEC セキュア・プログラミング講座:C/C++言語編 第6章 フェイルセーフ:体系だてたエラーハンドリング
つまり、正常な動作を阻害する挙動が生じた時に、それをエラーとし、そのエラーの内容に応じて適切にエラー状態を回復させる処理を行うことをエラーハンドリングというのだと思います。
また、Swift2.2の公式リファレンスでは、以下のように表現されています。
Error handling is the process of responding to and recovering from error conditions in your program.
訳すと、「プログラムのエラー状態に応じて、そのエラー状態を回復するプロセス」といった感じでしょうか?(間違っていたらご指摘ください、、)
エラーハンドリングのポイント
エラーハンドリングがどういうものかを確認したところで、
エラーハンドリングを行うにあたって、どのようなことが重要なのかが少し見えてきました。
(ポイントというか、前提みたいなものも含んでいますが、、)
- プログラマが、どこでどのようなエラーが発生しうるかを把握できていること
- 1を踏まえて、プログラム上でエラーの内容が事前に定義されている(判別可能な状態になっている)こと
- 2で判別可能な状態にされたエラーの内容に応じて、適切な処理が行われること
例えば、何か処理を行って、その処理が成功した時にはtrue
を、失敗した時にはfalse
を返すようなメソッドはよく見るかと思います。
このようなメソッドを呼んだ際に、false
が返ってきた時の処理が同一のもので対応出来る場合はこのような形でいいかと思います。
しかし、「処理が失敗した」という中にも、何が原因でどのように失敗したかによってリカバリーの処理を分けたい場合には、false
を返すだけではうまく対応できないでしょう。
このような時に以降で述べるような構文を使ってエラーハンドリングを行うと良さそうです。
ようやく本題ですが、Swiftでのエラーハンドリングを見ていきましょう。
Swift2.2でのエラーハンドリング
エラーを定義する
ErrorType
プロトコル
Swiftでは、エラーの内容をErrorType
というプロトコルに準拠した列挙型で定義することで便利にエラーハンドリングを行えるようです。
enum VendingMachineError: ErrorType {
case InvalidSelection
case InsufficientFunds(coinsNeeded: Int)
case OutOfStock
}
このErrorType
プロトコル自体には、何かプロパティやメソッドが定義されているわけではなく、空のプロトコルです。
空のプロトコルであるErrorType
プロトコルにわざわざ準拠させる理由は、明示的なプロトコルの準拠を行うことでエラーハンドリングのための定義であるということを誰にでも明確に指し示すことができるためです。
エラーを投げる
throw
文
throw
文を使うことで、エラーを投げることができます。
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
エラーが発生しうるメソッドの定義
エラーを投げるメソッドを定義するときには、次のように、メソッドの()
の後ろにthrows
を書きます。
func canThrowErrors() throws -> String
実装では、上記のthrow
文と組み合わせて、次のようにエラーを投げることができます。
※throws
でエラーを投げる可能性があることを宣言していても、必ずしもエラーを投げないといけないというわけではありません。
func canThrowErrors() throws -> String {
if 条件文 {
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
}
}
実装例
リファレンスの例を紹介します。
enum VendingMachineError: ErrorType {
case InvalidSelection
case InsufficientFunds(coinsNeeded: Int)
case OutOfStock
}
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func dispenseSnack(snack: String) {
print("Dispensing \(snack)")
}
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.InvalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.OutOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
dispenseSnack(name)
}
}
この例ではvend
メソッドでエラーが発生する可能性があることを宣言しています。
func vend(itemNamed name: String) throws
そして、guard
構文を使用して、列挙型で定義した3種類のエラーを投げています。
このvend
メソッドは、以下のようにエラーを投げない実装も考えられますが、
この場合、失敗は一律でfalse
が返ってくるので、どのguard
文でfalse
が返ったのか分からず、エラーに応じた処理ができなくなってしまいます。
func vend(itemNamed name: String) -> Bool {
guard let item = inventory[name] else {
return false
}
guard item.count > 0 else {
return false
}
guard item.price <= coinsDeposited else {
return false
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
dispenseSnack(name)
return true
}
こういった対応が適切でない場合は、前述のようにthrows
をつけてエラーを投げるようにした方が良さそうです。
エラーの伝搬
先ほどの実装例でのvend
メソッドを実行するメソッドを考えます。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
buyFavoriteSnack
メソッドは、中でvend
メソッドを実行しています。
このように、メソッド内の処理でエラーを投げるメソッドを実行する場合には、
- 後述の
try!
またはtry?
を使用するなど、エラーに対応する処理をメソッド内で行う -
try
のみを行って、エラーを伝播させる(そのメソッド自身も同じエラーを投げて、エラー処理を外側に任せる)
のどちらかのアプローチが必要になります。
上記例では、後者のアプローチを行っているため、buyFavoriteSnack
メソッドもvend
メソッド同様のエラーを投げます。
エラーを投げるInitializer
Initializerでも同様にエラーを投げることができます。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
エラーを検知して処理をする
これまでは、エラーを投げる側の実装を見てきましたが、ここからはエラーを受けとる側の処理を見ていきます。
do-catch構文
エラーハンドリングを行うときは、do
ブロックの中でtry
をつけてエラーを投げる可能性のあるメソッドを実行します。
そして、catch
ブロックでエラーのパターンマッチングを行い、それぞれにあった対応を行うことができます。
do {
try canThrowErrors() // エラーを投げる可能性のあるメソッド
// 成功した時の処理
} catch ErrorPattern { // エラーのパターンマッチング
// エラーの処理
}
こちらも先ほどの例で見た方がわかりやすいかと思います。
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
}
buyFavoriteSnack
メソッドをdo
ブロック内でtry
をつけて実行しています。
もしエラーが投げられた場合は、do
ブロックの下に続くcatch
ブロックで処理がされます。
例の場合、catch
ブロックが3つ設けられていますが、投げられたエラーにマッチするブロックの処理が実行されます。
その他の処理の仕方
上記のようにdo-catch構文を用いて、エラーの内容に沿った処理を行うこともできますが、それ以外に、以下のような処理も可能です。
try?
文
これは、try
でエラーが投げられた際に、エラーをオプショナルに変換して処理することができます。
次のx
とy
は同じ結果が得られます。
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
try!
文
try!
文は、エラーの伝播を停止します。
確実にエラーが投げられないと分かっている場合には、わざわざエラーハンドリングをする必要がないので、try!
文を使用して通常の処理のみを書いていくことができます。
ただし、try!
文で実行されたメソッドでエラーが投げられた場合には、実行時エラーになってしまうので、気をつけて使用した方が良さそうです。
let photo = try! loadImage("./Resources/John Appleseed.jpg")
Swift3.0でのエラーハンドリング
Swift3.0では、エラーを定義した列挙型を準拠させていたErrorType
プロトコルが~~ErrorProtocol
~~Error
プロトコルに変更されます。
==追記:2016/08/23=====
SE-0112でErrorProtocolではなく、Errorになっているようですね。
- https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md
-
https://github.com/apple/swift/commit/823c24b355017e3797c665c656b48fc61f01bc85
==追記ここまで==========
また、エラーハンドリングとは直接的に関係ないですが、Swift3.0では、型とプロトコルの名前以外はlowerCamelCase
で書くようにガイドラインが作成されています。
そのため、Error
に準拠した列挙型を作成する際には、その点の注意も必要です。
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
Swift3.0でError
プロトコルを使用している例 (2016/10/12時点)
例えば、Server-Side SwiftのフレームワークであるPerfect
でも、Swift3.0のコードとして、Error
プロトコルを使用しています。
Swift4 でのエラーハンドリング
Swift4ではSwift3からの変更はないので、これまで通り書くことが出来そうです。
公式のドキュメントは下記になります。
終わりに
以上、エラーハンドリングについて調べてみました。
正しくエラーハンドリングを行ってより良いプロダクトを作っていきたいですね。
間違っている点などありましたら、是非ご指摘ください。