LoginSignup
19
12
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

例外処理におけるfinally(ensure)は意味ない……なんて思ったら大間違い

Last updated at Posted at 2024-01-30

はじめに

この記事では各言語でよく使われる「例外処理」における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なしでrescueensureだけ使うこともできますが、これについては本題と関係ないので省略します。

以下はDart、Python、Rubyのコードで解説しますが、Dartの例外処理文はJavaScript、TypeScript、C#、Java、PHPとも大体似ているので、これらの言語を使っている方はDartのコードを見たら理解できると思います。

finallyは要らなくない?

まずこの記事を書くきっかけから説明します。

普段例外処理を行う時にこのようにtrycatchを使いますね。

dart
try {
  // 何かの実行
}
catch (e) {
  // try節でエラーが出たらここは実行する
}
python
try:
    # 何かの実行
except:
    # try節でエラーが出たらここは実行する
ruby
begin
  # 何かの実行
rescue
  # try節でエラーが出たらここは実行する
end

こう書いたらtry節の中で何かエラーが発生したらそのエラーがすぐ処理されて実際に現れず、その代わりにcatch節で書いた内容が実行される、という仕組みになりますね。

普段はこれだけ使うのは殆どですが、それに加えてfinally節をくっつけることができますが、これは必須ではないので、使われる場面が少ないはずです。例外処理を書いたことがあってもfinallyを使ったことない人も多いのではないでしょうか。

finallyの意味は「エラーが出ても出なくても実行する」と説明する記事や教科書も多いようですね。その説明は間違いなわけではないが、全然足りないのです。

もしそれだけだったらfinallyの必要性を全然感じません。

なぜならわざわざfinallyを書かなくてもtrycatch節の下に書いたら普段は実行されるから。

例えば:

dart
try {
  print("やるぞ!");
  // エラーが懸念される実行
  throw Exception();
}
catch (e) {
  print("エラーが出た!");
  // 例外が起きたら実行すること
}
finally {
  print("やっと終わった");
  // ここは例外起きても起きなくても必ず実行する
}

print("次は……");
// ここも例外起きても起きなくても実行するはず
python
try:
    print("やるぞ!")
    # エラーが懸念される実行
    raise Exception
except:
    print("エラーが出た!")
    # 例外が起きたら実行すること
finally:
    print("やっと終わった")
    # ここは例外起きても起きなくても必ず実行する
print("次は……")
# ここも例外起きても起きなくても実行するはず
ruby
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節の中に書いたコードは、こんな場合でも実行されます!

例えば:

dart
try {
  throw Exception();
}
catch (e) {
  throw e;
}
finally {
  print("ここは実行される");
}
print("ここは実行されない");
python
try:
    raise Exception
except Exception as e:
    raise e
finally:
    print("ここは実行される")
print("ここは実行されない")
ruby
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を使う必要があります。

dart
try {
  // ここで何かする
}
catch (e) {}
python
try:
    # ここで何かする
except:
    pass
ruby
begin
  # ここで何かする
rescue
end

ここでcatchの部分を書かないと構文エラーになります。

(なお、Rubyではbeginだけで問題ないが、そう書いても例外処理にならない)

ruby
begin
  raise # 構文エラーではないが、普通にエラーが出る
end

ただし、実はこれはcatchではなく、finallyでもいいです。つまりこう書いたらcatchがなくても構文エラーになりません。

dart
try {
  // ここで何かする
}
finally {}
python
try:
    # ここで何かする
finally:
    pass
ruby
begin
  # ここで何かする
ensure
end

ただしcatchがないから、結局try節の中でエラーが発生した場合、処理されずそのままエラーになります。

finallyはあくまで必ず実行することを確保するためであり、エラーを止める力がありません。

ということで、「エラーになるならそのままエラーにしてもいいが、それでも必ずやっておきたいことがある」という場合に使うです。

もしこう書いたら

dart
try {
  // 何かの実行
}
finally {
  // try節でエラーが出ても実行しておきたいこと
}
python
try:
    # 何かの実行
finally:
    # try節でエラーが出ても実行しておきたいこと
ruby
begin
  # 何かの実行
ensure
  # try節でエラーが出ても実行しておきたいこと
end

それはこう書くのと同じ。

dart
try {
  // 何かの実行
}
catch (e) {
  // try節でエラーが出ても実行しておきたいこと
  throw e;
}
python
try:
    # 何かの実行
except Exception as e:
    # try節でエラーが出ても実行しておきたいこと
    raise e
ruby
begin
  # 何かの実行
rescue => e
  # try節でエラーが出ても実行しておきたいこと
  raise e
end

ということでデバッグの時など、エラーを取り消したくはない場合try catchよりtry finallyを使った方がいいかもしれません。

finallyが関数内で使われる場合

もう一つ気をつけるべきなのは関数内でfinally使う場合です。

もしエラーが発生するならfinallyを使ってもそのまま発生して止めることはできない……と、上記でそう説明しましたが、関数の中で使う場合話はまたややこしくなります。

実はもし関数内でfinally節の中でreturnしたら、エラーが消えて通常に動くことになります。

例えば:

dart
kansuu() {
  try {
    throw Exception();
  }
  finally {
    return;
  }
}

main() {
  kansuu();
}
python
def kansuu():
    try:
        raise Exception
    finally:
        return

kansuu()
ruby
def kansuu
  begin
    raise
  ensure
    return
  end
end

kansuu

こんな関数にエラーは出ません。

catchを使ってその中でエラーが出る場合でも同じようにエラーが取り消されます。

このようにfinally節の中でreturnを書いたら絶対にエラーが出ない関数になります。(Dartの場合main()の中でも通用します)

ただし関数の中のfinally節でreturnの使用を避けるべきという主張の人もいるから、こんな使い方はしない方がいいと思います。

tryでreturnする場合

もし関数の中でtry finallyを使って、tryの中でreturnが実行される場合でもfinally節の内容も実行される。

dart
kansuu() {
  try {
    return;
  }
  finally {
    print("ここは実行される");
  }
}

main() {
  kansuu();
}
python
def kansuu():
    try:
        return
    finally:
        print("ここは実行される")

kansuu()
ruby
def kansuu
  begin
    return
  ensure
    puts "ここは実行される"
  end
end

kansuu

この場合finally節に書くのはreturnの前に書くのと同じような意味になりますが、finally節の中で書くことで、もし関数の中で例外が起きた場合でも必ず実行する、ということになります。

catchでreturnする場合

tryreturnがある場合と同じように、catch節の中でreturnする場合もfinallyは実行されます。

dart
kansuu() {
  try {
    throw Exception();
  }
  catch (e) {
    return;
  }
  finally {
    print("ここは実行される");
  }
}

main() {
  kansuu();
}
python
def kansuu():
    try:
        raise Exception
    except:
        return
    finally:
        print("ここは実行される")

kansuu()
ruby
def kansuu
  begin
    raise
  rescue
    return
  ensure
    puts "ここは実行される"
  end
end

kansuu

finallyのreturnは優先される

ではもしtryの中でもcatchの中でもfinallyの中でもreturnがある場合どうなるか、試してみませんか。

dart
kansuu(x) {
  try {
    if (x == 0)
      return "頑張る";
    else
      throw Exception();
  }
  catch (e) {
    return "エラー";
  }
  finally {
    return "終わり";
  }
}

main() {
  print(kansuu(0)); // 終わり
  print(kansuu(1)); // 終わり
}
python
def kansuu(x):
    try:
        if(x == 0):
            return "頑張る"
        else:
            raise Exception
    except:
        return "エラー"
    finally:
        return "終わり"

print(kansuu(0)) # 終わり
print(kansuu(1)) # 終わり
ruby
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は必ず実行されるから、たとえその前にtrycatchreturnが行われても、最終的にfinallyreturnすれば上書きされます。

このようにfinallyは最強ですね。

終わりに

以上の解説と例でfinallyが使われる意味はわかるようになったでしょうか。

最初は意味ないと思っていたものは実は意外と意味深いもので面白いですよね。

確かにfinallyの出番は少ないです。私も今まで使ったことありません。

それでも自分の書いたコードで使うかどうかは別として、誰かのコードの中でfinallyが使われたらどんな結果が招かれるか理解しておく必要があるでしょう。

参考&もっと読む

19
12
23

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
12