はじめに
この記事では各言語でよく使われる「例外処理」における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
が使われたらどんな結果が招かれるか理解しておく必要があるでしょう。
参考&もっと読む