例外の取り扱いをする場合にどれを使うのが適切なのかわからなくなったので、scala.util.control.Exceptionとscala.util.Try、両クラスの各機能をざっと使ってみる。
前半はscala.util.control.Exception, 後半はscala.util.Try。
とりあえずallCatchですべての例外をcatchする処理を書く。
最も手軽に使えそうなのがopt。例外だったらNone、成功すればSome(x)を返す。
import scala.util.control.Exception._ def divide(x: Int, y: Int) = allCatch opt { x / y } var result = divide(0, 0) println(result) #=> None result = divide(0, 3) println(result) #=> Some(0)
上記コードではallCatch optの中で除算を実行している。
1度目の呼び出し(divide(0, 0))は0/0を実行しているので例外になってNoneが返る。2度目の呼び出しはyに0以外を渡しているのでSome(計算結果)が返る。
eitherでLeftとRightの値が返るようにしてみる。
import scala.util.control.Exception._ def divide(x: Int, y: Int) = allCatch either { x / y } divide(0, 0) match { case Left(e) => e.printStackTrace() case Right(i) => println(i) }
withTryを使うとSuccess/Failureで返る。
import scala.util.control.Exception._ def divide(x: Int, y: Int) = x / y allCatch withTry { divide(0, 0) } match { case Success(i) => println(i) case Failure(t) => t.printStackTrace() }
withTryよりもwithApplyの方が好きかも。withApplyの後に例外時の処理、applyの後に処理を書く。
import scala.util.control.Exception._ def divide(x: Int, y: Int) = x / y val result = allCatch withApply { t: Throwable => -1 } apply divide(0, 0) println(result) //=> -1
これにandFinallyを使うとfinallyも書ける。
import scala.util.control.Exception._ def divide(x: Int, y: Int) = x / y val result = allCatch withApply { t: Throwable => -1 } andFinally { println("finally") } apply divide(0, 0) //=> finally println(result) //=> -1
allCatch withApply f1 and Finally f2 apply f3 のように書けるのは何かとやりやすい。
optやeitherなどを使って例外を処理した場合、ControlThrowableとInterruptedExceptionはcatchされても再度throwされるらしい。breakを途中で止められても困るしね。
下記の例では途中でBreakControl(ControlThrowableを継承している)をthrowしている為、withApplyは呼び出されずにそのまま例外がthrowされる。
// scala.util.control.BreakControlを起こすだけの関数 def break() = Breaks.break() val result = allCatch withApply { t: Throwable => -1 } apply break() //=> Exception in thread "main" scala.util.control.BreakControl println(result)
nonFatalCatchでは深刻でない例外のみcatchする。具体的にはVirtualMachineError, ThreadDeath, LinkageErrorなどが対象になるらしい。
下記の場合、1つ目のnonFatalCatchはException(fatalでない)なのでNoneが返るが、2つ目はLinkageErrorをthrowしているので例外がthrowされる。
var result = nonFatalCatch opt { throw new Exception } println(result) //=> None result = nonFatalCatch opt { throw new LinkageError() } //=> Exception in thread "main" java.lang.LinkageError println(result)
ultimatelyを使うとcatchなしでfinallyのみ記述できる。
def divide(x: Int, y: Int) = x / y ultimately { println("finally") } { divide(0, 0) }
scala-loggingが入ってるものとして、LazyLoggingを継承したtrait作って、そこにoptWithWarnというメソッドを追加してみる。
import scala.util.control.Exception._ trait LazyLogging extends com.typesafe.scalalogging.LazyLogging { def optWithWarn[U](body: => U): Option[U] = allCatch withApply { t: Throwable => logger.warn(t.getMessage, t); None } apply { Some(body) } }
これで例外時はwarnを出してNoneを返す処理になるはず。未テスト。
ignoringを使うと例外を無視する。但しclassofで例外を指定しないといけない。
def something() = { println("something"); throw new Exception } val result = ignoring(classOf[Throwable]) { something() } //=> something println(result) //=> ()
handlingでorして複数の例外の処理を書いてみる。
とりあえず単一の例外を取得。
def divide(x: Int, y: Int) = { x / y } handling(classOf[ArithmeticException]) by { e: Throwable => println("ArithmeticException") } apply { divide(0, 0) }
handlingの引数には複数クラスを指定できる。handling(classOf[ArithmeticException], classOf[NumberFormatException])のように。
またorで繋ぐことで例外ごとにhandlingを変えることもできる。
(handling(classOf[ArithmeticException]) by { e: Throwable => println("ArithmeticException") }) or (handling(classOf[NullPointerException]) by { e: Throwable => println("NullPointerException") }) or (handling(classOf[NumberFormatException]) by { e: Throwable => println("NumberFormatException") }) apply { divide(0, 0) }
handlingの処理をあらかじめ記述しておくとスッキリする。
def divide(x: Int, y: Int) = { x / y } val h1 = handling(classOf[NumberFormatException]) by { e: Throwable => println("NumberFormatException") } val h2 = handling(classOf[NullPointerException]) by { e: Throwable => println("NullPointerException") } val h3 = handling(classOf[NumberFormatException]) by { e: Throwable => println("NumberFormatException") } h1 or h2 or h3 apply divide(0, 0)
Tryでwrapすれば、getOrElseで失敗時の値を設定したり、recoverで例外のハンドリングができる。
下記はgetOrElseで失敗時は-1を返すようにしている。
import scala.util.Try def divide(x: Int, y: Int) = Try { x / y } val result = divide(0, 0).getOrElse(-1) println(result) #=> -1
getを用いると、例外でなければ値が取得でき、例外が発生していれば当該の例外がthrowされる。
def divide(x: Int, y: Int) = Try { x / y } val result1 = divide(4, 2).get println(result1) //=> 2 val result2 = divide(0, 0).get println(result2) //=> Exception in thread "main" java.lang.ArithmeticException: / by zero
Exceptionの方でもやったようにSuccess/Failureでパターンマッチできる。
def divide(x: Int, y: Int) = Try { x / y } divide(0, 0) match { case Success(i) => println(i) case Failure(e) => println(e.getMessage) } //=> / by zero
foreachで成功時だけ処理を行わせる。
下記のケースでは前者は例外にならないので結果が標準出力され、後者は例外が発生しているので何も出力されない。
def divide(x: Int, y: Int) = Try { x / y } divide(3, 1) foreach println //=> 3 divide(0, 0) foreach println //=>
recoverはFailureの中身に対して処理をしてSuccessに置き換えることができる。
下記の例では0除算で例外が起きているが、recover内でArithmeticExceptionの場合は標準出力して-1を返すと記述されている為、結果はSuccess(-1)が返る。
def divide(x: Int, y: Int) = Try { x / y } val t = divide(3, 0) recover { case e: IOException => { println("IOException"); -1 } case e: ArithmeticException => { println("ArithmeticException"); -1 } case e: NumberFormatException => { println("NumberFormatException"); -1 } case e => throw e } //=> ArithmeticException println(t) //=> Success(-1)
recoverWithだと勝手にSuccessでwrapされない。
下記の例では指定した例外の場合はSuccessで返し、それ以外の例外であればFailureで返している。
val t = divide(3, 0) recoverWith { case e: ArithmeticException => Success(-1) case e: NumberFormatException => Success(-1) case e => Failure(e) } //=> ArithmeticException println(t) //=> Success(-1)
TryはNonFatalな例外のみFailureで返し、それ以外だとそのまま例外をthrowする。
下記のようにFatalなLinkageErrorが発生した場合や、BreakなどのControlThrowable、InterruptedExceptionが発生した場合はcatchされない。
def something() = Try { throw new LinkageError() } something() //=> Exception in thread "main" java.lang.LinkageError