エラーハンドリングはエラーが出るような状況に対応し、解決するためのプロセスです。Swiftは、実行時に回復可能なエラーをスロー、キャッチ、伝播、および操作するためのファーストクラスのサポートを提供します。
いくつかの処理は実行完了や完全な出力を常に保証するものではありません。オプショナル型は値が存在しないことを表すために使用されますが、操作が失敗した場合、コードがそれに応じて応答できるように、失敗の原因を理解しておくと役立つことがよくあります。
例として、ディスクの中のファイルを読み込み処理する作業を考えます。この作業に失敗する可能性はいくつか考えられます。「指定されたパスにファイルが存在しない」「ファイルはあるが読み取りが許可されていない」「ファイルがエンコードされていない」など。これらのエラー原因を区別・特定することで、プログラムがエラーを解決することが可能になりますし、ユーザがエラー状況を把握できるようになります。
Contents
エラーの表示とスロー
Swiftでは、エラーはErrorプロトコルに準拠した型の値として表現されます。この空のプロトコルは、準拠した型がエラー処理に使用できることを示します。
Swiftの列挙型は特に関連するエラー状態をモデリングするのに適しています。連想値を使用すればエラーの性質についての追加情報を伝えることができます。例えば、ゲームの中で自販機を運営する状況でのエラー状況の表現を以下のようにすることができます。
enum VendingMachineError: Error { case invalidSelection //←商品の種類が不適切 case insufficientFunds(coinsNeeded: Int) //←お金が足りない(連想値で足りない金額を表現) case outOfStock //←品切れ }
エラーのスローは予期しない状況が発生し、通常の実行フローを続けることが不可能であることを示します。エラーをスローするためにthrow文を使用します。下のコードは、自販機でコインが5枚足りないことを示すエラーをスローしています。例えば下のコードは自販機で5枚コインが足りないことを示すエラーをスロー(投げる)しています。
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
エラーハンドリング
エラーがスローされた場合、周囲のコードの一部がエラーの処理を担当する必要があります。たとえば、問題の修正、代替アプローチの試行、ユーザーへの失敗の通知などです。
Swiftではエラーハンドリングの四つの方法が用意されています。
工事中🏗
それぞれのアプローチを以下で説明します。
エラーが発生するとプログラムのフローが変わりますので、どこでエラーが発生したかを素早く察知することが重要です。エラーが発生する可能性がある箇所にtryキーワードを記述します。あるいはエラーが発生する可能性がある関数・メソッド・イニシャライザの前にtry?あるいはtry!変数を記述します。以下でこれらのキーワードについて説明します。
Throwing Functionsを使ってエラーを伝える
関数・メソッド・イニシャライザがエラーをスローすることを示すために、throwsキーワードを関数定義の引数部分の後に記述します。throwsキーワードが記述された関数をthrowing Functionと呼びます。戻り値の型注釈がある場合、以下のように、リターンアロー( -> )の前にthrowsキーワードを記述します。
func canThrowErrors() throws -> String func cannotThrowErrors() -> String
throwing functionは、関数内でスローされたエラーを、関数が呼ばれているスコープに伝えます。
NOTE
throwing functionのみがエラーを伝えることができます。非throwing functionは必ず関数内でエラーを処理しなければなりません。
下のsample1-1ではvend(itemNamed:)メソッドを持つVendingMachineクラスを定義しています。vendメソッドは、
- リクエストした商品が利用できない場合
- 品切れの場合
- 価格が所持金を超えている場合(お金が足りない場合)
上記に合致する場合に適切なVendingMachineErrorエラーをスローします。
sample1-1
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 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 print("Dispensing \(name)") } }
vendメソッドについて
(第一チェックポイント)まずプロパティinventoryがディクショナリ。vendメソッドは引数として商品名を受け取るが、受け取った(指定された)商品名がinventoryのキーに存在しない場合nilが返される。指定された商品名が存在する場合はinventory[name]はオプショナル型Optional<Item>型を返す。(15行目)
guard let
のオプショナルバインディングが使われているので、返されたオプショナル型をアンラップした値を定数itemにセットする。この場合itemにセットされるのはItem型。guard letのオプショナルバインディングを使用することで、18行目移行定数itemには値があることが保証される。
つまりVendingMachineクラスのプロパティinventoryのキーに存在しない商品名を指定(引数として渡す)した場合エラー
VendingMachineError.invalidSelection
がスローされる。スローされたらvendメソッド外に処理が移る。存在する商品名を指定した場合18行目移行が実行される。
(第二チェックポイント)19行目のguard文の条件がfalseの場合、つまり指定された商品のcountプロパティが0以下の場合、品切れということでエラー
VendingMachineError.outOfStock
がスローされる。スローされたらvendメソッド外に処理が移る。条件式がtrueの場合、つまり指定した商品のcountプロパティが1以上の場合22行目移行が実行される。
(第三チェックポイント)23行目のguard文の条件がfalseの場合、つまり所持金が指定した商品の単価より少ない場合、エラー
VendingMachineError.insufficientFunds(coinsNeeded: item.price –coinsDeposited)
がスローされる。スローされたらvendメソッド外に処理が移る。条件式がtrueの場合、つまり所持金が足りている場合は26行目移行が実行される。
全てのチェックポイントを通過した場合、
商品を購入したので所持金(coinsDeposited)が商品の単価分減らされる。
指定された商品の在庫数を1減らす。
販売完了メッセージ表示。
vendメソッドの実装は、guard文を使用することで、商品購入に必要な条件が満たされていない場合、適切なエラーがスローし、vendメソッド外に処理を移します。throw文は即座に制御をメソッド外に移しますので、商品購入に必要な条件が全て満たされた場合のみ商品が販売されます(3つ目のguard文のブロックより下に記述されている商品購入処理が実行される)。
vend(itemNamed:)はスローされたエラーを伝えますので、vendメソッドを呼び出すコードは伝えられたエラーを受け取って処理する必要があります。処理する方法として、
do-catch文を使用する
try?あるいはtry!を使用する
上記の方法でエラーを処理する必要があります。あるいはエラーの伝搬を続ける必要があります。
例えば、下のサンプルのbuyFavoriteSnack(person:vendingMachine:)関数もthrowing functionです。vendメソッドがスローしたいかなるエラーも、buyFavoriteSnack(person:vendingMachine:)関数が呼び出されたポイントへ伝えられます。
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) }
vendメソッドがエラーをスロー
↓
buyFavoriteSnackメソッド内に伝えられる。
↓
buyFavoriteSnack呼び出しスコープに伝えられる。
このサンプルでは、buyFavoriteSnack(person:vendingMachine:)関数は人名を受け取り、その人が好きなスナックをディクショナリfavoriteSnacksから探します。受け取った人名がfavoriteSnacksのキーにない場合は”Candy Bar”が選択されます。(nil結合演算子(??)についてはこちらをご覧ください。)
そしてvend(itemNamed:)メソッドを呼び出して指定された人物に対応するスナックを購入しようとします。vend(itemNamed:)メソッドはエラーをスローすることができますので、メソッド名の前にtryキーワードを記述して呼び出されています。
Throwing initializersはthrowing functionsと同様にエラーを伝えることができます。例えば、下記の構造体PurchasedSnackのイニシャライザは、その内部でthrowing function(vendメソッド)を呼び出し、vendメソッドから伝えられたエラーを呼び出し元に伝えることでエラーをハンドリング(処理)します。
struct PurchasedSnack { let name: String init(name: String, vendingMachine: VendingMachine) throws { try vendingMachine.vend(itemNamed: name) self.name = name } }
Do-Catch文を使用してのエラーハンドリング
doブロック内でエラーがスローされた場合、catch節と照合して、どのcatch節で処理するかを調べます。
以下はdo-catch文の一般的なフォームです。
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}
catchの後にパターンを記述してそのcatch節がどのエラーを処理するかを示します。パターンを記述していないcatch節は、あらゆるエラーと合致し、(パターンを記述していないcatch節内で)スローされ合致したエラーと、errorという名のローカル定数と結び付けられます。
例えばsample1-2では列挙型VendingMachineErrorの3つ全てのケースについて照会しています。
sample1-2
enum VendingMachineError: Error { 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: 2), "Chips": Item(price: 10, count: 4), "Pretzels": Item(price: 7, count: 11) ] var coinsDeposited = 100 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 print("Dispensing \(name)") } } 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) } var vendingMachine = VendingMachine() vendingMachine.coinsDeposited = 8 do { try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) print("Success! Yum.") } 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.") } catch { print("Unexpected error: \(error).") } // Prints "Insufficient funds. Please insert an additional 2 coins."
sample1-2では、buyFavoriteSnack(person:vendingMachine:)関数はエラーをスローする可能性があるので、try式で呼び出されています。エラーがスローされると、実行は即座にcatch節に移行します。そしてどのcatch節にエラーを伝えるべきかを決定します。どのパターンにも合致しない場合、最後のcatch節でエラーが捕捉され、そのエラーが、そのcatch節内で使用できるローカル定数errorと結び付けられます。エラーがスローされなければ、do文内の残りのコードが実行されます。
catch節は全てのdo節がスローした全ての有効なエラーをキャッチして処理しなければいけない訳ではありません。もし全てのcatch節でエラーを捕捉できない場合、その外側のスコープにエラーを伝えます。しかし外側に伝えられたエラーはどこかの囲まれているスコープ内で処理される必要があります。非throwing functionの中では、その関数内のdo-catch文でエラーをハンドルしなければなりません。throwing functionの場合は、その中のdo-catch文でエラーを処理するか、あるいはその呼び出し元でエラーを処理しなければなりません。もしエラーが処理されないままトップレベルスコープまで伝えられると、実行時エラーが発生します。
sample1-3
enum someError: Error{ case someCase } enum VendingMachineError: Error { 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: 2), "Chips": Item(price: 10, count: 4), "Pretzels": Item(price: 7, count: 11) ] var coinsDeposited = 100 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 print("Dispensing \(name)") } } var otherFlag=false var vendingMachine = VendingMachine() func nourish(with item: String) throws { do { if otherFlag{ throw someError.someCase } try vendingMachine.vend(itemNamed: item) } catch is VendingMachineError { print("Invalid selection, out of stock, or not enough money.") } } do { try nourish(with: "Beet-Flavored Chips") } catch { print("Unexpected non-vending-machine-related error: \(error)") } // Prints "Invalid selection, out of stock, or not enough money."
sample1-3では、nourish(with:)関数の中で、もしvend(itemNamed:)メソッドがVendingMachineError列挙型のいずれかのケースのエラーをスローした場合、nourish(with:)メソッドが「”Invalid selection, out of stock, or not enough money.”」というメッセージを表示して、エラーを処理します。vend(itemNamed:)メソッドからVendingMachineError以外のエラーをスローされた場合、nourish(with:)関数は呼び出し元にエラーを伝えます。そのエラーは62行目のcatch節で捕捉され処理されます。
エラーをオプショナル値に変換する
func someThrowingFunction() throws -> Int { // ... } let x = try? someThrowingFunction() let y: Int? do { y = try someThrowingFunction() } catch { y = nil }
もしsomeThrowingFunction()がエラーをスローすると、xとyの値はnilになります。エラーがスローされなかった場合はxとyの値はsomeThrowingFunction()の返り値となります。xもyもオプショナル型になる点に注意してください。ここではsomeThrowingFunction関数がInt型の値を返すので、xとyはどちらもInt?型です。
try?を使うと全てのエラーを同じ方法で扱いたい場合に、簡潔なエラー処理コードを記述することができます。例えば以下のコードでは、データ取得のいずれかの方法でデータが取得できた場合そこでデータを返し、全ての方法が失敗した場合nilを返します。
func fetchData() -> Data? { if let data = try? fetchDataFromDisk() { return data } if let data = try? fetchDataFromServer() { return data } return nil }
参考
https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html