finally連鎖が止まらない
注意
(12/4追加)
コンパイラのバグでした。
fixされている模様です。
概要
finallyの中でenclosing関数の再帰呼び出しをするのは非常に危険。
知識
Scalaのfinallyは、その中で例外が発生すると
なぜかfinallyを最初からもう一度繰り返すらしい。
package finallytest object FinallyTest { def main(args : Array[String]) : Unit = { try {} finally { println("finally") throw new Throwable() println("after throw") } } }
実行結果
finally finally java.lang.Throwable at finallytest.FinallyTest$.main(FinallyTest.scala:7) at finallytest.FinallyTest.main(FinallyTest.scala) ...(以下略)
"finally"の出力が2回繰り返されているが、
"after throw"の出力は実行されていないことがわかる。
問題の現象
これを応用して、
関数の中のfinallyの中で再帰呼びだしすると、大変なことになる。
package finallytest object FinallyRec { def main(args : Array[String]) : Unit = { //no problem // finally_rec(100) finally_rec(10000) } def finally_rec(n : Int){ if(n < 0){ return } println(n) try {} finally { finally_rec (n - 1) } } }
実行結果
10000 9999 ...(略) 503250325038 5037 5036 5035 5034 5033 503250325033 503250325034 5033 503250325033 503250325035 5034 5033 503250325033 503250325034 5033 503250325033 503250325036 5035 5034 5033 503250325033 503250325034 5033 ...(止まらず)
考察
この原理をよく分かっていないが、
引数が100程度ならば問題は起こらないので、
スタックオーバーフローが原因と考えている。
関数の呼び出し先でスタックオーバーフロー例外 ->
finallyの中をもう一度実行 →
やはり関数呼び出し先でスタックオーバーフロー例外 →…
という連鎖に陥っていると考えている。
しかしなぜ5032〜5038と、幅があるのだろうか?
ただfinally中でスタックオーバーフローしただけでは、この問題は起こらない。
finallyの中でfinallyを含む自分自身を呼び出すのが重要らしい。
以下は問題にならない例。
package finallytest object FinallyRec2 { def main(args : Array[String]) : Unit = { try {} finally { rec(10000) } } def rec(n : Int) { if (n < 0){ return } println(n) rec(n - 1) () } }
この場合、10000からスタックオーバーフローするまで出力を続ける、を2回繰り返して止まる。
recの最後のユニット()は、
末尾再帰では最適化のせいでスタックオーバーフローしないので、その対策(笑)。