Dartツアー(4)制御文、例外

技術,プログラミング言語

Flutterを始めてみましたが、コードはDart言語で書きます。という訳で、Dart言語のツアーを見て勉強しながら、自分用にメモをまとめてみました。今回は制御文と例外処理です。

前回分は以下から。

制御文

Dartには以下の制御文があります。

  • ifとelse
  • forループ
  • whileとdo-whileループ
  • breakとcontinue
  • switchとcase
  • assert

この他に次に触れる例外処理も一種の制御文になります。

蛇足ですが直前にやったGo言語ではforしかループがなかったりするのと比べれば非常に充実しています。(どちらが正しいとかではありません)

ifとelse

Dartでは任意でelseを持つif文がサポートされています。まあ、どんな言語でもサポートされてます。一番重要なのは条件式は論理式でないといけない、ということです。ツアーにはなぜかelse ifの話が書いてありませんが、当然のように使えます。

forループ

  • 標準的なforループで反復処理が可能である。
  • Dartのforループの内側のクロージャはインデックス値を保持している
    • JavaScriptでよく見られる落とし穴を回避できる
  • 反復処理するオブジェクトがIterable(反復可能)で現在のカウンター値が不要ならばfor-in形式のループを使う。

whileとdo-whileループ

  • whileループはループの前に条件判断する
  • do-whileループはループの後に条件判断する

break文とcontinue文

  • ループの中断はbreakを使う
  • 次の反復処理までスキップするにはcontinueを使う

switch文とcase節

  • Dartのswitch文は整数、文字列、コンパイル時定数を等号(==)で比較する
    • 比較するオブジェクトは全て同じクラスのインスタンスでなければならない
    • クラスは等号(==)をオーバーライドしてはいけない
    • 等号以外の条件チェックはDartではできない
  • 列挙型はswitch文でもうまく機能する
  • 空でないcase節は原則としてbreak文で終了する
    • 他にはcontinue文、throw文、return文が使える
    • これらがないとビルドエラーになる
  • 空のcase節は空でない最初のcase節が実行される
  • case節の中は別スコープ、switchの外や、他のcase節とは別扱いになる

スコープの扱いは注意が必要そうです。switch文は長くなりがちなので、誤って外にある変数を再宣言してもエラーにならないし、意図しない動作をする可能性があります。

Assert文

  • 開発時はassert文を使用して、条件式が偽である場合に通常の実行を中断できる
  • 第1引数は論理式でなければならない
  • 第2引数でメッセージを追加できる
  • assert文が有効かどうかはケースによる(例えば以下のケースでは有効)
    • Flutterではデバッグモードで有効
    • dartdevcのような開発専用ツールはデフォルトで有効
    • dart runやdart2jsなどのツールでは、–enable-assertsで有効
  • 運用時はassert文は無視され、引数の式の評価はしない

例外

  • Dartでは例外をスローしたりキャッチしたりが可能である
  • 例外とは予期せぬことが起きたことを示すエラーである
  • 例外をキャッチしていない場合、例外を発生させたアイソレートは中断される
    • 通常、アイソレートとそのプログラムは終了する
  • Dartの例外は全て非チェック例外である(Javaとは違う)
    • メソッドは例外を宣言しなくて良い
    • 呼び出し元はキャッチしなくても良い
  • Dartの例外にはException型とError型に分けられる
    • 多数の定義済サブタイプがある
    • 独自の例外型の定義も可能である
  • ExceptionとErrorだけでなく、nullでないオブジェクトもスローできる

アイソレートについては、別のところで説明されています。大雑把に言えば特殊なスレッドと考えて良さそうです。つまり、例外が発生した場合、当該アイソレートが中断するのはもちろん、記述の仕方によってはそれを含むプログラム全体が、他のアイソレートともども終了してしまうということかと思います。

モバイルプラットフォームを含めて多くのコンピュタはマルチコアCPUを搭載している。これらの全てのコアを活用するために、開発者は伝統的に並行動作する共有メモリを用いたスレッド処理を使う。しかし、実行状態の共有はエラーを引き起こしやすく、コードが複雑になりがちである。

全てのDartコードはスレッドの代わりにアイソレートを内部で実行する。各アイソレートは実行可能な一つのスレッドと他のアイソレートとmutableなオブジェクトを共有しません。

(補足)Javaの例外

この節自体がJavaの例外を前提として書いてあるので、そこを理解しないと説明されていない用語が出てきてさっぱりです。

まず、Java(だけではないですが)にはチェック例外(checked exception、検査例外)と非チェック例外(unchecked exception、非検査例外)があります。そして、非チェック例外は実行時例外(RuntimeException)とエラー(Error)に分かれます。これが上で言っている、Dartは非チェック例外だけで、それはException型とError型に分けられる、という部分になります。

また、Javaでは非チェック例外の対応は任意であり、チェック例外の対応は必須です。ですので、Dartでは非チェック例外しかない=例外の宣言やキャッチは任意、となります。

では、チェックとか非チェックとは何か。違いはチェックすべきかどうか、だと思っています。

チェック例外とは、事前にチェックすることで防ぐことができる、あるいは、チェックすることでこのままでは処理できないことを呼び出し元に伝えるものになります。一方で非チェック例外とはそもそも何らかの異常が起きているものを示すもので、プログラムの不具合等で異常が発生しているのが実行時例外、何らかの外的要因によってプログラムが継続できなくなったのがエラーとなります。

想定されるもの、すべきものがチェック例外で、想定できない、そもそも処理が継続できないものが非チェック例外になります。実行が継続できないとは言っても、何らかの方法で軟着陸させる必要があるので、そういう非常時に何とか最低限の中断処理を行うのが本来の例外処理になります。

いくつか例を挙げておきます。一つの考え方ですので、こうしなければならないというものではありません。

内容種類備考
メモリが足りなくなったエラー(非チェック例外)そのまま終了するとデータが失われるのを防ぐため何とかする等。でもメモリが足りないのでどうにもできないことも。
ヌルポ実行時例外(非チェック例外)基本的にはプログラムのバグが起因。直すのが基本。
インデックス範囲外へのアクセス実行時例外(非チェック例外)ヌルポと同様。
入出力エラーチェック例外入出力時にエラーが起きた。例えば、何か入力するところで内容が空だった等。
数字による文字列を期待しているところで、非数字の文字が含まれていたチェック例外文字列を数値に変換する場合とか。
パースエラーチェック例外何らかのルールに従った入力をするところで、それに沿っていないとか。
例外の種類

もちろん、実行時例外の中には処理を継続可能なものもあります。ヌルポなんかは該当部分を無視して無理やり実行を続けることもできるでしょう。が、何か異常が起きているのは確かで、その状態で作成したデータを果たして信用して良いのかという問題もあります。問題がないケースは実行を続けても構わないですが、それを判断するのはプログラマになります。

中にはSQLエラーみたいなどちらもあり得るみたいなものもあるので、非チェック例外なら対応不要というものでもありません。SQLエラーはDBエンジン側の問題でも発生するし、SQL文の文法エラーでも発生するので、どちらもあり得るわけです。それを分けて定義しなかったSQLの問題とも言えますけど。

SQLに限らず、何らかの外部システム連携も同様です。

チェック例外の扱いについてはJavaの問題点として認識されています。少しネットを検索すれば、チェック例外の問題点についての記事が色々と出てきます。Dartに限らずJava以後の言語ではチェック例外を扱わないケースが多いかと思います。中にはGo言語のように例外処理自体を実装しないケースもあります(try-catchがないだけで、異常自体発生時にプログラムを軟着陸させる仕組みはありますし、擬似的にtry-catch的なことをすることも可能、らしい)。
が、Javaに慣れているとユーザ定義型でチェック例外を独自に定義して例外処理してしまうケースも多いかと思います。

throw文

  • exceptionをスローできる
  • 任意のオブジェクトをスローできる
    • 通常はErrorかExceptionを実装した型をスローする
  • throwは式なので式が使えるところにはどこでも書くことができる。

サンプルはcatch節、Finally節とまとめて。

catch節

  • 再スローしなければ例外は伝播しない
  • 例外をキャッチすることでそれを処理するチャンスを得る
  • 型指定した場合、サブタイプも含めてキャッチできる
  • 型を指定しないcatch節では全てのオブジェクトを処理する
  • 複数タイプの例外をスローできるので、複数のcatch節を指定できる
    • 複数のcatch節が該当する場合も、最初に該当するcatch節が処理する
  • onとcatchはどちらかでも両方でも使用できる
    • 例外の種類を指定するのがon
    • 例外処理の中でそのオブジェクトを必要とする場合はcatch
  • catchの引数は2つまで書くことができる
    • 1つ目はスローされた例外オブジェクト
    • 2つ目はスタックトレースオブジェクト
  • 例外処理を伝播する場合はrethrowを使う

finally節

  • 例外発生の有無によらずに実行されるコードはfinally節を使用する
  • catch節が例外にマッチしない場合、finally節が実行されてから例外が伝播する
  • マッチするcatch節がある場合は、その後にfinally節を実行する

コメントにも書きましたが、これは例外がどうキャッチされて処理されるかを見るためのサンプルなので、実際にこんなコードを書いてはダメです。

(補足)例外の種類、再び

ここまで見ると分かるように、Dartでは非チェック例外しかないというより、チェック例外も非チェック例外の中に含めてしまうことで、チェック例外という概念を無くした、というべきかと思います。

例えば、ファイルが存在しない場合にFileSystemExceptionがスローされます。これはIOExceptionのサブクラスですので、Exceptionクラスのサブクラスでもあり、実行時エラー扱いになります。この例外はファイル操作が失敗したときにスローされ、何に失敗したのかはメッセージで確認できます。
これはJavaではIOExceptionとかFileNotFoundExceptionにあたると思われるので、そうするとJavaではチェック例外に相当します。Go言語では関数が複数の値を返すことができ、第2返り値をエラー状況を示すのに使うことにしていますが、DartではJavaでは3種に分けられていた例外を2種にまとめてしまい、しかもJavaで問題になっていたチェック例外を無理やり非チェック例外にしているとも捉えられるので、ちょっとどうかな?という気はします。ただ、それはDart言語をよく分かっていないためで、Dart言語では何か合理的な方法があるのかもしれません。

一応、Dart的には以下の区分けがあります。

  • Exceptionはプログラムの問題ではなく、実行中に異常があった(コード修正が不要)
  • Errorはプログラムの問題(コード修正が必要)

また、例外ハンドリングについても言語仕様ではないですが、以下が推奨されます。

  • on無しでのcatchは避ける:Errorもキャッチしてしまうため(Errorは原則ハンドリングしない)
  • on無しでcatchした場合、errorを必ず出力(ErrorなのかExceptionなのか分かるようにするためと思われる)
  • プログラムの問題であり、修正が必要な場合のみErrorをスロー(主にデバッグ目的に限定している?)
  • Errorやそれを実装した型をcatchしない:
  • catch後に再度例外をスローする場合はrethrowを使う:元の例外のスタックトレースが保持される(余計なスタックトレースが増えない)

色々ありますが、Errorは特別な意図がない限りキャッチしないでそのまま実行時エラーとして処理すべき、という話になってます。例えば、ユーザが入力したファイル名について、存在をチェックしないで読み込みかけて、FileSystemExceptionを拾ってユーザ向けにエラーメッセージを出す、なんてのは却下ということでしょう。

関連する話として、引数に制約がある場合(例えば正の値のみ、とか)に呼び出し元でチェックすべきか、呼び出し先でチェックすべきかというのがあるかと思います。で、結局両方でチェックするという無駄なことをしてみたり。ちゃんと決めれば良いのですが、何となく放置してしまいがちです。(自分だけ?)

どうしても、念の為、とか呟きながら呼び出し先でチェックして返したり。

こういうのは呼び出し元が責任を負うべきで、それを怠って呼び出し先で問題が発生するのはプログラムの不具合であり、例外(Error)を返すべきというのがJavaの検査例外の考え方かと思います。ただ、そのために呼び出し先ではこんな例外を投げるよというのを全部列挙した上に、呼び出し元ではそれを全部キャッチしなければならないという面倒な話になっています。

Dartの場合、ベースの考え方(引数の補償は呼び出し元がすべき)は同じですが、呼び出し先ではいちいちそれを列挙しないし、それをキャッチするかもプログラマに任されているのかと思います。

では同じように最近の言語でアプリ向けであるSwiftだとどうなるのかも調べてみました。こちらはそもそもエラーの内容で分類するのではなく、エラーをどう扱うかで4種に分けているそうです。こういうものはこのタイプという指針はありますが強制ではなく、プログラマがどうしたいかを明示した上でそれに沿って処理するようです。ここでは詳細は触れませんが。正直、内容に合わせて細かく何とかExceptionとか何とかErrorを定義して、これはどうすべき、別のはこうすべき、といちいち定義して…なんてやり方より合理的な気はします。同じ「入出力時のエラー」にしてもその中身によってどう処理したいかは違ってくるし、だからと言ってそれぞれ毎に細かく例外を定義しまくっていたら、それはそれで本質的ではない気はします。

言語仕様はその設計思想や言語開発者の考え方が反映されているので、掘り下げてみると色々考え方の違いが透けて見えて面白いです。

次回

次回はクラスです。

2022-03-30技術Dart,プログラミング言語

Posted by woinary