finallyの挙動を正しく理解する、returnとリソース解放の交差点
finallyとは、try/catchで例外の有無にかかわらず必ず実行されるブロックです。リソースの後始末に使いますが、中でreturnするとtry側の返り値を上書きします。
finallyは例外があってもなくても必ず通る
finally の一番の役割は「成功しても失敗しても必ず実行される」ことですね。ファイルハンドルやロックのような、確保したら必ず解放したいものの後始末に向いています。try の中で例外が飛んでも finally は通るので、解放漏れを防げます。
$fp = fopen('data.csv', 'r');
try {
return $this->parse($fp); // 途中で例外が出ても
} finally {
fclose($fp); // ここは必ず実行される
}
try の中に return があっても finally はその後に実行される、という点が肝で、ここを知らないと解放タイミングを誤解しがちだと思います。
finallyの中のreturnはtryのreturnを上書きする
ここが落とし穴なんですが、finally の中で return を書くと、try や catch で返そうとしていた値を上書きしてしまいます。しかも投げられていた例外まで握りつぶすので、意図しない挙動になりやすいです。finally では基本的に return しない、と決めておくのが安全だと思います。
function test(): string
{
try {
return 'try';
} finally {
return 'finally'; // これが勝つ。'try' は捨てられる
}
}
echo test(); // finally
返り値を評価してからfinallyが走る順番
もう少し細かい話をすると、try の return 式は先に評価されて値が確定し、その後 finally が実行され、最後にその確定済みの値が返ります。なので finally 内で変数をいじっても、既に確定した返り値は変わりません。ここも直感と少しずれるところですね。
function counter(): int
{
$n = 1;
try {
return $n; // ここで 1 が確定
} finally {
$n = 99; // 返り値には影響しない
}
}
echo counter(); // 1
まとめ
finally は後始末を確実に走らせるための仕組みで、リソース解放に使うのが本筋だと思います。ただし finally の中で return すると try 側の返り値も例外も上書きしてしまうので、そこは避ける。返り値は try 時点で確定して finally はその後に走る、という順番を押さえておけば、挙動に驚かされることは減る気がします。
よくある質問
Q. finallyは必ず実行されますか?
A. はい。tryの中で例外が飛んでも、returnがあっても、finallyは必ず通ります。リソース解放に向いています。
Q. finallyの中でreturnしてもいいですか?
A. 避けたほうが安全です。try側の返り値も、投げられていた例外も上書きして握りつぶしてしまいます。
Q. try内でreturnした値をfinallyで変えられますか?
A. 変えられません。return式が先に評価されて値が確定し、その後finallyが走るので、変数をいじっても返り値は変わりません。