1. finallyブロックとの出会い
リソースのクリーンアップと解放
想像してみて:君が化学実験室で実験してて、全部終わった後(もしくはフラスコを何個か爆発させちゃった後でも)、必ず作業台を片付けて、薬品を流して、電気を消さなきゃいけないよね。finallyブロックはまさにそのためにあるんだ。tryやcatchの後、例外があってもなくても、必ず実行される部分なんだよ。
try
{
// ここに「危険な」コードを書く。例外が発生するかも
}
catch
{
// ここで例外をキャッチして処理する
}
finally
{
// このコードは必ず実行される。例外があってもなくても
}
なんで必要なの? 一番の理由は、リソースを確実に解放するため。ファイルを閉じたり、データベース接続を切ったり、サーバールームのドアのロックを解除したり…。finallyがなかったら、エラーの後にリソースが「ぶら下がったまま」になることもあって、それは本当に困る。例えば、ファイルがロックされたままで、管理者でも開けなくなっちゃうとかね。
全部catchに書いちゃダメなの?
リソースの解放をcatchに書けばいいんじゃない?理論的にはできる。でも、何も問題がなかった場合、catchは実行されない。リソース解放を「絶対に」やりたいなら、finallyが必要なんだ。
現実の世界でも、成功でも失敗でも、片付けは必須な作業ってあるよね。finallyはまさにそのために生まれたんだ。
2. finallyブロックの特徴
finallyが実行されない時ってある?
これはトリックな質問!finallyは必ず実行される。tryブロックでreturn(メソッドから早く抜けるやつ)を使っても、新しい例外を投げても、finallyは絶対に実行されるんだ。
static void Test()
{
try
{
Console.WriteLine("リターン前");
return;
}
finally
{
Console.WriteLine("finallyはやっぱり実行されるよ!");
}
}
//Test()の呼び出し
Test();
出力はこうなる:
// リターン前
// finallyはやっぱり実行されるよ!
でも、もしアプリが「強制終了」されたら(例えばPCの電源が落ちた、プロセスがkillされた、CLR自体が死んだとか)、finallyは実行されない。これはもうどうしようもないね。
finally演算子とコールスタック
コールスタックについては次のレクチャーで詳しくやるけど、今はざっくり説明すると、呼び出されたメソッドの積み重ねみたいなもので、プログラムがcatchを見つけるまで「降りていく」感じだよ。
もう一つ大事なポイント:tryで例外が発生して、catchでキャッチされなかった場合(例えば、適切なハンドラがない時)、プログラムは今のメソッドを抜けて、コールスタックを上に進んでいく。その時、各レベルで必ずfinallyブロックが実行されるんだ。
3. throw演算子:自分で例外を投げる方法
throwって何?なんで必要?
例外をキャッチするだけじゃ足りない時もあるよね。自分でエラーを作って「外に投げたい」時、throw演算子を使うんだ。
throw new Exception("これは自分の特別なエラーだよ!");
throwはCLRに「ここでヤバいことが起きたから、自分のExceptionを投げるよ。あとは呼び出し元でなんとかして!」って伝える感じ。
新しい例外を作らずにthrowする
throw;はcatchブロックの中で使えて、今キャッチした例外をもう一回投げ直す時に使う。例えば、エラーの一部だけ処理して、あとは上のコードに任せたい時とかね。
try
{
DangerousOperation();
}
catch (Exception ex)
{
LogError(ex);
throw; // 今の例外をもう一度投げる。コールスタックはそのまま
}
もしthrow ex;って書いたら、コールスタックの情報が消えちゃう。これは良くない書き方だよ。
4. finallyとthrowの組み合わせは?
例外を投げてもfinallyは実行される
じゃあ、tryの中でthrowが発生して、finallyもある場合どうなるか見てみよう:
try
{
Console.WriteLine("エラー前…");
throw new Exception("try中にエラー発生!");
}
catch
{
Console.WriteLine("Catchがエラーをキャッチした。");
}
finally
{
Console.WriteLine("Finallyが実行された。");
}
結果はこうなる:
エラー前…
Catchがエラーをキャッチした。
Finallyが実行された。
もし適切なcatchがなくても、finallyはプログラムが落ちる前に必ず実行されるよ。
細かい話:finallyの中でもthrowしたらどうなる?
finallyの中でthrowが発生した場合、その例外が前の例外を上書きしちゃう。つまり、try/catchで何が起きたかの情報は消えちゃう。だから、finallyの中でthrowするのは、すでに例外が発生してる場合はおすすめしないよ。
try
{
throw new Exception("tryでエラー");
}
finally
{
throw new Exception("finallyでエラー");
}
// 結果:「finallyでエラー」だけが外に出る
5. 実践的なアドバイスとよくあるミス
初心者がよくやるミス
- リソース解放の時にfinallyブロックを使わず、catchだけに頼っちゃう。
- 新しい例外を発生させるかもしれないコードをfinallyの中に書いちゃう。これで予想外のエラーが起きる。
- returnがtryの中にあっても、finallyは「スキップされない」ってことを忘れがち。必ず実行されるよ。
finallyの代わり:どっちを使う?
using構文(これは後で詳しくやるよ)が登場してから、リソース解放はもっと楽になった。でも実は、usingの裏側でもfinallyが使われてるんだ。ちょっと特殊なケース(例えばロック解除やエラー通知の送信とか)では、やっぱりfinallyを使うことになるよ。
面接でfinallyがよく出る理由
一度でも高負荷なサーバーを書いたことがあるリクルーターなら、finallyについて絶対に質問してくる。「tryの中にreturnがあって、finallyでthrowしたらどうなる?」とか「例外発生時にリソース解放は保証される?」とかね。これで君は答えだけじゃなくて、ちゃんと仕組みも説明できるようになった。finallyがどう動くかだけじゃなくて、なんで必要なのか、いつ使うべきか、そして本気のコードには絶対欠かせない理由まで語れるよ!
GO TO FULL VERSION