はじめに
この記事では各言語でよく使われる「例外処理」におけるfinally(Rubyではensure)という構文の使い方と必要性について説明します。
例外処理に関しては殆どの言語で通用する話なので、今回はどの特定の言語の話でもなく、できるだけ一般的に語るつもりですが、説明するためのサンプルコードはDart、Python、Rubyで表示することにします。
各言語の例外処理
色んな言語には「例外処理」という構文があります。その中で殆どは「try catch finally throw」と表記しますが、PythonとRubyなど違う表記が使われます。
各言語の例外処理の表記を纏めてみるとこのようになります。
| 言語 | 最初に実行 | 例外があれば実行 | 最後に絶対実行 | 例外を引き起こす |
|---|---|---|---|---|
| C# | try | catch | finally | throw |
| Java | ||||
| JavaScript | ||||
| TypeScript | ||||
| PHP | ||||
| Dart | ||||
| Python | except | raise | ||
| Ruby | begin | rescue | ensure |
簡潔のためにここからは一番一般的である「try catch finally throw」だけ表記します。Rubyを使っている方は頭の中で変換しておいてください。
実際にRubyのbegin文は例外処理のこと以外にも使われますし、beginなしでrescueとensureだけ使うこともできますが、これについては本題と関係ないので省略します。
以下はDart、Python、Rubyのコードで解説しますが、Dartの例外処理文はJavaScript、TypeScript、C#、Java、PHPとも大体似ているので、これらの言語を使っている方はDartのコードを見たら理解できると思います。
finallyは要らなくない?
まずこの記事を書くきっかけから説明します。
普段例外処理を行う時にこのようにtryとcatchを使いますね。
try {
// 何かの実行
}
catch (e) {
// try節でエラーが出たらここは実行する
}
try:
# 何かの実行
except:
# try節でエラーが出たらここは実行する
begin
# 何かの実行
rescue
# try節でエラーが出たらここは実行する
end
こう書いたらtry節の中で何かエラーが発生したらそのエラーがすぐ処理されて実際に現れず、その代わりにcatch節で書いた内容が実行される、という仕組みになりますね。
普段はこれだけ使うのは殆どですが、それに加えてfinally節をくっつけることができますが、これは必須ではないので、使われる場面が少ないはずです。例外処理を書いたことがあってもfinallyを使ったことない人も多いのではないでしょうか。
finallyの意味は「エラーが出ても出なくても実行する」と説明する記事や教科書も多いようですね。その説明は間違いなわけではないが、全然足りないのです。
もしそれだけだったらfinallyの必要性を全然感じません。
なぜならわざわざfinallyを書かなくてもtryとcatch節の下に書いたら普段は実行されるから。
例えば:
try {
print("やるぞ!");
// エラーが懸念される実行
throw Exception();
}
catch (e) {
print("エラーが出た!");
// 例外が起きたら実行すること
}
finally {
print("やっと終わった");
// ここは例外起きても起きなくても必ず実行する
}
print("次は……");
// ここも例外起きても起きなくても実行するはず
try:
print("やるぞ!")
# エラーが懸念される実行
raise Exception
except:
print("エラーが出た!")
# 例外が起きたら実行すること
finally:
print("やっと終わった")
# ここは例外起きても起きなくても必ず実行する
print("次は……")
# ここも例外起きても起きなくても実行するはず
begin
puts "やるぞ!"
# エラーが懸念される実行
raise
rescue
puts "エラーが出た!"
# 例外が起きたら実行すること
ensure
puts "やっと終わった"
# ここは例外起きても起きなくても必ず実行する
end
puts "次は……"
# ここも例外起きても起きなくても実行するはず
つまり、例外問わず次に実行したいことはわざわざfinally節を作って中に書く意味はありません。
確かにfinallyで書いた方がtryの内容と密接な関係があるとわかりやすいかもしれませんが、それだけのことでしょうね。
こういうことなので、私も最初に勉強した時にfinallyの必要性を感じなくて無視していましたが、結局これは色んな言語に搭載されている機能だからやはり何か意味があるはずでしょう。
実はPHPだって最初はfinallyがなかったが、バージョン5.5から追加されたらしいです。
C++でもfinallyはないが、C#になるとfinallyがあります。
その必要性について察して、色々検索したらやっとわかりました。
以下は私が見つけたfinallyの本当の意味を解説していきます。
catchの中で例外が発生する場合
普段try節の中でエラーが出たらcatch節に入ることでエラーが治まって、そこで例外処理が始まりますが、もしそのcatchの中でまた何かのエラーが出たらどうなりますか?
この場合はやはり実際にエラーが出てプログラムは止まってしまいますね。そしてその下に書いたコードは当然ながら実行されません。
しかしfinally節の中に書いたコードは、こんな場合でも実行されます!
例えば:
try {
throw Exception();
}
catch (e) {
throw e;
}
finally {
print("ここは実行される");
}
print("ここは実行されない");
try:
raise Exception
except Exception as e:
raise e
finally:
print("ここは実行される")
print("ここは実行されない")
begin
raise
rescue => e
raise e
ensure
puts "ここは実行される"
end
puts "ここは実行されない"
この例でfinallyを使う意味はやっとはっきりとわかりますね。
結局エラーが発生してプログラムが止まることに変わりないのですが、エラーが出る前にfinally節は実行されます。
「finallyは例外が起きても起きなくても必ず実行される」という説明ですが、これはcatchの中で例外が起きる場合も含まれます。
例外処理のことを教える時にfinallyを話に出したらこの場合の説明も必要だと思います。ここまで説明しないとfinallyを使う意味は伝わらないから。
だから「例外処理の途中でまた例外が起きても必ず最後に実行する部分」という説明の方がfinallyの適切な解説だと思います。
こうやってfinallyを使うと、最悪の場合でも何があっても必ず実行されるという保証にもなります。
どうしても実行しておきたい大切なことはfinallyに書いたらいいかもしれません。
なお、世の中何が起きるかわからないから、結局finallyを使っても実行されない場合はいくつかあるでしょう。
例えばこの記事の場合です。
catchを省略する場合
以上finallyの必要性について説明しましたが、実際にfinallyに関して説明しておくべきことがまだあります。
まず実はfinallyを書くと、catchを書かなくてもいいということになっているのです。
普段try文を使う時、tryだけ書いてはいけず、catchもセットで一緒にしますね。たとえ処理したいことがなくても空っぽのcatchを使う必要があります。
try {
// ここで何かする
}
catch (e) {}
try:
# ここで何かする
except:
pass
begin
# ここで何かする
rescue
end
ここでcatchの部分を書かないと構文エラーになります。
(なお、Rubyではbeginだけで問題ないが、そう書いても例外処理にならない)
begin
raise # 構文エラーではないが、普通にエラーが出る
end
ただし、実はこれはcatchではなく、finallyでもいいです。つまりこう書いたらcatchがなくても構文エラーになりません。
try {
// ここで何かする
}
finally {}
try:
# ここで何かする
finally:
pass
begin
# ここで何かする
ensure
end
ただしcatchがないから、結局try節の中でエラーが発生した場合、処理されずそのままエラーになります。
finallyはあくまで必ず実行することを確保するためであり、エラーを止める力がありません。
ということで、「エラーになるならそのままエラーにしてもいいが、それでも必ずやっておきたいことがある」という場合に使うです。
もしこう書いたら
try {
// 何かの実行
}
finally {
// try節でエラーが出ても実行しておきたいこと
}
try:
# 何かの実行
finally:
# try節でエラーが出ても実行しておきたいこと
begin
# 何かの実行
ensure
# try節でエラーが出ても実行しておきたいこと
end
それはこう書くのと同じ。
try {
// 何かの実行
}
catch (e) {
// try節でエラーが出ても実行しておきたいこと
throw e;
}
try:
# 何かの実行
except Exception as e:
# try節でエラーが出ても実行しておきたいこと
raise e
begin
# 何かの実行
rescue => e
# try節でエラーが出ても実行しておきたいこと
raise e
end
ということでデバッグの時など、エラーを取り消したくはない場合try catchよりtry finallyを使った方がいいかもしれません。
finallyが関数内で使われる場合
もう一つ気をつけるべきなのは関数内でfinally使う場合です。
もしエラーが発生するならfinallyを使ってもそのまま発生して止めることはできない……と、上記でそう説明しましたが、関数の中で使う場合話はまたややこしくなります。
実はもし関数内でfinally節の中でreturnしたら、エラーが消えて通常に動くことになります。
例えば:
kansuu() {
try {
throw Exception();
}
finally {
return;
}
}
main() {
kansuu();
}
def kansuu():
try:
raise Exception
finally:
return
kansuu()
def kansuu
begin
raise
ensure
return
end
end
kansuu
こんな関数にエラーは出ません。
catchを使ってその中でエラーが出る場合でも同じようにエラーが取り消されます。
このようにfinally節の中でreturnを書いたら絶対にエラーが出ない関数になります。(Dartの場合main()の中でも通用します)
ただし関数の中のfinally節でreturnの使用を避けるべきという主張の人もいるから、こんな使い方はしない方がいいと思います。
tryでreturnする場合
もし関数の中でtry finallyを使って、tryの中でreturnが実行される場合でもfinally節の内容も実行される。
kansuu() {
try {
return;
}
finally {
print("ここは実行される");
}
}
main() {
kansuu();
}
def kansuu():
try:
return
finally:
print("ここは実行される")
kansuu()
def kansuu
begin
return
ensure
puts "ここは実行される"
end
end
kansuu
この場合finally節に書くのはreturnの前に書くのと同じような意味になりますが、finally節の中で書くことで、もし関数の中で例外が起きた場合でも必ず実行する、ということになります。
catchでreturnする場合
tryでreturnがある場合と同じように、catch節の中でreturnする場合もfinallyは実行されます。
kansuu() {
try {
throw Exception();
}
catch (e) {
return;
}
finally {
print("ここは実行される");
}
}
main() {
kansuu();
}
def kansuu():
try:
raise Exception
except:
return
finally:
print("ここは実行される")
kansuu()
def kansuu
begin
raise
rescue
return
ensure
puts "ここは実行される"
end
end
kansuu
finallyのreturnは優先される
ではもしtryの中でもcatchの中でもfinallyの中でもreturnがある場合どうなるか、試してみませんか。
kansuu(x) {
try {
if (x == 0)
return "頑張る";
else
throw Exception();
}
catch (e) {
return "エラー";
}
finally {
return "終わり";
}
}
main() {
print(kansuu(0)); // 終わり
print(kansuu(1)); // 終わり
}
def kansuu(x):
try:
if(x == 0):
return "頑張る"
else:
raise Exception
except:
return "エラー"
finally:
return "終わり"
print(kansuu(0)) # 終わり
print(kansuu(1)) # 終わり
def kansuu(x)
begin
if(x == 0)
return "頑張る"
else
raise
end
rescue
return "エラー"
ensure
return "終わり"
end
end
puts kansuu(0) # 終わり
puts kansuu(1) # 終わり
結局finallyの中のreturnはどうしても優先されます。
finallyは必ず実行されるから、たとえその前にtryやcatchでreturnが行われても、最終的にfinallyがreturnすれば上書きされます。
このようにfinallyは最強ですね。
終わりに
以上の解説と例でfinallyが使われる意味はわかるようになったでしょうか。
最初は意味ないと思っていたものは実は意外と意味深いもので面白いですよね。
確かにfinallyの出番は少ないです。私も今まで使ったことありません。
それでも自分の書いたコードで使うかどうかは別として、誰かのコードの中でfinallyが使われたらどんな結果が招かれるか理解しておく必要があるでしょう。
参考&もっと読む