Swift

Swift/エラー処理

エラー処理構文

Swift2.0 で導入されたエラー処理構文。

エラーの発生

プログラムの実行中に例外的な状況が発生すると、 発生した例外の情報を連れて呼び出し元に直ちに戻ることができる。

throw 式

throw文の式は、「任意の型で構わないが、プロトコル ErrorType に適合する必要がある」。

プロトコル ErrorType は空のプロトコルとして定義されている。

public procotol ErrorType { }
extension ErrorType { }

エラーを表現するために共用型の列挙型を定義した例。

enum TicketError : ErrorType {
  case WrongDate, Shortage    // よくあるエラーを列挙する
  case Code(Int)              // エラーコードを表現可能
  case Unknown(String)        // 説明を文字列で表現できる
}

throw文を実行するかもしれない(throwing function)関数は、throwする可能性があることを定義で宣言しておく必要がある。 宣言は引数リストの直後に throws というキーワードを記述する。

func getTicket(order:String, _ fee:Int) throws -> Ticket
func printTicket(pass; Ticket) throws
func %%(a:Int, b:Int) throws -> Int

イニシャライザでもthrowできるが、その場合も throws 宣言しておく必要がある。

init(solution: Int) throws

エラーの伝播と捕捉

エラーは catch されるまで、次々に上位の呼び出しへ投げられ続けられる(伝播 propagation)。

エラーをthrowする関数は、関数名の前に try をつけて呼び出す。 エラーが発生しない場合は通常の関数呼び出しだが、エラーが発生すると関数の実行は途中で打ち切られる。

  1. try演算子を使って呼び出すが、それ以上は何もしない。
    エラー画はっせいした場合は、その呼び出し以降のプログラムは実行されず、 さらに上流の呼び出し元に伝播する。
  2. do-catch構文を使い、try演算子を使った呼び出しでエラーが起きたらcatchする。
  3. try? 演算子を使って呼び出す。
    エラーが発生しなかった場合は通常の型(オプショナル型でない)の値が、 エラーが発生した場合は nil が返される。
  4. try! 演算子を使って呼び出す。
    エラーが発生しなかった場合は通常の型(オプショナル型でない)の値が返される。 エラーが発生した場合は実行時エラーとなる。

do-catch構文

do {
  文 ...
  try演算子を用いた呼び出し
  文 ...
} catch パターン where節 {
  文 ...
} catch {   // 条件を指定しないcatch文は最後に書く
  文 ...
}

do-catch構文の例

enum FooError : ErrorType {  // 共用体を使うと便利
  case Baz, Ainz, Ooal
  case Calc(Int)
}
func div(a:Int, _ b:Int) throws -> Int {
  guard b != 0 else { throw FooError.Calc(0) }
  return a / b
}
func mod(a:Int, _ b:Int) throws -> Int {
  guard a != 0 else { throw FooError.Ainz }
  let d = try div(a,b)
  return a - b * d
}
func floor2nd(m:Int, _ n:Int) throws -> Int {
  var p = 0
  do {
    p = try mod(m, n)
  } catch FooError.Calc(let e) {
    print("2: エラー Clac(\(e))をキャッチ")
  }
  return p
}
func floor1st(x:Int, _ y:Int) {
  var k = 0
  do {
    k = try floor2nd(x, y)
  } catch FooError.Ainz {
    print("1: エラー Ainz")
  } catch {                    // すべてのエラーをキャッチする
    print("1: エラー ", error)  // パターンを記述しない節は定数errorを使う
  }
  print(k)
}
floor1st(9, 0)       // 実行
2: エラー Calc(0)     // 表示(floor2nd でキャッチされた)
0                    // 表示
floor1st(0, 2)       // 実行
1: エラー Ainz        // 表示(floor1st でキャッチされた)
0                    // 表示

try? および try! 演算子

if let n = try? div(10,3) {
  print(n)     // 3 が表示される
}
if (try? tear("shall we")) != nil {
  /* エラーがない場合の処理 */
}

try? を使うと、エラーが発生してもそれをキャッチするのでエラーを外部に伝播させることはない。 エラーの種類はわからない。

try! にすると、エラーが発生するとプログラム実行が止まる。

throwing function と try? 演算子

T型の値を返す throwing functionに try? 演算子を適用すると、得られる値は T? 型となる。 したがてって、元々 Int? 型を返す throwing functionに try? 演算子を適用すると、 得られる値は Int?? 型、すなわち Optional<Optional<Int>> 型となる。

func convInt(s: String) throws -> Int? {
  if s == "" { thorw FooError.Ooal }
  return Int(s)                      // 整数またはnilを返す
}
for s in [ "41", "gown", ""] {
  // if case let x? = try? convInt(s)    も同じ。エラーが小木田か判断できる
  if let x = try? convInt(s) {
    print("x: ", x)     // "41" -> Optional(41), "gown"->nilを表示する
  } else {
    print("Failure")    // "" のときはここで処理する
  }
}
for s in [ "41", "gown", ""] {
  if case x?? = try? convInt(s) {
    print("x: ", x)     // "41" -> 41 を表示する
  } else {
    print("Failure")    // "gown", "" のときはここで処理する
  }
}
for s in [ "41", "gown", ""] {
  switch try? convInt(s) {
  case let x??: print("x??: x=\(x)")    // "41" はここで表示される
  case nil?: print("nil?")              // "gown" はここで表示される
  case nil: print("nil")                // "" はここで表示される
  }
}

Int型のインシャライザは文字列を引数にすると Int? 型の値(Int型の値またはnil)を返す。 関数convIntは さらに空文字 "" の場合にエラーをthrowする。 関数 convInt と try? 演算子の組み合わせで得られる型は Int?? となる。 返す値は .Some(.Some(整数)), .Some(.None), .None の3種類ある。 これらを区別して扱うには switch 文を使う。 if-let文、if-case文ではエラーが起きたかどうか、または整数に変換できたかどうかの一方だけがわかる。

処理の中断と後始末

何らかの処理を必ず実行しなくてはならないときは、defer文を使う。

defer { ... }

defer文はコードブロックの内部に記述され、そのコードブロックの実行が終了するか、 中断される時に必ず実行される。

import Foundation
func copyFile(path:String, to dest;String) -> Bool {
  let fpin = fopen(path, "r")
  guard fpin != nil else { return false }
  let fpout = fopen(dest,"w")
  guard fpout != nil else { fclose(fpin); return false }
  while true {
    let ch = fgetc(fpin)
    if ch < 0 { break }
    fputc(c,fpout)
  }
  fclose(fpin)
  fclose(fpout)
  return true
}
import Foundation
func copyFile(path:String, to dest;String) -> Bool {
  let fpin = fopen(path, "r")
  guard fpin != nil else { return false }
  defer{ fclose(fpin) }        // 後でやるべき作業を登録する
  let fpout = fopen(dest,"w")
  guard fpout != nil else { fclose(fpin); return false }
  defer{ fclose(fpout) }        // 後でやるべき作業を登録する
  while true {
    let ch = fgetc(fpin)
    if ch < 0 { break }
    fputc(c,fpout)
  }
  return true
}

defer文の動作について

  • 実行が及んでいないdefer文のブロックは実行されない
  • defer文のブロックは、defer文が実行されたのと逆の順序で実行される
  • defer文のブロックに変数などが含まれていた場合、ブロックが実行される時の値が使われる (クロージャのように変数の値がキャプチャされるわけではない)。
func doit(a:Int) {
  var n = 0
  defer { print(1, n) }
  if a == 0 { return }
  defer { print(2, "oops") }   // a == 0 なら実行が及ばない
  n = a
}
doit(0)
print("---")
doit(99)

実行結果

1 0
---
2 oops
1 99

ループのコードブロックは繰り返しのたびに新たに評価されるので、 defer文で指定した内容は繰り返しが1回行われる度に実行される。

for x in 1 ..< 3 {
  defer { print("defer: ", x) }
  print("loop: ", x)
}

実行結果

loop: 1
defer: 1
loop: 2
defer: 2

doブロックの最後で表示する文を defer 文で選択しているかのように間違えがちだが、 if ブロックや elseブロックの中の defer 文で指定した内容はそのブロックを 抜けるときに実行されてしまう。

var n = 0
do {
  if n == 0 {
    defer { print("then: ", n) }
    n = 1
  } else {
    defer { print("else: ", n) }
    n = 2
  }
  n = 1000
  print("end: ", n)
}

実行結果

then: 1
end: 1000

defer文とARCの動作

// FooError, 関数modの定義は前と同じなので省略
class NPC {
  let name: String
  init(name:String) { self.name = name }
  deinit { print("deinit: ", name) }
}
func floor2nd(m:Int, _ n:Int) throws -> Int {
  let npc2 = NPC(name: "Beta")
  defer { print("2: defer") }
  let p = try mode(m,2)     // この呼び出しの中でエラーが発生する
  return p
}
func floor1st(m:Int, _ n:Int) {
  do {
    let npc1 = NPC(name: "Alpha")
    defert { print("1: defer") }
    print( try floor2nd(x,y) )     // この呼び出しの中でエラーが発生する
  } catch {
    print("1: エラー",error)
  }
}
floor1st(10, 0)
2: defer
deinit: Beta
1: defer
deinit: Alpha
1: エラーCalc(0)

アクセス制御

アクセス制御 (access control) 、つまり可視性 (visibility) の3つのレベル

  • public
    importすればどんなソースファイルからでもアクセスできる。
  • internal
    定義を含むソースファイルと同じモジュール内ならば、どのソースファイルからでもアクセスできる。 importは必要ない。何も指定しないとこれになる。
  • private
    定義されたソースファイルの中だけでアクセスできる。

アクセス制御の指定

private var localBuffer = [Int]()
internal var shareBuffer = [String]()
pubilc var spoolingBuffer = [PrintJob]()

publicとして指定された変数は、publicな型でなくてはいけない。 publicとして指定された関数は、引数も返り値もpublicな型でなくてはいけない。

タプルや関数のように複数の要素を組み合せて構成されるものの可視性は、 その中で最も可視性のレベルが低い要素の可視性のレベルに合わせられる。

func analyze(obj: Relic, method: Proc) -> Report
// 関数analyzeの可視性は Relic, Proc, Report より高くできない

クラスや構造体はひとまとまりの型として可視性を指定でき、 さらに個々のメンバなどにも可視性が設定できる。 ただし、メンバの可視性を型全体の可視性よりも高くすることはできない。

  • 型がprivateの場合 -- メンバもprivate
  • 型がinternalの場合 -- メンバもinternal (private指定するとprivate)
  • 型がpublicの場合 -- メンバはinternal ( (private指定するとprivate, public指定するとpublic)

クラス継承とアクセス制御

サブクラスはスーパクラスよりも高いレベルの可視性を指定することはできない。 サブクラスを利用するコードから、スーパークラスから継承された機能にもアクセスできる必要があるため。

メンバであるメソッドやプロパティを上書き定義する場合、 サブクラスではスーパークラスで指定されていたよりも高いレベルの可視性を指定できるが、 スーパークラスで指定されていたよりも低いレベルの可視性は指定できない。 サブクラスはスーパークラスのように振る舞う必要がある場合があるため。

継承と関係なく、サブクラスのメソッドがスーパークラスのメソッドを利用して実装されている場合、 スーパークラスの可視性がサブクラス側の可視性に影響することはない。

拡張定義のアクセス制御

拡張定義の先頭 (extension の前) にアクセス制御の修飾子 public, internal, private のどれかを 指定することができる。何も指定しない場合は internal が指定されたものとみなされる。

プロパティのアクセス制御

プロパティや添字付けの定義でも可視性の宣言ができる。 プロパティに関しては、setterにgetterよりも低い可視性を与えることができる。 public, internal, private に加えて、setterだけの指定である public9set), internal(set), private(set) が指定できる。

class USBCamera {
  private var focus = 2.0
  private var shutterSpeed = 1.0 / 1000.0
  private func take() { print("take a picture") }
}
class WebCamera: USBCamera {
  internal override var focus: Double {
    didSet { super.shutterSpeed = ss(super.focus) }
  }
  internal private(set) override var shutterSpeed: Double {
    get { return super.shutterSpeed }
    set { super.shutterSpeed = newValue }
  }
  override init() { super.init() }    // overrideが必要
  override func take() { super.take() }  // internalとなる
  private func ss(f:Double) -> Double { return (f*f)/4000.0 }
}

クラス USBCamera の型自体は internal だが、プロパティ focus, shutterSpeed も関数 take() もprivateである。 クラス USBCamera は他のクラスからは直接利用させない予定だが、 クラス全体を private にしてしまうとサブクラスWebCamera?からも初期化できなくなるので、 クラス自体は internal とする。

クラス WebCamera? の型自体は internal であり、 そのメンバもデフォルトで internal であるが、わかりやすさのために明示的に指定している。

クラス USBCamera にはイニシャライザがないが、default initializerが自動的に用意されるので、 サブクラス WebCamera? のイニシャライザには override を指定しなくてはいけない。

アサーションとテスト

デバッガ LLDB の使い方


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2016-06-10 (金) 16:53:12 (1454d)