【Swift】エラー処理構文の使い方。do-catchブロックを使ってエラーハンドリングを行う。(Swift 2.1、XCode 7.2)
エラーハンドリングとは
プログラム実行中に問題が発生して正常終了できなかった場合に、復帰させる処理を施すことをエラーハンドリングという。
例えば、下図のようにインスタンスAからインスタンスBのメソッドを呼び出したときに、インスタンスBのメソッドの処理でデータ不正や入出力の問題によりメソッドを正常終了できない場合、インスタンスAに対して戻り値や正常終了を返すのでは無く、エラーを投げる。
インスタンスAはインスタンスBから投げられたエラーを受け取ってプログラム実行を継続するための処理を行うといったことだ。
エラー処理構文を使ってエラーハンドリングをする
では実際にエラーハンドリングを行うサンプルプログラムを作ってみよう。
まず、以下のコードのようにエラーの種類を定義したEnumを作成する。Enumには必ずSwift標準のErrorTypeというプロトコルを適用する必要がある。エラーの名前に決まりは無く、例では数値が低すぎるエラー「LowPrice」、数値が高すぎるエラー「OverPrice」を定義した。
1 2 3 4 5 |
enum PriceError : ErrorType { case LowPrice //数値が低い case OverPrice //数値が高い } |
次にエラーを投げる可能性のあるメソッドを作成する。以下のコードのように引数と戻り値の間にthrowsを記述すると「このメソッドはエラーを投げる可能性がある」ということを意思表示したことになる。
1 2 3 |
func メソッド名(引数名:引数の型, ...) throws -> 戻り値の型 { 処理 } |
以下のコードは、エラーを投げる可能性のあるクラスの例。値上げメソッドが呼ばれたときに、引数が0以下の場合にLowPriceのエラー、1000以上の場合にOverPriceのエラーを投げるメソッドがある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* ** 商品クラス(エラーを投げる可能性がある) */ class Shouhin { var price = 1000 //価格 //値上げメソッド func addPrice(price:Int) throws{ if(price <= 0) { //引数が0以下の場合、エラーを投げる。 throw PriceError.LowPrice } else if( price >= 1000 ){ //引数が 1000以上の場合、エラーを投げる。 throw PriceError.OverPrice } else { self.price = self.price + price print("値上げしました。値上後\(self.price)円") } } } |
次に、メソッドを呼び出す側のクラスを作成する。以下のコードのようにメソッド呼び出しの先頭にtryを記述し、それをdoブロックで囲む。tryを記述することで「今から呼び出すメソッドはエラーを投げてくる可能性があります。」ということを意思表示したことになる。
doブロックの下のcatchブロックでエラーの種類ごとに必要な処理を記述する。どのエラーの種類にも当てはまらないエラーの場合はcatchのみのブロックの処理が実行される。
1 2 3 4 5 6 7 8 9 |
do { try メソッド呼び出し } catch エラーの種類 { 処理 } catch エラーの種類 { 処理 } catch { 処理 } |
以下のコードは、商品クラスのインスタンスの値上げメソッドを呼び出すクラスの例。メソッドを呼び出したあとにエラーが返ってきたら、エラーの種類ごとのエラーメッセージを出力する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* ** 商品管理クラス(商品クラスのメソッドを呼び出す。) */ class Manager { func callPriceIncrease(sho:Shouhin, num:Int){ do { //商品の値上げメソッドを呼び出す。 try sho.addPrice(num) } catch PriceError.LowPrice { print("0円以下を値上げすることはできません。") } catch PriceError.OverPrice { print("1000円以上を値上げすることはできません。") } catch { print("不明なエラーが発生しました。") } } } |
上記で作成したクラスを実行すると以下のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//商品クラスと商品管理クラスのインスタンスを生成 var sho = Shouhin() var manager = Manager() //メソッドを呼び出す。 manager.callPriceIncrease(sho, num:1200) manager.callPriceIncrease(sho, num:0) manager.callPriceIncrease(sho, num:500) //実行結果 //1000円以上を値上げすることはできません。 //0円以下を値上げすることはできません。 //値上げしました。値上後1500円 |
エラーにエラーコードやメッセージを付加する
エラーの種類を全部名前で定義している場合ではないときは以下のように定義すればエラーの種類にコードや文字列を持たせることができる。
1 2 3 4 5 6 7 8 9 |
/* ** エラーの種類にコードや文字列を持たせる。 */ enum エラー名 : ErrorType { case エラーの種類(Int) case エラーの種類(String) } |
以下のコードはエラーの種類にコードを持たせた例。catchブロックの引数でコードを受け取り、処理の中で使うことができる。文字列の場合も同様に引数で受け取れる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/* ** エラーコードを利用する。 */ //エラーの種類にエラーコードを利用する。 enum NumError : ErrorType { case ErrorCode(Int) } //数値チェッククラス class NumCheck { func checkNum(num:Int) throws -> Bool { switch num { case 100: throw NumError.ErrorCode(111) case 200: throw NumError.ErrorCode(222) case 300: throw NumError.ErrorCode(333) default: return true } } } //テスト実行 do { var test = NumCheck() try test.checkNum(100) } catch NumError.ErrorCode(let code) { print("エラーコードは\(code)") } //実行結果 //エラーコードは111 |
エラーの伝達
tryをつけてメソッドを呼び出したときの呼び出し先でさらにtryをつけてメソッドを呼び出すことができる。
例えば、下図のようにメソッド呼び出しを繋げていった結果、インスタンスDのinputメソッドがエラーを投げた場合、まずインスタンスCに対してエラーが投げられる。
しかし、インスタンスCにはdo-catchブロックが無いので、インスタンスDから投げられたエラーはインスタンスCを素通りして、インスタンスBに渡り、インスタンスBにもdo-catchブロックが無いので素通りする。そして最終的にインスタンスAのcathブロック内の処理が行われることになる。
上図のイメージを実際にコードにすると以下のようになる。クラスDから投げられたエラーがクラスAまで伝達され、クラスAのcatchブロックで処理されたことが分かる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* ** エラーの伝達 */ //エラーの種類 enum NumError : ErrorType { case WrongNumber } //クラスA class TestA { func test() { do { let testB = TestB() try testB.get() } catch { print("エラーが発生しました。") } } } //クラスB class TestB { func get() throws { let testC = TestC() try testC.sum() } } //クラスC class TestC { func sum() throws { let testD = TestD() try testD.input() } } //クラスD class TestD { func input() throws { throw NumError.WrongNumber } } //クラスAのスンスタンスを生成して実行 var test = TestA() test.test() //実行結果 //エラーが発生しました。 |
メソッド呼び出しをdo-catchブロックで囲んだが、エラーを受けたときにcatchブロックに該当するエラーが無かった場合は、エラーは素通りして呼び出し元に伝達される。
例えば、以下のコードはインスタンスBからインスタンスCのinputメソッドを呼び出したときにインスタンスCからNumberFormatErrorが投げられてきたが、catchブロックにはInputErrorに対する処理しか記述されていないので、エラーが呼び出し元のインスタンスAにエラーが伝達して処理される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/* ** catchブロックに該当するエラーが無いときのエラーの伝達 */ //エラーの種類 enum NumError : ErrorType { case NumberFormatError //数値フォーマットエラー case InputError //入力エラー } //クラスA class TestA { func test() { do { let testB = TestB() try testB.get() } catch NumError.NumberFormatError{ print("数値フォーマットエラーが発生しました。") } catch { print("不明なエラーが発生しました。") } } } //クラスB class TestB { func get() throws { do { let testC = TestC() try testC.input() } catch NumError.InputError{ print("入力エラーが発生しました。") } } } //クラスC class TestC { func input() throws { throw NumError.NumberFormatError } } //クラスAのスンスタンスを生成して実行 var test = TestA() test.test() //実行結果 //数値フォーマットエラーが発生しました。 |
最後に必ず実行させたい処理がある場合
エラーが発生してもしなくても最後に必ず実行したい処理がある。例えば、ファイルの入出力でエラーが発生したときのファイルをクローズさせる処理などだ。
そんなときは、以下のコードのようにdeferブロックを利用する。deferブロックの中に書かれた処理は外側のブロックを抜けるときの最後に必ず実行される。
例えば、以下の例ではインスタンスBのinputメソッドを呼び出す前にdeferブロックでメソッドを呼び出したあとに実行する処理を記述する。「テスト終了」、「テスト開始」の順番でコードを記述しているが、実行してみると「テスト開始」、「テスト終了」という順で表示され、deferブロックの処理が最後に行われていることが分かる。
注意点は、defer文はエラーが発生する可能性があるメソッドを呼び出す前に記述しなければならない点である。例えば、以下の例のinputメソッドの後にdeferブロックを移動するとその処理は実行されない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/* ** deferブロックを使って、エラーが発生しても最後に必ず実行する処理を記述する。 */ //エラーの種類 enum NumError : ErrorType { case InputError //入力エラー } //クラスA class TestA { func test() { do { defer { print("テスト終了") } let testB = TestB() print("テスト開始") try testB.input() } catch { print("エラーが発生しました。") } } } //クラスB class TestB { func input() throws { throw NumError.InputError } } //クラスAのスンスタンスを生成して実行 var test = TestA() test.test() //実行結果 //テスト開始 //テスト終了 //エラーが発生しました。 |
また、こんな使われ方がされるかは微妙だが、deferブロックを複数定義した場合は、あとに定義したほうから順番に実行される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* ** 複数のdeferブロックを定義した例 */ //エラーの種類 enum NumError : ErrorType { case InputError //入力エラー } //クラスA class TestA { func test() { do { defer { print("テスト1") } defer { print("テスト2") } let testB = TestB() print("テスト3") try testB.input() } catch { print("エラーが発生しました。") } } } //クラスB class TestB { func input() throws { throw NumError.InputError } } //クラスAのスンスタンスを生成して実行 var test = TestA() test.test() //実行結果 //テスト3 //テスト2 //テスト1 //エラーが発生しました。 |