見出し画像

Swiftでプログラミング- Error Handling

エラー処理とは、プログラム内のエラー状態に対応し、そこから回復するプロセスです。Swiftは、実行時に回復可能なエラーを投げたり、キャッチしたり、次に伝えたり、操作したりするための最高の機能を提供します。

いくつかの操作は、常に実行を完了したり、有用な出力が保証されていません。Optionalは、値がないことを表現するために使用され、失敗の原因をわかりやすくします。

例えば、ディスク上のファイルからデータを読み込んで処理するというタスクを考えてみましょう。ファイルが指定されたパスに存在しない、ファイルに読み取り権限がない、ファイルが互換性のあるフォーマットでエンコードされていないなど、このタスクが失敗する原因はいくつかあります。これらの異なる状況を区別することで、プログラムはいくつかのエラーを解決し、解決できないエラーをユーザーに伝えることができます。

Swiftでのエラー処理は、CocoaやObjective-CのNSErrorクラスを使用するエラー処理パターンと相互に運用されます。

Representing and Throwing Errors

Swiftでは、エラーはErrorプロトコルに準拠した型で表されます。この空のプロトコルは、型がエラー処理に使用できることを示します。

Swiftの列挙は、関連するエラー条件のグループをモデル化するのに特に適しており、関連する値は、伝達されるエラーの性質についての追加情報を可能にします。例えば、ゲーム内で自動販売機を操作する際のエラー条件を表現する方法は以下の通りです。

    enum VendingMachineError: Error {
       case invalidSelection
       case insufficientFunds(coinsNeeded: Int)
       case outOfStock
   }

エラーを投げることで、何か予期せぬことが起こり、通常の実行の流れを続けることができないことを示すことができます。エラーを投げるには throw 文を使います。たとえば,次のコードでは,自動販売機に追加のコインが5枚必要であることを示すためにエラーをスローしています。

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Handling Errors エラーの処理

エラーが投げられるとき、コードの実行部分は、エラー処理しないといけません。たとえば、問題を修正したり、別の方法を試したり、ユーザーにエラーを知らせたりします。

Swiftでエラーを処理するには4つの方法があります。関数からその関数を呼び出すコードにエラーを表示したり、do-catch文を使ってエラーを処理したり、オプションの値としてエラーを処理したり、エラーが発生しないことを主張したりします。

関数がエラーを発生すると、プログラムの流れが変わってしまうため、エラーを発生させる可能性のあるコードの箇所を素早く特定することが重要です。コードの中でエラーが発生する場所を特定するには、エラーを発生させる可能性のある関数、メソッド、イニシャライザを呼び出すコードの前に try キーワード、または try?try! これらのキーワードについては、以下のセクションで説明します。

Swift でのエラー処理は、try、catch、throw キーワードの使用により、他の言語での例外処理に似ています。Objective-Cを含む多くの言語での例外処理とは異なり、Swiftでのエラー処理は、計算コストがかかる可能性のあるプロセスであるコールスタックの巻き戻しを伴いません。そのため、throw文のパフォーマンス特性はreturn文のそれに匹敵します。

Propagating Errors Using Throwing Functions スローイング関数での伝播

関数、メソッド、イニシャライザがエラーを投げることができることを示すには、関数の宣言でパラメータの後に throws キーワードを記述します。throwsの付いた関数をスローイング関数と呼びます。関数が戻り値を指定している場合は、戻り値の矢印(->)の前に throws キーワードを記述します。

   func canThrowErrors() throws -> String
   func cannotThrowErrors() -> String

スローイング関数は、その内部で発生したエラーを、呼び出されたスコープに伝えます。

エラーを伝搬できるのはスローイング関数だけです。スローイング関数以外の関数内でスローされたエラーは、その関数内で処理しなければなりません。

以下の例では、VendingMachine クラスに vend(itemNamed:) メソッドがあり、要求されたアイテムが入手できない場合、在庫切れの場合、または現在の預け入れ金額を超えるコストの場合、適切な VendingMachineError をスローします。

   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(itemNamed:)メソッドの実装では、guardを使用してメソッドを早期に終了し、スナックの購入条件が満たされていない場合は適切なエラーを投げます。throw文はすぐにプログラムの制御を移すので、これらの要件がすべて満たされている場合にのみ、アイテムの販売が行われます。

vend(itemNamed:)メソッドは、スローされたエラーを伝えるため、このメソッドを呼び出すコードは、do-catch文、try?またはtry!を使用してエラーを処理するか、引き続き関数ないでエラーを処理しなければなりません。例えば、以下の例の buyFavoriteSnack(person:vendingMachine:) も スローイング関数であり、vend(itemNamed:) メソッドが投げたエラーは、 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)
   }

この例では、buyFavoriteSnack(person: vendingMachine:)関数は、指定された人の好きなスナックを調べ、vend(itemNamed:)メソッドを呼び出して購入しようとします。vend(itemNamed:)メソッドはエラーが発生する可能性があるため、前にtryキーワードを付けて呼び出しています。

スローイング initializersは、スローイング関数と同じようにエラーを伝えます。例えば、以下のリストにあるPurchasedSnack構造体のイニシャライザは、初期化プロセスの一部としてスローイング関数を呼び出し、発生したエラーを呼び出し元に伝えて処理します。

    struct PurchasedSnack {
       let name: String
       init(name: String, vendingMachine: VendingMachine) throws {
           try vendingMachine.vend(itemNamed: name)
           self.name = name
       }
   }

Handling Errors Using Do-Catch do-catch文の使用法

コードブロックを実行してエラーを処理するには、do-catch文を使用します。do句のコードでエラーが発生した場合、catch句と照合して、どのcatch句がエラーを処理できるかを判断します。

ここでは、do-catch文の一般的な形を紹介します。

   do {
       try expression
       statements
   } catch pattern 1 {
       statements
   } catch pattern 2 where condition {
       statements
   } catch pattern 3, pattern 4 where condition {
       statements
   } catch {
       statements
   }

catchの後にパターンを書くことで、その節が処理できるエラーを示します。catch節にパターンがない場合、その節は任意のエラーにマッチし、そのエラーをerrorという名前のローカル定数にバインドします。

たとえば、次のコードは VendingMachineError 列挙の 3 つのケースすべてにマッチします。

   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."

上の例では、buyFavoriteSnack(person:vendingMachine:)関数がtry式で呼び出されていますが、これはエラーが発生する可能性があるからです。エラーが発生した場合、実行は直ちにcatch節に移り、エラーを継続させるかどうかを決定します。パターンがマッチしなかった場合,エラーは最後のcatch節で捕捉され,ローカルのエラー定数に束縛されます。エラーが発生しなかった場合、do文の残りの文が実行されます。

catch節は、do節のコードが投げる可能性のあるすべてのエラーを処理する必要はありません。どのcatch節もエラーを処理しなかった場合、エラーは周囲のスコープに伝えます。ただし、伝わってきたエラーは、周囲のスコープで処理されなければなりません。throwしない関数では,囲んでいるdo-catch文がエラーを処理しなければなりません.スローイング関数では、囲んでいるdo-catch文か呼び出し元がエラーを処理しなければなりません。エラーが処理されずに最上位のスコープに伝わると、ランタイムエラーが発生します。

例えば、上記の例では、VendingMachineErrorではないエラーは、代わりに呼び出し側の関数でキャッチされるように書くことができます。

    func nourish(with item: String) throws {
       do {
           try vendingMachine.vend(itemNamed: item)
       } catch is VendingMachineError {
           print("Couldn't buy that from the vending machine.")
       }
   }
   do {
       try nourish(with: "Beet-Flavored Chips")
   } catch {
       print("Unexpected non-vending-machine-related error: \(error)")
   }
   // Prints "Couldn't buy that from the vending machine."

nourish(with:)関数では、vend(itemNamed:)がVendingMachineError列挙のケースの一つであるエラーを投げた場合、nourish(with:)はメッセージを印刷してエラーを処理します。それ以外の場合は、nourish(with:)はエラーをそのコールサイトに伝播します。このエラーは一般的な catch 節で捕捉されます。

関連する複数のエラーを捕捉する別の方法として、catchの後にカンマで区切ってリストアップする方法があります。例えば

    func eat(item: String) throws {
       do {
           try vendingMachine.vend(itemNamed: item)
       } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
           print("Invalid selection, out of stock, or not enough money.")
       }
   }

eat(item:)関数は、キャッチすべきvendingMachineのエラーをリストアップし、そのエラーテキストはそのリストの項目に対応しています。リストアップされた3つのエラーのいずれかがスローされた場合、このcatch節はメッセージを表示することでそれらを処理します。その他のエラーは、後から追加される可能性のある自動販売機のエラーを含め、周囲のスコープに伝えられます。

Converting Errors to Optional Values エラーのOptional Valueへの変換

try? を使用すると、エラーをOptional Valueに変換して処理することができます。try? 式の評価中にエラーが発生した場合、式の値は nil になります。たとえば,次のコードでは,x と y は同じ値であり,動作も同じです.

func someThrowingFunction() throws -> Int {
   // ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
   y = try someThrowingFunction()
} catch {
   y = nil
}

someThrowingFunction()がエラーを出した場合、xとyの値はnilになります。そうでなければ、xとyの値は、その関数が返した値になります。xとyは、someThrowingFunction()がどのような型を返すかに関わらず、optionalであることに注意してください。ここでは、関数が整数を返しているので、xとyはoptionalな整数です。

try? を使うと、すべてのエラーを同じように処理したい場合に、簡潔なエラー処理コードを書くことができます。たとえば、次のコードは、データを取得するためにいくつかのアプローチを使用し、すべてのアプローチが失敗した場合はnilを返します。

    func fetchData() -> Data? {
       if let data = try? fetchDataFromDisk() { return data }
       if let data = try? fetchDataFromServer() { return data }
       return nil
   }

Disabling Error Propagation エラーの無効化

投げられる関数やメソッドが、実際には実行時にエラーにならないことがわかっている場合があります。そのような場合には、式の前に try! を書いてエラーを無効にし、エラーが投げられないことをランタイムアサーションとして呼び出しを行うことができます。実際にエラーがスローされた場合は、ランタイムエラーが発生します。

例えば、次のコードでは、loadImage(atPath:)関数を使用しています。loadImage関数は、指定されたパスの画像リソースをロードし、画像がロードできない場合はエラーをスローします。この場合、画像はアプリケーションに同梱されているため、実行時にエラーが発生することはありませんので、エラーを無効にすることが適切です。

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Specifying Cleanup Actions

deferステートメントは、コードの実行が現在のコードブロックから抜ける直前に、一連のステートメントを実行するために使用します。このステートメントを使用すると、コードの実行がどのように終了したか(エラーが発生したか、returnやbreakなどのステートメントによって終了したか)にかかわらず、必要な後始末を行うことができます。例えば、defer文を使って、ファイル記述子を閉じたり、手動で割り当てられたメモリを解放したりすることができます。

defer文は、現在のスコープが終了するまで実行を延期します。このステートメントは、deferキーワードと、後で実行されるステートメントで構成されます。defer文には、breakやreturn文、エラーの発生など、制御を文の外に移すようなコードを含めることはできません。遅延されたアクションは、ソースコードに書かれている順序とは逆に実行されます。つまり、最初のdefer文のコードが最後に実行され、2番目のdefer文のコードが最後から2番目に実行される、という具合です。ソースコード順で最後のdefer文が最初に実行されます。

   func processFile(filename: String) throws {
       if exists(filename) {
           let file = open(filename)
           defer {
               close(file)
           }
           while let line = try file.readline() {
               // Work with the file.
           }
           // close(file) is called here, at the end of the scope.
       }
   }

上の例では、defer文を使って、open(_:)関数に対応するclose(_:)の呼び出しを保証しています。

エラー処理コードが含まれていない場合でも、defer文を使用することができます。

この記事が気に入ったらサポートをしてみませんか?