Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 451 - Swift 2 : Error Handling(エラー・ハンドリング)

Swiftで遊ぼう!の古い記事-> Life-LOG OtherSide

Swiftデザインパターンの勉強に取り組んでいるのですが、データのセーブとロードの機能実装でモタモタしてます(^^;)

実はNSURLの扱いやNSErrorの扱いのところでエラーに遭遇して先に進めなくなったので、Swift 2で変更になったところを勉強してから取り組もうと思います。

まず、Swiftの構造的な変更としてError Handlingがあります。エラーの扱いとNSErrorとの絡みで、かなり変更があったのでじっくり取り組んでみます。

The Swift Programming Language(Swift 2)本で、Error Handlingの章が新しく追加されているので、これから使用すべき機能ということです。

と言っても私のような素人にとってどう使っていいのか解りにくいところもあります。

まず、教科書をじっくり読んで構文に慣れるところから始めます。

Error Handling

説明の冒頭にも書かれていますが、プログラム内で生じるエラーに対する反応だけで無く、回復させるステップも含まれているのがエラー・ハンドリングです。

CocoaクラスのNSErrorクラスと切っても切れない関係でシームレスな繋がりの説明は「Using Swift with Cocoa and Objective-C本」にあるので、また時期がきたら勉強します(^^;)

Swiftでエラーは、ErrorTypeプロトコールに準拠した型で実装されます。このプロトコールは全くの空っぽで、自分でエラーを想定して用意する必要があります。自分のプロジェクトにしっかり組み込むことでかなりエラーの解析がしやすくなりそうです。

エラーの条件を設定するのはenum型です。このenum型もSwift 2でかなり拡張されて便利になりました。また別の機会に詳しく勉強します。

正本では、自動販売機(Veniding Machine)の内部ロジックを例として取りあげているので、全く同じ例文コードの説明をしていきます。

VendingMacineクラスを書いていくのですが、その内部ロジックで発生するであろうエラーを推定してエラーコードを自作します。

  1. 自動販売機で取り扱っていない品物を選んだら、エラーとして「InvalidSelection」を投げる(throw)。
  2. 品物を買うコインが足りなければ、いくら足りないか返すメソッド「InsuffficientFunds(coinsNeeded: Int)」を投げる。
  3. 品物が無かったら「OutOfStock」を投げる。
enum VendingMachineError: ErrorType {
    case InvalidSelection
    case InsufficientFunds(coninsNeeded: Int)
    case OutOfStock
}

これでエラーの条件は決まりました。次にVendingMachineクラスをコードしていくのですが、ますItemというStructure型を用意します。これで「price:Int」と「count:Int」を持たせます。

struct Item {
    var price: Int
    var count: Int
}

そして、「キー(key)」がString型の商品名、「値(value)」が構造体Item型としたDictionary(辞書)型[ String: Item ]のinventoryというプロパティを用意します。

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
}

このinventoryと一緒に手持ちのコイン数を表すcoinsDepositedもプロパティとして持たせます。

このクラスは初期化ステップを省いて初期値を入れているところがデモらしいですね。

そしてこのクラスの中にメソッドを用意します。

ます、どの商品が選択されたかユーザーに解るようにヘルパーメソッドとし次の表示メソッドを入れます。

func dispenseSnack(snack: String) {
  print("Dispensing \(snack)")
}

このメソッドはエラー・ハンドリングに関係ありません。次のエラー・ハンドリングを扱うメソッドの中で利用するだけです。

func vend(itemNamed name: String) throws {
 guard var item = inventory[name] else {
  throw
  VendingMachineError.InvalidSelection
 }
 guard item.count > 0 else {
  throw
  VendingMachineError.OutOfStock
 }
 guard item.price <= coinsDeposited else {
  throw
  VendingMachineError.InsufficientFunds(coninsNeeded: item.price - coinsDeposited)
 }
 coinsDeposited -= item.price
 --item.count
 inventory[name] = item
 dispenseSnack(name)
}

エラーを発生するであろうメソッドをコードする時に、エラーを選別するためにパラメータの後に「throws」というキーワードを入れます。これでこのメソッドがエラーを生じた場合に他の処理に投げる(throw)ことができます。

guard文は、オプショナル判定をする「if-let」のピラミッド地獄から解放させられる便利な機能で、「if let」の多重入れ籠状態を解消してコードをリーダブルにできます。私もこれから「guard」を利用してオプショナル判定をします。

まず最初にメソッドの引数に登録した品物以外の入力があった場合、このメソッドはエラーを生じます。この場合、エラーハンドリングとして、VendingMachineErrorタイプの.InvalidSelectionを投げます。

次にストックが0以下になると、OutOfSotckを投げて、コインが足りなければ、足りない量を計算して返すInsufficientFundsを投げます。

これらのチェックをクリアした場合、アイテムの値段だけコインを差し引きして、ストックを1つ減らして、1つ減ったとりう表示をさせます。

こうのようにメソッドを書けば、どういうエラーが発生するのか区別させることができます。当然のようにこれだけでは不十分です。エラーを投げる(thorw)ことはできても、このままではエラーになります。投げ(throw)られたらキャッチ(catch)しないといけません。

私のような素人でiOSアプリを作ろうとしているプログラマーに重要なのは、throws付きのメソッドをコードするよりもNSError絡みのthrows付きのメソッドを利用することに慣れる方が先でしょう。これはプロトコールの使用法を習得するステップと似ています

エラー・ハンドリングには以下の4つの処理で構成されています。

  1. 関数からコードまでエラーを発生させることができ、そこから関数を呼ぶことができます(throws)。
  2. 「do-catch」構文を使ってエラーを処理します。
  3. オプショナル値としてエラーを処理します。
  4. エラーが生じていないように振る舞わせます。

エラーが生じると、プログラムの流れが変わります。どこでエラーが生じるか見極めることが重要で、エラーが生じやすい関数、メソッド、イニシャライザを呼ぶ前に「try」キーワードを加えます*1

エラーが発生すると、これを処理する「do-catch」を考えるのですが、教科書の説明文をみていきます。

まず準備段階として次のDictionary型の定数を用意します。

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)
}

この関数にも「thorows」のキーワードがあります。この関数の中で、thowsキーワードの付いたメソッドを持っているVendingMachineクラスが引数として与えられるからです。そして、この中で重要なのが、メソッド「vend」の前に「try」キーワードが付いていることです。このキーワードが付いているということは、このvendメソッドも「throws」キーワードがあるので、ErrorTypeでエラーが投げ(throw)られます。というか「try」を付けないとエラーになります。

この「buyFavoriteSnack()」もそのまま使うと、playgroundでエラーになります。

例えば、次のようにVendingMachineクラスのインスタンスを生成します。

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 100

そして、直接メソッドに入れると...

f:id:yataiblue:20151004082843j:plain

「Call can throw but is not marked with 'try'」というエラーが表示されます。「throws」キーワードがあるメソッドは次のように呼ばないといけません。

try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)

すると問題なく実行されます。しかし、この段階では、エラーが発生しても処理を実装していないので、「vendingMacine.coinsDeposited = 8」のように意図してエラーを発生させるとランタイムエラーになります。

このエラーをキャッチするシステムが「do-catch」です。

do {
    try buyFavoriteSnack("Tom", vendingMachine: oneVendingMachine)
} 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.")
}

こうしてErrorTypeをキャッチして別処理させます。

この仕組みを上手く開発に取りこめばかなりエラー解析が楽になりそうです。まだ素人の私にはどのように拡張させていけばいいのか理解できていませんが、データのやり取りにエラー・ハンドリングを加えることの重要性はなんとなく解ります。特にインターネットを介してデータをやり取りする場合、エラーは予測できるからです。

ファイルローディングを扱うNSURLクラスの取り扱いにエラー・ハンドリングは欠かせないということです。

NSErrorクラスの取り扱いにも色々あるようで、「do-catch」だけでエラーの対応はできません。「thorws」のキーワードを付けた関数やメソッド、イニシャライザは、エラーを投げます(throw)。しかし、どうも「throws」キーワードに関係なくエラーを投げるようです(←ちょっと確信が持てませ んが(^^;)。エラー処理でもう一つ重要なのは、エラーなら「nil」を返して、エラーでなければオプショナル値を返す「try?」です。

しかし、「try」を強制アンラップ「!」する「try!」の取り扱いがよく解りません。強制アンラップをするということは、エラーじゃないことが確証された状態です。もしエラーになると実行時にアプリはクラッシュになります。じゃあ「try」のまま変更しなくてもいいような気もします... 実例をみて理解しないと解りませんね。

そして、「defer」もエラーを迂回するコードになります。エラーになってもならなくても「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.
    }
}

例文の理解はできますが、これが実際のコードで利用されるとなると別です。

これでエラーの基礎的な内容を説明しました。

実際のコードを少し見てみましょう

WebページにあるイメージをiOSに取りこむ一般的な手順を今まで(Swift1.2)のコーディングの仕方で記述すると以下のようになります。

// Swift 1.2 スタイル
let myURL = NSURL(string: "http://xxxxx.xxx.xx/xxxxxx.xxxx/xxxx.jpg")
let myData = NSData(contentsOfURL: myURL, options: NSDataReadingOptions.DataReadingMappedIfSafe)
let myImage = UIImage(data: myData)

まず大きく変わったのがNSURLクラスの扱いがすべてオプショナルになっているので、上記のような書き方でエラーになります。まず、最初の「myURL」はオプショナルNSURL型になるため使用するためにアンラップが必要です。といことで、最初のエラーメッセージは、2行目のコード、NSDataの初期化で渡すパラメータの「myURL」に「」を付けるように指示が出ます。

// Swift 1.2 スタイル
let myURL = NSURL(string: "http://xxxxx.xxx.xx/xxxxxx.xxxx/xxxx.jpg")
let myData = NSData(contentsOfURL: myURL!, options: NSDataReadingOptions.DataReadingMappedIfSafe)
...

!」を付けたら次にエラー表示が変化します。

f:id:yataiblue:20151020192239j:plain

「Call can throw but is not marked with 'try'」になりました。まさにこれです!NSDataクラスはイニシャライザに「throws」が組み込まれているんです!「throws」の定義された関数、メソッド、イニシャライザを使用する時、必ず「try」が必要になります。

// Swift 2.0 スタイルに近づいた...
let myURL = NSURL(string: "http://xxxxx.xxx.xx/xxxxxx.xxxx/xxxx.jpg")
let myData = try NSData(contentsOfURL: myURL!, options: NSDataReadingOptions.DataReadingMappedIfSafe)
let myImage = UIImage(data: myData)

もうエラーハンドリングを勉強したので、これじゃ不十分だって分かりますね。「try」を付けて実行した場合、エラーが発生した時にcatchが定義されていないのでランタイムエラーに陥ります。

これを処理する「do-catch」をコードする必要があります。

// まさにSwift 2.0 スタイル
let myURL = NSURL(string: "http://xxxxx.xxx.xx/xxxxxx.xxxx/xxxx.jpg")

let myData: NSData
do { 
  myData = try! NSData(contentsOfURL: myURL!, options: NSDataReadingOptions.DataReadingMappedIfSafe)
} catch {
    print("Download is failed")
}

let myImage = UIImage(data: myData)

ストリーボード上で上記のコードを実現させるために「try」を「try!」にしないと動きません。

この意味がちょっと分からないけど、なんとか動かすことはできました。

こんなところでエラーハンドリングを終えます。

*1:throwsの実装されているメソッドに限ります