LL脳がscalaの勉強を始めたよ その19


Scalaコップ本の第7章の続きをやりますよー、今回はtry~catchの例外処理やらmatchやらですよー

try式による例外処理

Scalaで扱う例外処理もよくある形ではあるんですが、とりあえずファイルを読み込んでゴニョゴニョする処理にtry~catch~finallyを組み込んだサンプルを書いてみるのです

// ファイル読み込み関連のjavaパッケージをインポート
import java.io.FileReader
import java.io.FileNotFoundException
import java.io.IOException

// 例外処理開始
try{
    // ファイル読み込み
    val file = new FileReader("input.txt")
    // ファイル内の文字数を取得
    val charNum = file.read
    // ファイル内の文字がない場合は例外をスロー
    if(charNum <= 0) throw new IllegalArgumentException
    else println("file read done")

// 例外をキャッチして処理
}catch{
    // 例外に応じて処理を振り分けますよー
    case ex: FileNotFoundException => println("No File")
    case ex: IOException => println("File IO Error")
    case ex: IllegalArgumentException => println("No String")

// 例外の発生如何にかかわらず処理しますよー
}finally{
    // とりあえずファイルは閉じましょう
    file.close()
}

大体の流れはこんな感じで、投げた(もしくは処理内で自動的に投げられた)例外に応じて処理していくといういつもの流れですね。ついでに他の言語と同じようにtry~catch、try~finallyだけでも実行出来るみたいデス

んじゃ、各要素に若干補足していきますよ

例外のスロー

上の例でthrow new <例外>の形式でぶん投げているやつです。Scalaでは結果型を持つ式みたいでNothing型を持っているみたいですね。実際は処理が結果型を取得する前に例外がぶん投げられてしまうので使うことができないワケですけども...

なお例外投げるときに例外メッセージを付けられますね

try{
    // 例外メッセージを付与しますよ
    throw new IllegalArgumentException("huga")
}catch{
   // 例外メッセージを出力しますねー
    case ex: IllegalArgumentException  => println(ex)
}
例外のcatch

ぶん投げた例外をキャッチしますよーの場所ですね、例外の振り分けは上のサンプルのようにcaseを使ってやるみたいですね

いちいち振り分けないで一括で例外として処理する場合はこんな感じのcatch処理になりますね

try{
    throw new IllegalArgumentException("huga")
}catch{
   // caseに例外の種類を欠かないと全例外でのcatch処理になりますね
    case ex  => println(ex)
}

Javaと違ってScalaは例外のキャッチやthrow節宣言が必須になっていないらしいです。つまり例外の宣言だけしておいてエラー処理はあとでじっくり考えるというのもありなのかしら?

まあ、絶対忘れるだろうから出戻り増えまくりな感じがしますけども…

ついでにThrow節の宣言は@throwsアノテーションで宣言するらしいです。@throwsアノテーションが何者か不明ですが29章あたりでやるらしいのでほっぽっておきますよー

finally節

例外が発生しようがしまいが実行する処理を記述する場所ですね。そういやPHPにはfinallyがないってid:hyukixが悶絶してましたな。言われてみれば使ったことなかったな…

ちなみに上のサンプルではファイルクローズ処理でfinally節を使ったのですが、Scalaではその手をの処理をローンパターンとかいうのでやるらしいデス…となるとfinallyはどういうパターンで使うべきかな…まあ、そのうち思いつくかしら?

ローンパターンは9章でやるみたいデス

finallyでの値の生成

Scalaのtry~finally処理でのfinally説では”値を返してはいけません”の注意

Scalaのfinally節では明示的にreturnすることで値を返すことができるのだけども、tryブロックやcatch節の値をあっさりと上書きしてしまうので注意が必要デス

ついでに、Scalaでは明示的なreturnを省略できることが多いので、下のような混乱パターンを作ってしまう可能性がありますな、どちらかというとこっちの方が問題っぽい気がしマス

// 結果値2を返す
def f():Int = try { return 1 } finally { return 2 }
// 結果値1を返す
def f():Int = try { 1 } finally { 2 }

コップ本にも書いてある通り、finally節は値を返すのではなく(ファイルクローズや一時ファイルの削除)処理等の後始末のための副作用を発生させたい場合に使うのが吉みたいですねぇ。

match式

Web上のScalaサンプルを見ると、かなりの万能選手な雰囲気のあるmatch式についてデス。本格的なmatch式については15章で気合いれてやるらしいので、この節では一番単純なパターンにふれる程度ですよー

switch文の代わりにmatch式

match式は他言語のswitch文の代わり使うことがでますよー、それではmatch式のサンプルを書いてみますよー

// 状況によって変化する変数hogeを定義
val hoge = "huga"
// match式で振り分け
hoge match {
    case "hoge" => println("match hoge")
    case "huga => println("match huga")
    case "moge" => println("match moge")
    // 各条件に当てはまらない場合のデフォルト処理
    case _ => println("other")
}

switch文につきもののbreakを書かなくていいのでとても楽ちんですね。ちなみにswitchでのdefaultに似た動作が"case _"になりますよ。

ついでにmatchは式なので結果値を返せることから、次のような書き方もできますねー

// 状況によって変化する変数hogeを定義
val hoge = "huga"
// match式で振り分けた結果から変数に値を設定
val result = hoge match {
    case "hoge" => "match hoge"
    case "huga => "match huga"
    case "moge" => "match moge"
    case _ => "other"
}

各caseの中で変数定義をしなくて済むので楽ちんですし、caseの振り分けという処理に単純化できるのでミスが減りそうでいい感じです。

…と、ここまでが単純なmatch式の使い方ですが、match式では正規表現やその他いろいろと使えそうなので15章をwktkして待つことにしますよー

breakとcontinueは使わない

Scalaでは関数リテラル(8章で説明するみたいデス)と相性の悪いcontinueとbreakがそもそもコマンドの中にないそうデス

無いなら無いでなんとかしなければ…なのですが、とりあえずこんな針で置き換えればいいんじゃね?という提案がこの節デス

置き換えてみようぜ!提案は次のとおりです

  • continue → if
  • break → Booleanに置き換えてwhileループが処理判定

...とりあえず理解がおっつかないのでサンプルやってみましょーか

ひとまず次のようなbreakとcontinueいりのjavaコードを例にします。このコードはコマンド行引数リストから先頭が"-"でなく、かつ末尾が".scala"のモノを探すコードみたいデス

int i = 0;
boolean foundIt = false;
while(i < args.length) {
    // 先頭が"-"のときはcontinue
    if(args[i].startsWith("-"){
        i = i + 1;
        continue;
    }
    // 末尾が".scala"のときはbreakして終了
    if(args[i].endWith(".scala")){
        foundIt = true;
        break;
    }
    i = i +1;
}

javaコードですなぁ(´・ω・`)…き、気をとりなおして、Scalaで書き換えてみますよ

var i = 0
var foundIt = false
// breakの条件をwhileに組み込み
while(i < args.length && !foundIt) {
    // continueをifに置き換え
    if(!args(i).startsWith("-")){
        // break条件で変数の値を書き換え
        if(args(i).endsWith(".scala")) foundIt = true
    }
    i = i + 1
}

基本的な箇所は変わってないみたいなので、前のjavaコードと比較するとbreak、continueがそれぞれifとBoolean(に絡むwhileの条件)に置き換わっていることが分かりますなー。

しかしながらvar使いまくりでwhileが残っているのは関数型的Scalaっぽくない!!ということでさらに書き換えますよー

// 再帰にするんで関数定義
def searchForm(i; Int):Int = {
    // 見つからなかった場合は-1を返す
    if(args.length <= i) -1
    // 頭に-を見つけたら次の引数に移動(先頭に"-"がつかない条件より)
    else if (args(i).startsWith("-")) searchForm(i + 1)
    // 末尾に.scalaを見つけたら数値(何番目の引数か)を返して終了
    else if (args(i).endWith(".scala")) i
    // 条件に合わなければ次の引数に移動
    else searchForm(i + 1)
}
// スクリプト実行するための実行行
val i = searchFormm(0)

このスタイルのほうがわかりやすいようになれば、とりあえず切紙*1ってところですかねー

ちなみに上みたいなコードだとScalaコンパイラ再帰関数を生成せずにWhileループと似た様なコードを生成するみたいなので、速度的にもそこそこ速くなりそうな雰囲気ですね。これみたいに再帰呼び出しが全て末尾だとかなんとかいう条件で、こういう再帰呼び出しは関数の先頭にジャンプする云々みたいな話は8章でやるみたいなので、またあとでー

いじょー

次で7章終われるかしら…が、頑張ります

*1:武道のレベル的なアレ:切紙→目録→免許皆伝とか切紙→目録→印可→免許→皆伝→秘伝→口伝とか複数パターンあるみたいですけど