M.Hiroi's Home Page

お気楽 Scala プログラミング入門

例外処理


Copyright (C) 2014-2024 Makoto Hiroi
All rights reserved.

はじめに

今回は「例外処理」について説明します。一般に、例外 (exception) はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を備えているプログラミング言語が多くなりました。もちろん Scala にも例外処理があります。なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本稿でもエラーのことを例外と記述することにします。

●例外の捕捉

通常、例外が発生すると Scala はプログラムの実行を中断しますが、致命的な例外でなければプログラムの実行を継続する、または特別な処理を行わせたい場合もあるでしょう。このような場合に、例外処理が役に立ちます。Scala では発生した例外を捕まえるのに try 式を使います。try 式の構文を下図に示します。

try {
  処理A
} catch {
  case 変数1: ExceptionClass1 => 式1
  case 変数2: ExceptionClass2 => 式2
  ...
}

        図 : 例外処理

Scala の場合、try も式になります。try は、そのあとに定義されている処理 A を実行します。処理 A が正常に終了した場合は try も終了し、返り値は処理 A の結果になります。もしも、処理 A で例外が発生した場合、処理 A の実行は中断され、その例外が catch の case 節で指定した例外と一致すれば、その case 節を実行します。try の返り値は case 節で実行した式の値になります。

case 節には例外を型 ExceptionClass で指定します。捕捉した例外は case 節の変数にセットされます。try の catch には複数の case 節を指定することができます。Scala の例外は Java と同じクラスが使用されいて、Throwable というクラスとして定義されています。例外は階層構造になっていて、すべての例外は直接または間接的に Throwable を継承します。Throwable は Error と Exception に分けられ、Exception は RuntimeException とそれ以外の例外に分けられます。

Error を継承した例外は、復旧するのが困難なエラーが発生したことを表します。RuntimeException を継承した例外は、Java の仮想マシン (JVM) で発生したエラーを表します。たとえば、0 で割ったときに送出される例外 ArithmeticException や、配列の添字が範囲外であることを表す例外 ArrayIndexOutOfBoundsException などがあります。

Java の場合、Error と RuntimeException は「非チェック例外」といって、try 文で例外処理を記述しなくてもプログラムをコンパイルすることができます。RuntimeException 以外の Exception は「チェック例外」といって、try 文で例外処理を記述しないとコンパイルでエラーになります。ところが、Scala の例外はすべて「非チェック例外」になります。try 文による例外処理を記述しなくてもコンパイルエラーにはなりません。ご注意くださいませ。

●try 式の使い方

try 式の使い方は簡単です。次の例を見てください。

scala> try { 10 / 5 } catch { case e: ArithmeticException => println(e); 0 }
val res0: Int = 2

scala> try { 10 / 0 } catch { case e: ArithmeticException => println(e); 0 }
java.lang.ArithmeticException: / by zero
val res1: Int = 0

try 式の中で割り算を実行します。Scala の場合、0 で除算すると例外 ArithmeticException を送出して実行を中断します。ここで、try 文の catch 節に ArithmeticException を指定すると、例外を捕捉して処理を続行することができます。

10 / 2 は 5 を返しますが、10 / 0 は 0 で除算しているので例外 ArithmeticException が送出されます。この例外クラスは catch 節に指定されているので、その節が実行されて例外クラスのインスタンス e の内容を表示して 0 を返します。

●例外の送出

例外は throw で送出することができます。

throw new ExceptionClass(args, ...)

throw には例外クラスのインスタンスを引数として渡します。throw が実行されると、プログラムの実行を直ちに中断して、例外を受け止める catch に該当する case 節があると、そこへ制御が移ります。該当する case 節がない場合、プログラムの実行は中断されます。

簡単な例を示しましょう。

scala> try { throw new RuntimeException("Oops!") } catch { case e: RuntimeException => println(e) }
java.lang.RuntimeException: Oops!

例外に渡した引数は、例外クラスのインスタンスに格納されます。例外クラスのインスタンスは catch の case 節で受け取ることができます。上記の例では、送出された例外のインスタンスは変数 e にセットされます。例外に渡したメッセージはインスタンスに格納されます。

●例外の定義

例外は他の例外を継承することで、ユーザが独自に定義することができます。Scala の場合、Exception か RuntimeException を継承するといいでしょう。次の例を見てください。

scala> class BarException(mes: String) extends RuntimeException(mes)
// defined class BarException

scala> throw new BarException("oops!")
rs$line$4$BarException: oops!
  ... 33 elided

このように、BarException は RuntimeException を継承しているので、フィールド変数やメソッドを定義しなくてもスーパークラスのコンストラクタを呼び出すだけで動作します。

●大域脱出

Scala の例外は、try の中で呼び出した関数の中で例外が送出されても、それを捕捉することができます。この機能を使って、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exit)」を実現することができます。

簡単な例を示しましょう。

scala> class GlobalExitException extends RuntimeException
// defined class GlobalExitException

scala> def bar1() = println("call bar1")
def bar1(): Unit

scala> def bar2() = throw new GlobalExitException
def bar2(): Nothing

scala> def bar3() = println("call bar3")
def bar3(): Unit

scala> def foo() = { bar1(); bar2(); bar3() }
def foo(): Unit

scala> try { foo() } catch { case e:GlobalExitException => println("Golbal Exit") }
call bar1
Golbal Exit

実行の様子を下図に示します。

 ┌───────┐
 │try { ... }   │←─┐
 │catch { ... } │    │
 └───────┘    │
        ↓             │
 ┌──────┐      │
 │   foo()    │──┐│
 └──────┘    ││
       ↓↑          ↓│
 ┌──────┐  ┌ bar2() ───────────┐ 
 │  bar1()    │  │throw new GlobalExitException │
 └──────┘  └───────────────┘

            図 : 大域脱出

通常の関数呼び出しは、呼び出し元の関数に制御が戻ります。ところが bar2 で throw が実行されると、呼び出し元の関数 foo を飛び越えて、制御が try の catch に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。

●finally 節

ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。このような場合、try に finally 節を定義します。finally 節は try の処理で例外が発生したかどうかにかかわらず、try の処理が終了するときに必ず実行されます。例外が発生した場合は、finally 節を実行したあとで同じ例外を再送出します。なお、catch 節と finally 節を同時に try 文に書く場合は、catch 節を先に定義してください。そのあとで finally 節を定義します。

簡単な例を示しましょう。大域脱出で作成した foo を呼び出す関数 baz を作ります。

scala> def baz() = try { foo() } finally { println("clean up") }
def baz(): Unit

scala> try { baz() } catch { case e:GlobalExitException => println("Golbal Exit") }
call bar1
clean up
Golbal Exit

関数 bar2() で送出された例外 GlobalExitException は baz() の finally 節で捕捉されて println("clean up") が実行されます。その後、例外 GlobalExitException が再送出され、try の catch 節に捕捉されて Global Exit と表示されます。


初版 2014 年 8 月 24 日
改訂 2024 年 12 月 20 日