Quantcast
Browsing Latest Articles All 29 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

Operational Notifications Exception Design / ONED for DevOps

> to English Pages
https://kyowg.blogspot.com/2018/05/operational-notifications-exception.html

1. 要約

このポストでは、開発速度、品質、機会損失などの改善サイクルの向上を目的とした DevOps のサービス運営視点に基づく Web アプリケーションのドメインロジック例外処理設計の一例として、Operational Notifications Exception Design / ONED をご紹介します。
(私的研究論文を抜粋しました。)

2. 注記

2-1. 記事の有効範囲

  • この記事は、ONED の概念の解説が目的のため、検査例外、非検査例外、その他の例外メカニズムなどまでは言及しません。
  • この記事では、Web アプリケーションに限定し、汎用ライブラリ、汎用ツール、ローカルアプリケーションは対象外とします。
  • 開発言語によって構文が異なるため、適宜読み替えてください。
  • 例外の言葉の定義は対象外です。

2-2. ONED 固有の表現

  • ONED
    Operational Notifications Exception Design を “ONED” と定義します。

  • イベント例外クラス
    SyntaxException や ArgumentException などのイベントベースの例外クラスを “イベント例外クラス” と定義します。

  • 例外トリガー
    throw や raise 構文などを “例外トリガー” と定義します。

  • 例外キャプチャ
    catch や resucue や except 構文などを “例外キャプチャ” と定義します。

  • 例外インスタンス
    例外クラスのインスタンスを “例外インスタンス” と定義します。

3. ONED の概念

3-1. 例外クラスの構造

ONED が提唱する例外クラスの構造は以下の通りとなります。
下記の要点と有用性を追って解説していきます。
(構文は開発言語によって適宜読み替えてください。)

Example 1: 例外クラス

InternalCriticalException("messages: HTTP_STATUS_CODE")

3-2. 責任分界点

Example 2: 例外クラス名

InternalCriticalException

Example 2 の例外クラス名の接頭辞 “Internal” は責任分界点を意味します。
ONED で推奨する責任分界点の例は以下の通りとなります。

# Internal
内部システム: 例外が発生したホストの責任
例えば、この事例の場合は Web サーバーなど...

# External
外部システム: 外部システムの責任
例えば、Google APIs のような外部システムなど...

# Medial
中間部システム: 組織内リソースの責任
例えば、Web サーバーからみた DB サーバーなど...

# Terminal
クライアントシステム: クライアントの責任
例えば、Web クライアントや、API クライアント、ユーザーデバイスなど...

このように、責任分界点を引いて、例外発生の責任を明確にしています。
例外発生の責任の明確化の有用性は、システム障害の初動対応におけるリカバリー速度の向上が見込める事です。
もし、アプリケーション開発経験が豊富なトップエンジニアさんは、この解説だけでこれらの有用性がご理解いただけるかもしれません。
要するに、ONED の概念は、サービス運営を意識したコーディングを重視しているということになります。
次のセクションで、ONED における重要度の考え方について解説します。

3-3. 例外レベル

Example 2: 例外クラス名

ExternalCriticalException

上記のミドル Fix の Critical は、例外レベルを表します。
通常、ロギングなどで使われるエラーレベルは、重要度を表すことが多いと思いますが ONED の例外レベルでも重要度によって段階分けされます。

しかし、たまに以下のような質問を受けることがあります。
「どの重要度の例外を配置したら良いかわからない。」
「どの事象が重要でどの事象が重要じゃないかわからない。」
「これらの概念を使いこなすにはある程度の経験が必要だ。」

決してそういうことではありません。
それをいまから解説します。
例えば、「ファイルが存在しない」という例外処理コードを実装している最中だとします。
もし、サービスリリース後にこの例外が発生したら、開発責任者は、全てのケースにおいて緊急メール通知を受けるべきでしょうか?
もし、自動回復処理によって後続処理の継続が可能な例外ならば、緊急メール通知ではなく、ロギングに留める方が生産的という考え方もあるかもしれません。
全てをメール通知する事によって、もしかしたら緊急体制の形骸化を招くかもしれません。
ONED における例外レベルの定義は、運営中に実行されなければならない自動処理の内容に過ぎないという事です。
言い換えれば、運営中にどのような処理を例外ハンドラに実行させておきたいかという事になります。

  • ログを吐きたいか?
  • ユーザーにエラー画面は出力すべきか?
  • 処理は継続するべきか?
  • 緊急メールで通知して欲しいか?
  • 緊急電話で叩き起こして欲しいか?
Level Log Alert Display Processing
Info o 継続
Notice o 開発時 継続
Warning o 運営時 継続
Partial o o 一部 継続
Error o o 運営時 中断
Critical o o 運営時 中断
Fatal o o 運営時 全経路処理中断

これらのことを、例外処理の実装ポイントごとに考えたらいいだけです。
もし、過度な通知だったら、サービスリリース後にキャリブレーションすればいいでしょう。
したがって、ONED の例外レベルは、例外発生時に何の処理をさせておきたいかの段階分けに過ぎず、その段階を言葉で命名したに過ぎません。
極端にいえば、1, 2, 3, 4, 5, 6, 7 でも、A, B, C, D, E, F, G で良いわけですし、Warning 自体に何か特別な意味を含ませているわけではありません。
例外レベルの概念にワードを選択した理由の一つは、軽微な事象は 1 なのか? 7 なのか?重大な事象は A なのか? G なのか?という迷いを無くすためです。
したがって、ワードが持つ意味を起点にして、例外を実装すべきではないという事です。
例えば、「この例外が発生したらこの内容で自動処理しておきたい。この処理内容のレベルの名前は何だっけ?」という流れであるべきという事です。

例外の処理内容 => 重要度 => 例外レベル 

例外レベルの概念にワードを選択したもう一つの理由は、緊急度と混同させないようにするためです。
したがって、あえて重要度という言葉を使用しています。
緊急度の賛否については、次のセクションで解説します。

また、例外レベルの処理内容の定義は、「運営時に実用的なもの」であるべきでしょう。
したがって上記の処理内容の定義は、全汎用的である必要はなく、サービスやプロダクトの必要に応じて適宜変更できるようにすべきでもあります。
(例えば、A サービスでは、処理内容を追加するなど...)
ONED における例外処理コーディングは、「運営の観点に重心を置くべき」という考え方です。

3-4. 緊急度の賛否

例外レベル ≠ 緊急度

ただし、ONED の例外レベルで注意しなければならないのは、重要度であるべきで、緊急度であるべきではないという点です。
なぜなら、Web アプリケーションの例外発生時における緊急度は、運営時の総合判断に委ねられるべきと考えるからである。
運営時の総合判断とは、サービスの運営状況、リソース状況、ユーザーへの影響範囲、データの整合性などを、運営時の状況から総体的に判断したものを指しています。
同じ例外の事象(例えばファイルが存在しない)でも、運営状況よっては緊急対応しないという判断もありうるからです。
したがって、開発段階で定義した緊急度は、運営リカバリー時においては無意味になりかねないという事からも、緊急性こそ運営時で判断されるべきと考えるからです。
例えば、PSR-3 のように Web 言語が推奨するログレベルも emergency レベルが使われ、また緊急性と関係のある解説も並んでいます。
このような緊急性を開発段階だけで定義してしまうことこそ、開発主体志向や、縦割り組織のプロジェクトを育てる要因になってしまうかもしれません。

3-5. イベント例外クラス

ONED では、イベント例外クラスの使用はあまり推奨されていません。

FileNotFoundException

なぜなら、3-3. のセクションでも解説しましたように、イベント駆動でトリガーされる例外クラス名は、その通知内容に責任分解点や例外レベルが明示されないため、ONED で重視している、運営時の初動リカバリーのパフォーマンスが十分に発揮できない可能性があると考えるからです。
例えば FileNotFoundException や ArgumentException などの事象表現については、開発視点によるものであり、それらの事象表現は引数などの例外メッセージに十分含める事ができるでしょう。
もし、イベント例外クラスを使用する場合には、その例外メッセージに責任分解点や例外レベルを含めるなどの実装が推奨されます。

3-5. HTTP ステータスコード

Web アプリケーションの例外処理の場合、HTTP クライアントに対して適切に HTTP ステータスコードをレスポンスできるように、例外トリガーは HTTP ステータスコードを伝達すべきでしょう。

Ruby Example:

raise Exception.new('messages: code')

PHP Example:

throw nex Exception('message', 500)

なぜなら、最上位の例外キャプチャは、HTTP クライアントに対してどのステータスコードを返すべきかを判断できない場合があるからです。
これは、HTTP ステータスコードをセンシティブに取り扱う事が求められる Web API サーバーにとっては重要な事かもしれません。

3-6. 中断処理

Exception ≠ Interruption

ONED では、例外 = 中断という概念はありません。
Web アプリケーションであっても、例外の継続処理は非常に重要な要素と考えるからです。
そこで、継続処理の考え方と実装例を次項で示します。

3-7. 継続処理の実装例

ONED の継続処理とは、例外発生時に必要な回復処理と通知処理などを実施し、通常の後続処理に戻すことを指し、その実現方法は問いません。
以下にその実装例を示します。

Retry Mechanism

Ruby や Python パッケージのような、リトライ機構を使った例外の継続処理後に例外ブロックを再試行する方法。
ただし、同じ階層の例外ブロックにしか使えため注意が必要です。

Goto Mechanism

C 系や、PHP、Go、Python パッケージのような goto 機構を使った例外の継続処理後に例外トリガー付近で宣言したラベルまで処理を戻す方法。
ただし、同じスコープ内でないと goto 機構は効かないため注意が必要です。

例外ブロックを駆使する

回復/継続処理に例外ブロックを駆使する方法。
(Java の場合は検査例外)

回復・継続処理を例外クラス内にラップする

例外クラス内で回復/継続処理をラップする方法。
(例外は最上位で捕捉)

if (! is_file("/dir/file.txt")) {
    // Instance
    $FileNotFound = new FileNotFoundException("Could not find the file.", 500);
    // Recovery processing
    if (! $FileNotFound->makeFile("/dir/file.txt")) {
        // Interrupt processing
        throw $FileNotFound;
    }
    // Continue processing
    $FileNotFound->log("Notice");
    $FileNotFound->mail("Notice");
}
// Standard processing

継続・中断処理を例外クラス内にラップ

例外クラス内で継続/中断処理をラップする方法。
(throw は最上位で捕捉)

if (! is_file("/dir/file.txt")) {
    // Try recovery processing
    if (! touch("/dir/file.txt")) {
        // Interrupt processing
        new InternalCriticalException("Could not create the file.", 500);
    }
    // Continue processing
    new InternalNoticeException("Could not find the file.", 500);
}
// Standard processing

class InternalNoticeException extends X
{
    const DISPALY;
    const LOG;
}

class X extends Exception
{
    logger
    mailer
    thrower
    display
}

3-8. 最終捕捉

ONED による Web アプリケーション開発においては、最上位の例外キャプチャを使って、想定内の例外と想定外の例外を全て制御すべきだと考えます。

3-9. throw キーワード

throw ≠ instance

例外の throw キーワードについて、たまに誤解されているエンジニアさんをお見かけする事があります。
例えば、

throwable なクラスを継承したサブクラスは、インスタンスと同時に throw しなければならない。

というものです。
もし仮にそのような制約があるとするならば、まずはインスタンス済みオブジェクトを再 throw してる矛盾について気がつかなければならないでしょう。
C 系, Python, Java, Ruby, Perl, PHP, Javascript など、あらゆる言語においてこのような制約はどこにも存在しません。
throw は、単にオブジェクトを throw している構文に過ぎず、そのタイミングや実際に throw するかどうかは任意となっています。
下記のような定型な構文は、単にインスタンスと同時にオブジェクトを throw してるに過ぎません。

C++ Example:

throw new FooException;

4. まとめ

この投稿では、開発速度、品質、機会損失などの改善サイクルの向上を目的とした DevOps のサービス運営視点に基づく Web アプリケーションのドメインロジック例外処理設計の一例として、Operational Notifications Exception Design / ONED をご紹介しました。
このように、ONED は運営視点にも基づいた例外処理設計の概念となります。
実際に、ONED を導入したいくつかのプロジェクトでは、開発速度、品質、機会損失などの改善サイクルが向上しました。

「例外」がないからGo言語はイケてないとかって言ってるヤツが本当にイケてない件

 この記事は、Go3 Advent Calendar 2018 の8日目の記事です。
 7日目は @codehex さんによる「Go でアプリケーションとクライアントのミドルウェアを作成する方法知ってますか?」でした。

 本日はネタ全開でお送りいたします。

Disclaimer(免責事項)

 はじめに言い訳というか、これを書いた経緯というか。

 というツイートをいたしまして、言った手前自分でやるか、と思い立った次第です。
 なので、ネタとしてお楽しみください。

 なお、炎上した場合にも、それすらもネタとして楽しむ所存ですのでアシカラズ。

 それでは、いってみましょう。

Go言語がイケてない…だ…と……?

 Go言語はイケてない言語としてよくdisられているが、その中でも2大disられポイントがこれだ↓

  • Genericsがないm9(^Д^)プギャー
  • 「例外」がないm9( ゚,_ゝ゚)ブブッ

 前者をdisってるヤツらについてはすでに 別の人がdisっているので 、今回は後者、「例外」1 がないことをdisってるヤツらがいかにイケてないかdisることにする。

 なお、これ以降のコード例では、ライブラリのインポートや冗長なクラス宣言などは省略することにする。

事実:達人たちは「例外」を使わない

 おまいら、Googleの「Google C++ Style Guide」を読んだことはあるか?ここに日本語訳があるので、まずは読んでからdisりに来い。
 ここにあるように、GoogleではC++のコーディングで throw の使用を禁じている。それによる恩恵よりもデメリットのほうが上回ると考えているからだ。
 これはなにもGoogleに限った話ではない。LLVM Coding Standard も同様に例外機構の使用を禁じている

 V8やLLVMといった、世界で最も利用されているであろう言語処理系を開発している達人たちは、「例外」を使っていないのだ。彼らの開発した処理系が例外機構を実現しているのというのに、なんとも皮肉な話である。

いやいや、それはC++の話でしょ。C++はtry-catchの存在が考えられていなかった時代のコードとか、メモリ管理の煩雑さとかあるから、問題多いのはわかるよ。
でも、Goはそういうしがらみないじゃん。新しい言語を設計するなら、Javaみたいに「例外」をいれるのがモダンな言語としては常識なんだよ。そんなことも知らないの?

 オーケー、わかった。アンタの言っている「例外」ってのはJavaのtry-throw-catchのことなわけね?で、アンタはJavaの「例外」をどれだけ理解しているわけ?

そもそも「例外」ってなんなん?

 んじゃ一度、アンタらにとってのバイブル的な存在であるはずの『Java Language Specification』に立ち返って、「例外」とはなんだったのか考えてみようじゃまいか。

呼び出し元に異なる型の値を返せる手段としての例外

 「Chapter 11. Exceptions」では次のように書かれている:

Explicit use of throw statements provides an alternative to the old-fashioned style of handling error conditions by returning funny values, such as the integer value -1 where a negative value would not normally be expected.

※強調は筆者による

 奇妙な値(funny values)っていうのは、C言語でよく見られるこういうやつね↓

C
pid_t funny = fork();
if ( funny < 0 ) {
    // pidがマイナス値だとエラー
    fprintf(stderr, "Can't fork");
    exit(1)
} else if ( funny == 0 ) {
    // 子プロセスでの処理
} else {
    // 親プロセスでの処理
}

 これはたしかに、いろいろと問題だ。
 具体的になにがエラー原因だったのかは error 変数を参照しなければいけなかったりするし、なにより、正常時に返す値の型でエラーが表現できない場合に、どうやって呼び出し元にエラーを通知したらいいものか?

 たとえば、次のようにint配列の平均を求める関数があったとしよう:

C
int average(int* a, int n) {
    int s = 0;
    for ( size_t i=0; i < n; i++ ) s += a[i];
    return s / n;
}

 これを、次のように空配列に対して呼び出すと、

C
int main (){
    int a[] = {};
    int r = average(a, 0);
    printf("%d\n", r);
}

 コアダンプしてしまう:

[1]    939 floating point exception (core dumped)

 n == 0 の場合にはエラーを返すようにしたいが、呼び出し元にintしか返せないのに、どうやって呼び出し元に例外を知らせたらいいというのか?2 この関数は、正常な戻り値として 0 はもちろん負数だって返しうる。異常時のために使える"スキマ"はすでに int の中にはないのだ3

 こんなとき、Javaだったらこう書ける:

Java
static int average ( int[] a ) {
    if ( a.length == 0 ) {
        // 配列が空なら ArithmeticException を投げる
        throw new ArithmeticException("division by zero");
    }
    int s = 0;
    for ( int i=0; i < a.length; i++ ) s += a[i];
    return s / a.length;
}

 この関数は配列が空だったとき、

Java
public static void main(string[] args) {
    int a[] = new int{};
    try{
        System.out.println(average(a));
    } catch ( ArithmeticException e ) {
        System.err.println(e)
    }
}

 例外によってエラーを教えてくれる。しかも、その原因は ArithmeticException という人間にもわかりやすい型で教えてくれるのだ。

Every exception is represented by an instance of the class Throwable or one of its subclasses (§11.1). Such an object can be used to carry information from the point at which an exception occurs to the handler that catches it.

 メソッドの処理結果として(正常時の結果の型とは別に) Throwable を継承するオブジェクトを好きなように投げることができるので、このオブジェクトの中にエラーに関する情報などを入れれば、呼び出し元に例外についての詳細な情報も通知することができる。

 つまり、正常な場合と異常な場合とで異なる型の値を呼び出し元に返せるようにすることが、「例外」の役割の1つなわけだ。

大域脱出の手段としての例外

 また、「Chapter 11. Exceptions」では次のようにも書かれている:

During the process of throwing an exception, the Java Virtual Machine abruptly completes, one by one, any expressions, statements, method and constructor invocations, initializers, and field initialization expressions that have begun but not completed execution in the current thread. This process continues until a handler is found that indicates that it handles that particular exception by naming the class of the exception or a superclass of the class of the exception (§11.2). If no such handler is found, then the exception may be handled by one of a hierarchy of uncaught exception handlers (§11.3) - thus every effort is made to avoid letting an exception go unhandled.

※強調は筆者による

 例外が発生したその場で処理されなかったとしても、呼び出し元を遡っていって、処理してくれるハンドラ(=型が合致するcatch節)を探すことで、なんとかして例外が処理されるように努力を尽くすというようなことを言っている。

 つまり、こういうことだ:

Java
static float variance ( int[] a ) {
    int s = 0;
    int av = average(a);
    for ( int v : a ) {
        v -= av;
        s += v*v;
    }
    return s / a.length;
}

public static void main(string[] args) {
    int a[] = new int{};
    try{
        System.out.println(variance(a));
    } catch ( ArithmeticException e ) {
        System.err.println(e)
    }
}

 average を呼び出している variance では例外を処理していないが、それを呼び出している main で見事に例外が補足されて処理されている。
 ここで、average 自身の実行はもちろん、それを呼び出している variance も一気に飛び越えて、main に実行が戻ってきている。こうした入れ子になった関数呼び出しを一気に巻き戻すことを「大域脱出」と呼ぶ。

 古式ゆかしいエラーを示す値を返す方法では、経験上、戻り値が無視されることが多かったと上で書いてあった。そのため、たとえその場では無視されてしまったとしても、呼び出し元の誰かが処理してくれることを期待して、その誰かの元へと一気に脱出することが、「例外」のもうひとつの役割だと言えよう。

 実際のところ、JavaScriptのように変数の型がない動的型付け言語では、return文で任意の型の値が返せてしまう:

JavaScript
function average ( arr ) {
    if ( arr.length == 0 ) return new Error("division by zero");
    return arr.reduce((x, y) => x+y) / arr.length;
}

let r = average([]);
if ( r instanceof Error ) {
    console.log(`Something wrong!: ${r}`);
} else {
    console.log(`OK: ${r}`);
}

 そのため、動的型付け言語においては、事実上この大域脱出のみが例外機構の役割と言ってもいいだろう。
 つまり、return文で普通に返ってきたのではなく、throw文で大域脱出してきた場合には「なんか普通と違うことがあったな」と捉えるという不文律の上に、JavaScriptの「例外」は例外機構として成立している。

function average ( arr ) {
    if ( arr.length == 0 ) throw new Error("division by zero");
    return arr.reduce((x, y) => x+y) / arr.length;
}

try {
    let r = average([]);
    console.log(`OK: ${r}`);
} catch ( e ) {  // if文よりもこう書いたほうが、「なんか普段と違う」感がある
    console.log(`Something wrong!: ${e}`);
}

 もちろん、これ自体は良い約束事だ。4

 なるほど、よく考えられてるわー。

ほれみろ、やっぱり例外は叡智の結晶であってイケてる機能なのだ。そして、それがない言語はイケてないんだ!

 ちょっ!待てって。そんな結論あせんなし。

分けて議論しようじゃないか

 いわゆる「例外」が議論されるとき、上の2つが無意識にごっちゃになって議論されていることが話が混乱する原因じゃねーのかな?でも、この2つが不可分であると誰が決めたわけ?
 別々に提供されていても、例外機構としては問題ないんじゃないか?むしろ、別々になっているほうが良いこともあるんじゃね?

は?別々?そんなの例外機構じゃねーよ

 だから決めつけんなし!まずは考えてみるべ。

複数の値を返せる多値返却

 Go言語の場合、throw に頼らずとも、異なる型の値を返すことができる:

Go
func average ( a []int ) (int, error) {  // 値を2つ返す関数
    if len(a) == 0 {
        return 0, errors.New("division by zero")
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a), nil
}

func main () {
    a := []int{}
    r, err := average(a)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("OK: %d\n", r)
}

 このように、正常時の結果とエラー時の原因を表す型の値(普通は error インターフェースを実装した値)の2つを関数から返すようにして、エラーの値をチェックするように書くのがGo言語の例外処理の流儀である。
 なので、try-throw-catchなんてものがなくても、例外処理はちゃんとできるのだ。

複数の値が返せるとか、キモっw
そんな変な言語なんて誰が使ってんだよ?ww

 は?まじで言っちゃってんの?
 多値を返せる言語なんていくらでもあんべ。

 90年台にはCGIのデファクトスタンダードだったPerlだって↓

Perl5
sub average {
    my $a = shift;
    return (0, "division by zero") if @$a == 0;
    my $s = 0;
    $s += $_ foreach @$a;
    return $s / @$a, undef;
}

my ($r, $e) = average([]);
if ( defined $e ) {
    die $e;
}
print "OK: $r\n";

 機械学習の流行で今をときめくPythonだって5

Python
def average(a):
    if len(a) == 0:
        return 0, Exception("division by zero")
    s = 0
    for i in a:
        s += i
    return s / len(a), None

av, err = average([])
if err is not None:
    print err
    exit(1)
print av

 超美しい型システムで有名なHaskellさんだって↓6

Haskell
average :: [Int] -> (Int, String)
average as = if (length as) == 0
                 then (0, "division by zero")
                 else ((foldl (+) 0 as) `div` (length as), "")

main = do (r, e) <- return (average [])
          if e /= "" then putStrLn e
                     else putStrLn $ "OK: " ++ show r

 みんな多値返却できるわ。
 むしろ、引数が複数とれるのに返り値は複数かえせないことのほうがキモいんじゃ!

 多値返却がないから、「例外」とかいう大仰そうなもの持ち出してきて自分の無力さを偉そうに自慢するなんて、まじイケてねーわーw むしろ痛えわーww

で、でも、それ大域脱出のほうができてねーじゃん!

 お?じゃあ、次いっちゃう?

大域脱出がないと誰が言った?

 たしかにGo言語にはthrowもcatchもないけど、誰も大域脱出ができないなんて言ってない。
 panicdeferrecoverという機能があるんだぜ:

Go
func average ( a []int ) int {
    if len(a) == 0 {
        panic(errors.New("division by zero"))  // ここから↓のdeferまで一気に脱出する
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a)
}

func variance ( a []int ) float64 {
    s := 0.0
    av := average(a)
    for _, i := range a {
        t := (float64)(i - av)
        s += t * t
    }
    return s / (float64)(len(a))
}

func main () {
    defer func(){
        if e := recover(); e != nil {
            log.Fatal("Caught error: ", e)
        }
    }()
    a := []int{}
    r := variance(a)
    fmt.Printf("OK: %f\n", r)
}

 このように、panic を呼ぶとその呼び出し元の defer があるところにまで遡っていって、defer が見つかったらそれが実行される7。そして、defer の中では recover を使って panic が投げた値を捕捉(catch)できるというわけ。
 ね?ちゃんと大域脱出できてんしょ?
 どこの panic でどんな値が投げられてくるのかは型チェックされないから、実質的には上で書いたJavaScriptの throw と同じことが実現できてるっつーわけ。

defer とか recover とか、予約語のセンスが意味わかんねーんだけど・・・

 それ言ったら、Javaやってないヤツからしたら、throw とか catch とか意味わかんねーんですけど?
 日本語しかしゃべれねーヤツが英語わかんねーとかブーたれてんのと一緒だべ?まじイケてねーこと言ってねーで、他言語も勉強しろや。
 それにな、慣れるとこっちの書き方のほうがいいなって思える点もいろいろあんだよ!ブツクサいってねーで、慣れろ!!

そんなにtry-catch風に書きたいなら

 こういうこともできる↓

Go
package main

import (
    . "github.com/mattn/go-try/try"
    "errors"
    "fmt"
)

func average (a []int) int {
    if len(a) == 0 {
        panic(errors.New("division by zero"))  // throwの代わりにpanicで例外を投げる
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a)
}

func main() {
    Try(func() {
        av := average([]int{})  // 空配列を渡しているので、
        fmt.Println(av)         // この行は実行されない
    }).Catch(func(e error) {
        fmt.Println(e)          // averageからここに飛んでくる
    })
}

 Go言語なら、その気になればtry-catch風の書き方をライブラリとして実装できるというわけだ。
 ただし、これは推奨された書き方ではない8

なぜ Go は「大域脱出」を例外処理として採用しなかったのか?

そんなんあるなら、じゃあなんで標準ライブラリで if f, err := os.Open(...); err != nil { ... } とかやってんだよ?全部 panic 使ってJavaっぽくすりゃ良かったじゃん?

 あー、それな。
 それについては、Dave Cheneyさんが歴史をおって解説してくれている
 詳しくは全文を読んでほしいけど、一部を引用しておこう:

Java exceptions ceased to be exceptional at all, they became commonplace. They are used from everything from the benign to the catastrophic, differentiating between the severity of exceptions falls to the caller of the function.

 いろんなことに例外を使った結果として、「Javaの例外はまったく例外的なものではなくなってしまった」と。そして、どの「例外」がどれくらい深刻なものなのか(つまり、正常な状態に復帰させるべき例外はどれで、すぐさまシステムを停止させるべき例外はどれか)を見分けるのは(例外を起こした側ではなくて)関数の呼び出し側に丸投げされることになってしまった。

 極めつけは、

because every exception Java extends java.Exception any piece of code can catch it, even if it makes little sense to do so, leading to patterns like

catch (e Exception) { // ignore } 9

 こういうパターンがまかり通るようになってしまった。

 このパターンがどんなに理に適っていないとしても、また、わかったつもりで使っているのだとしても、利用しているメソッドがどんなExceptionを投げてくるのか完全にはわからない以上、知らないうちに重要な例外を握りつぶしてしまうということが起こる。
 自分でも気づかないうちに例外を握りつぶしてしまうことは、システムを予測不可能な状態にしてしまうし、もちろんデバッグも困難にさせる。

 Java Language Specification の「Chapter 11. Exceptions」には次のようにも書かれている:

Experience shows that too often such funny values are ignored or not checked for by callers, leading to programs that are not robust, exhibit undesirable behavior, or both.

 例外が無視されて誰にも処理されずに終わってしまうことのないよう、なるべく誰かに catch されるようにした結果、ある意味で catch しすぎてしまったとも言える。その結果、robustなプログラムを書くための弊害となってしまったのであれば、なんとも皮肉な話である。

そ、そんな Exception を catch するだけして無視しちゃうような乱暴なコード、誰も書かないし・・・

 そうな、アンタはそうなのかもな。
 でも、もしウソだと思うんなら、GitHubを見てから 語り合おうや。

 一方でGoでは、例外は呼び出し元が責任をもって処理をするべきという考えを持っている。言い換えれば、システムが "例外的な状態" におちいったときには呼び出し元が "正常な状態" へと戻すことが期待されている。そして、"正常な状態" へと戻すことが期待されないような状態におちいったときには panic を使うべき という約束の上に、Goの例外処理は成り立っている。
 本来あってはならないような状態に陥ってしまった場合には、その場を取り繕うようなことはせず、さっさとプログラムを停止して、コードを修正すべきだという考えに立脚しているのだ。
 プログラムを早く修正できるようにするため、なるべく早期にプログラムを停止し、異常な状態におちいった箇所のなるべく近くで状態を調べ上げレポートするべきという信条にもとづいているのである。(これは Fail-fast の考え方である)

 それから、並行処理を書くにはthrow-try-catchは適さないって話 もある。
 もともとGoの設計ゴールのひとつは、 並行処理を安全に効率よく書けるプログラミング言語 となることだった。そのため、並行処理と相性の悪い try-catch 方式は、Goの推奨されたやり方にはなれなかったのだ。まぁ、詳しくは読んどいてくれや。

それ、本当に型安全だと思ってんの?

ちょぉっと待っっったーーー!!

 む、新手か!?

さっきから黙って聞いていれば、メソッドがどんな Exception を投げてくるかわからないだと?どうやらキサマは検査例外のことも知らんようだな?
Java言語仕様には 検査例外(Checked exceptions) という極めて洗練された静的型チェックの仕組みがあるということを!!!

たとえば、一見正しそうなこのJavaプログラム、

Java
import java.io.*;

class Main {
    public static void main(String[] args) {
        new File("tempfile").createNewFile();
    }
}

これをコンパイルしようとすると、ほれ、このとおりエラーとなる:

5: error: unreported exception IOException; must be caught or declared to be thrown
        new File("tempfile").createNewFile();
                                          ^
1 error

エラーメッセージが示しているように、createNewFileIOException を投げるメソッドであり、呼び出し元がそれを catch していないことをコンパイラが検知してくれるのだ。
なぜJavaではこのようなことが可能なのかというと、Javaでは各メソッドが自らが投げる可能性のある例外を型情報として持っているのだ。たとえば、createNewFile のシグネチャはこのとおり

public boolean createNewFile()
                      throws IOException

自ら IOException を投げると宣言している。
こういった例外を投げるメソッドを使っているプログラムのコンパイルを通すには、このように例外をちゃんと catch するか、

Java
public static void main(String[ ] args) {
    try{
        new File("tempfile").createNewFile();
    } catch (IOException e) {
        System.err.println(e);
    }
}

もしくは、自分の呼び出し元が正しく catch して処理できるように、自らもまた IOException を投げることを宣言しなくてはならない:

Java
public static void main(String[ ] args) throws IOException {
    new File("tempfile").createNewFile();
}

従って、どんな例外が投げられるのかわかっているため、"知らずに重要な例外を握りつぶす" などという妄挙は起こり得ぬのだ!
見たか、Java言語の型システムの素晴らしさをぉぉぉ!!

(ちっ、めんどくさいヤツ来たな…)

 もちろん、俺だって検査例外のことを知らないわけじゃない。
 Dave Cheneyさんも書いているように、検査例外はC++の「例外」の問題点をうまく克服しているいいアイデアだって、最初はみんな思ってた。新しいものが発明されるときってのは、いつだってそうさ。
 でも、歴史がいつだってそう証明してくれているように、現実はそんなにうまくはいかなかった。

 例として、入力としてユーザー名とパスワードを受け取って認証結果のtrue/falseを返すメソッドについて考えてみよう。
 パスワードを照合する実装としては、/etc/passwd のようなファイルを使う場合や、LDAPのようなデータベースに問い合わせる場合などいろいろ考えられるから、実装が切り替えられるように抽象化しておきたい。
 これは Authenticator という次のようなインターフェースで表せるだろう:

Java
interface Authenticator {
    boolean authenticate(String name, String password);
}

 そして、これを実装する FileAuthenticator クラスはこう書けそうだ:

Java
class FileAuthenticator implements Authenticator {
    @Override
    public boolean authenticate(String name, String password) throws FileNotFoundException {
        File f = new File("/etc/passwd");
        FileReader r = new FileReader(f);
        ...()
    }
}

 でもこれ、コンパイル通らないんだぜ:

error: authenticate(String,String) in FileAuthenticator cannot implement authenticate(String,String) in Authenticator
        public boolean authenticate(String name, String password) throws FileNotFoundException {
                       ^
  overridden method does not throw FileNotFoundException
1 error

 なんで怒られてるかっつーと、「オーバーライド元のメソッド(インターフェースの宣言)が FileNotFoundException を投げるって宣言してないのに勝手に投げるな」って言われてるわけ。

それはそうだろう。呼び出し元は Authenticator を使うとしか思っていないのだから、Authenticator が投げないと言っている例外を勝手に投げられては、検査例外のコンパイル時チェックが働かなくなってしまうではないか。仕様にそった正しい動作である。

 じゃあ、呼び出し元にファイルが開けなかったってどうやって知らせるわけ?

例外を投げる必要があるのであれば、インターフェースでそれを明示するべきである。つまり、こう書くべきである:

Java
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException;
}

 だよね。そうなるよね。
 じゃあ、これに加えてLDAPを使った実装を作ろうとすると LdapException が投げられる可能性もあるんだけど、どうすんの?

こ、こう書くのである…

Java
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException, LdapException;
}

 あとさ、MySQLに問い合わせる実装も追加するかもしんないんだけど・・・・それ、毎回インターフェース宣言書き換えなくちゃいけないの?
 っつーか、実装を抽象化してるはずのインターフェースが、FileNotFoundException だったり LdapException だったりって実装固有の情報を持っちゃってるって、抽象化としてどうなの?

ぐぬぬ・・・・・・

 というように、検査例外は実装の詳細を不用意にさらけ出しているという批判は昔からある問題だ。

 実装元のインターフェースが書き換え可能な場合はまだマシ10で、実装しなくちゃいけないインターフェースがサードパーティのフレームワークのものだったりすると、書き換えて対応することすらできない。

 こういうことはよくあるわけで、実際にはどうすりゃいいの?っていうと、こういうワークアラウンドがよく知られている:

Java
class FileAuthenticator implements Authenticator {
    @Override
    public boolean authenticate(String name, String password) {
        try {
            File f = new File("/etc/passwd");
            FileReader r = new FileReader(f);
            ...()
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

 例外を RuntimeException でラップして投げ直してしまえばいい。

 この RuntimeException は非検査例外って呼ばれてるものの一種で、コイツとコイツのサブクラスはコンパイル時のチェックを受けない11。なので、authenticate メソッドのシグネチャでも宣言しなくていい。
 もし RuntimeException で例外を隠してしまわないようにしたいというのなら、プログラムを大きく書き換えなくてはいけない。多くの人はそんなことしたくないから、結果として、こういうワークアラウンドが蔓延することになるわけだ。

 というわけで、さっきの「メソッドがどんな例外を投げるのかはわかっている」というのはウソで、メソッドシグネチャを見ただけでは、どんな RuntimeException が飛んでくるのかはわからない。
 しかもタチの悪いことに、みんな大好きヌルポ(NullPointerException) がこの RuntimeException のサブクラスだったりする12

 なのでさっきの例を使って、

Java
// ボクは FileNotFoundException と LdapException を投げることがあるから気をつけてね!
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException, LdapException;
}

public class Main {
    // 実はバグってて、ヌルポを投げることがある実装↓
    private static Authenticator auth = new MyAuthenticator();

    public static void main(String[] args) {
        try{
            boolean ok = auth.authenticate(args[0)], args[1]);
            if ( ok ) {
                System.out.println("OK");
                System.exit(0);
            }
        } catch (Exception e) {
            // FileNotFoundException か LdapException が投げられるかもしれない
            // けど、どちらの場合も失敗として無視すればいいや
        }
        System.out.println("NG");
        System.exit(1);
    }
}

 とかやって、NullPointerException も握りつぶしてしまって、なかなかバグに気づけないということが起こるわけだ。

 検査例外が完全に間違ったアイデアだったとまでは言わない13。でも、検査例外はひとつの問題を解決すると同時に、別の問題を作り出してしまったんだ。つまり、トレードオフがある。しかし、そのトレードオフに表面上は気づきにくいがゆえに、罪が深い。
 Javaよりもあとに設計された言語である C# や、Javaの後継とすら言われることのある Scala が検査例外を採用しなかった14のには、それなりの理由があるってわけだ。

 Goも同じだ。Java後の歴史をよく反省した上で、抜本的な解決策として大域脱出を「例外」として使わない道を選んだんだ15
 Goに try-throw-catch がないことで、まるで Java よりもすごい退化した言語のようにdisるヤツらがいるが、そういうおまいらこそが、Java後の世界から何も学ばず進化できてないんだってこと、そろそろ自覚しねーと、まじでやべぇぞ。

ある関数言語話者の感想

ずーっと横で聞いてたんだけどさ、

 うおっ!?また新手かよ!

キミたち、大域脱出くらいでよくもまぁ、そんなに熱くなれるよね。
大域脱出なんて、言語で直接サポートしなくても、ライブラリとして提供すればよくない?こんなかんじで↓

Scheme
(require 'macro) ; Required to use this with SCM. Other Scheme implementations
                 ; may not require this, or it may even be an error.
(define *cont-stack* '())

(define (push-catch cont)
  (set! *cont-stack* (cons cont *cont-stack*)))

(define (pop-catch)
  (let ((retval (car *cont-stack*)))
    (set! *cont-stack* (cdr *cont-stack*))
    retval))

(define (throw exn)
  (if (null? *cont-stack*)
      (error "Can't use throw outside of a try form")
      ((car *cont-stack*) exn)))

(define-syntax try
  (syntax-rules (catch)
    ((try (body ...)
      catch exception (catch-body ...))
     (call-with-current-continuation
      (lambda (escape)
    (dynamic-wind
        (lambda ()
          (let ((exception
             (call-with-current-continuation
              (lambda (cc)
            (push-catch cc)
            #f))))
        (if exception
            (begin catch-body ... (escape)))))
        (lambda ()
          (begin body ...))
        (lambda () (pop-catch))))))))
> (try ((display "one\n")
        (throw "some-error\n")
        (display "two\n"))
   catch err ((display err)))
one
some-error

(※SCMで実行してください)

あれ?もしかしてキミたちの言語、継続が第一級の計算対象になってないの?
それやばくない?例外がどうとか言ってる前に、そっちのほうがまじでイケてなくない?

 ・・・・・・・。
 おあとがよろしいようで。

References


  1. 文中で「例外」とあえてカッコ書きにしているのは、暗にtry-throw-catchとその類型のことを指しています。本来的には例外とは処理の結果が通常どおりにはいかなかったことを指すので、それをreturnで表そうがthrowで表そうが構わないはずです。しかし、try-throw-catchがないことを指して「例外がない」と揶揄されることが多いので、「例外」とカッコ書きで使うことにしました。 

  2. 「そんなときのためにC言語には longjump という機能があってじゃな…」というC言語賢者のあなたは、下の大域脱出の話まで読み飛ばしてください。 

  3. intにはスキマがなくても、floatだったら NAN (not a number) というスキマがある。割り算は小数が発生しうるのに、ここでの例をfloatではなくあえてintにしているのはこのため。まあ、説明のためであって、実用プログラムではないのでご理解ください。 

  4. 裏を返せば、JavaScriptのtry-throw-catchのようなものを例外機構と認めている人にとっては「例外機構=大域脱出」でしかないということを示しています。勝手な印象ですが、「例外」がないことをdisっている人の9割は大域脱出の部分しか意識していないように思います。 

  5. これは多値というより、タプルという1つの値なのでは?という意見もありましょうが、細かいことは置いといてください。 

  6. これも多値というよりタプルなんですが、細かいことは(ry また、Haskellならこういう場合はMaybe なり Either なりを使うべきってご意見もあるでしょうが、モナドの説明をするにはこの余白はあまりにも狭す(ry 

  7. 正確にいうと、defer が実行されるのは panic が呼ばれたときだけではありません。defer が書かれた関数から抜けるときに必ず実行されます。try-finally を知っている人にとっては、finally節と同じだと思えばいいでしょう。Goでは finally のなかで catch に相当する処理を行うわけですね。 

  8. 念のため、mattnさんのライブラリが良くないというわけではなく、Go言語の流儀ではないということです。 

  9. このコード片は引用元の記事からそのまま転載しています。懸命なJavarista16諸氏はお気づきでしょうが、これは正しいJavaコードではありません。正しくはこう書くべきでしょう: catch (Exception e) { /* ignore */ } 

  10. これをマシと言ってしまっていいのかは、意見がわかれるところでしょうね。 

  11. 補足すると、Error も非検査例外の一種であり、Javaの型階層の中ではこの Error あるいは RuntimeException とそれらのサブクラスのみが非検査例外であり、それ以外のすべての Throwable が検査例外と決められています。 

  12. あるいは NullPointerExceptionArrayIndexOutOfBoundsException といった如何にもコーディングのミスで起きてしまいそうな例外が RuntimeException ではなく Error のサブクラスだったとしたら、Java の例外処理はもう少し平和な世界になっていたのかもしれない・・・・・・と起こり得なかった世界線について思いを馳せることもあります。 

  13. 実際、近年設計された言語であるSwiftはJavaの検査例外に似た機能を持っています。ただし、投げられる値の型は書かないという大きな違いがあります。これは「失敗する可能性のある計算とそれ以外 だけ は区別できるようにしよう」という試みで、実質的にはHaskellのMaybeと同じコンセプトと考えられます。投げられる値の型を1つだけ指定できるようにしようという議論もされているらしいですが、その場合にもやはりHaskellのEitherに相当するものと言えます。Javaの失敗をよく踏まえて設計されていると感じます。 

  14. なお、JavaからScalaのメソッドを呼び出したときの互換性を確保するために、@throws アノテーションを使って例外を投げることをJavaの呼び出し元に知らせることはできる。でも、アノテーションをつけた場合でも、やっぱりScalaの世界ではコンパイル時チェックされません。 

  15. この選択そのものに対する意義はあってもいいと思っています。が、意義をとなえるなら、それに代わるベターな対案を提示するべきでしょう。その意味では、Haskell や Scala の Either を使う例外処理のようなコンポーザブルな手法と比較するのは有意義でしょう。 

  16. http://www.nilab.info/z3/20120708_04.html 

段階的に理解する Java 例外処理

はじめに

例外処理の問題は Java コードレビューでの頻出指摘事項である。この記事で述べる通り、Java の例外処理において守るべき基本的なルールはそれほど複雑ではない。だが、たとえ職務経歴上は経験年数の長い Java プログラマであっても、適切な例外処理を実装できないケースは残念ながらよく観測される。さらに経験年数が短い Java プログラマにおいては言わずもがなである。

なぜ不適切な例外処理が広くはびこっているのか。そこには大きく分けて三つの要因が考えられる。まず、Java 言語仕様において例外機構 (特に検査例外) に歴史的事情による混乱があり、プログラマに過度の自由が与えられていることである。次に、アプリケーションを開発するだけでなく実際に運用してみない限り、不適切な例外処理の弊害に気づけないことである。最後に、適切な例外処理を学ぶためのコンパクトにまとまった資料が世に存在しないことである。入門者向け書籍では e.printStackTrace() を乱発するような場当たり的な例外処理が目立ち、適切な例外処理の方法は中級者以上向けの書籍やフレームワークのドキュメントに散在している。

そこで本記事では、入門者向け・初級者向け・中級者向けの 3 段階に分けて、例外処理の過度の自由の罠をいかに避けるか、また運用の視点を含めて適切な例外処理とは何かを、コンパクトにまとめてみたいと思う。なお、対象アプリケーションは主にサーバサイドを想定している (が、多くの項目はサーバサイドに限らず適用可能なはずだ)。

入門者向け

ここでは、現場に出る前に理解しておくべきことについて述べる。

例外機構の特性を理解する

異常系の表現

メソッド呼び出しの異常系を特殊な戻り値で表現するこのコードには大きな問題がある。まず、呼び出し元が createItem() の呼び出し元が異常系の処理を実装し忘れるかも知れない。また、戻り値が空であることがわかるだけで、実際に何が異常だったのかが呼び出し元ではわからない。

Item item = createItem();
if (item == null) {
    // 異常系
    ...
} else {
    // 正常系
    ...
}

例外機構を導入すると、コードは次のように書き換えられる。これにより、呼び出し元に異常系の処理を明示的に実装するよう誘導できる。また、catch 節で捕捉した例外のメッセージやスタックトレースにより、呼び出し元で異常の詳細を把握できる。

try {
    Item item = createItem();
    // 正常系
    ...
} catch (ItemCreationException e) {
    // 異常系
    ...
}

多段の呼び出し階層への対応

例外機構を使えば、多段の呼び出し階層においてもシンプルに異常系に対応できる。例えば、一般的なレイヤードアーキテクチャを採用したアプリケーションでは、呼び出し階層がこのように多段になる傾向がある (入門者レベルでは、何やら複雑そうな雰囲気がわかれば細かい用語は気にしなくても良い)。

  • (Presentation 層) Web フレームワークの前後処理
    • (Presentation 層) Controller の 入り口メソッド
      • (Presentation 層) Controller の private メソッド
        • (Domain 層) Facade 的な Service のメソッド
          • (Domain 層) 実際のビジネスロジックを担う Service のメソッド
            • (Data Source 層) データアクセスロジックを担う Repository のメソッド

ここで問題になるのは、下位層での異常系をどう上位層に伝えるかである。例えば、最下位層のデータアクセスで異常が発生した場合に、最上位層の後処理で適切なエラーレスポンスを返す必要があるとする。特殊な戻り値でこの要件を満たすには、最下位層から最上位層まで異常な戻り値をバケツリレー式に返していかなければならない。

これに対して、特殊な戻り値の代わりに例外機構を使うことで、メソッドの入出力をシンプルに保ったまま任意の下位層の異常を任意の上位層で処理できる。下位層は何も考えずに throw new IllegalStateException("Something is wrong") のような例外を送出し、どこかの上位層がその例外を catch 節で捕捉するだけで良い。

検査例外と非検査例外の違いを理解する

例外クラスの階層

以下に基本的な例外クラスの継承ツリーを示す (なおパッケージは全て java.lang である)。Exception を継承した例外は検査例外であり、RuntimeException を継承した例外は非検査例外である。なお ThrowableError については、入門者レベルでの詳しい理解は不要である (むしろ触れないほうが良い)。

  • Throwable
    • Exception
      • (各種検査例外)
      • RuntimeException
        • (各種非検査例外)
    • Error

検査例外

検査例外は、呼び出し元に何らかの対処を強制する例外である。例えば、以下のようにファイルの内容を読み込むコードを書くと、コンパイルエラーが出力される。

List<String> lines = Files.readAllLines(Paths.get("items.csv"));
Error:(x, x) java: 例外java.io.IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります

ここで、とりあえず入門者目線でコンパイルを通すために取れる手段は 2 つある。まず、try-catch で捕捉する方法である。なお、ここで捕捉した後の処理には罠が多い点に注意すること (適切な対処については後述する)。

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    // 正常系
    ...
} catch (IOException e) {
    // 異常系
    ...
}

もしくは、メソッドの throws 節に明示して呼び出し元に委ねる方法である。ただし、この方法が使える場面は現実的には多くない。詳しくは中級者向けの項目で述べる。

public void processItems() throws IOException {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    // 正常系
    ...
}

非検査例外

非検査例外とは、検査例外のような呼び出し元での対処が強制されない例外である。例えば、Integer.parseInt(String) メソッドは非検査例外の NumberFormatException を送出する可能性がある (参考: Javadoc) が、コンパイルエラーは出力されない。代わりに実行時にエラーが発生する。

Integer value = Integer.parseInt("ABC"); // 特に何もしなくてもコンパイルは通る
java.lang.NumberFormatException: For input string: "ABC"

検査例外をやり過ごす

RuntimeException にラップして送出する

さて、コンパイルを通すために検査例外を catch したらどうすべきか、入門者レベルではこの RuntimeException にラップして送出する方法だけでも覚えておいてほしい。送出した非検査例外がどうなるかについては、入門者レベルでは詳しく理解する必要はない。とにかく余計なことはしないことだ。

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    // 正常系
    ...
} catch (IOException e) {
    throw new RuntimeException(e); // 非検査例外にラップして送出する
}

より適切な非検査例外にラップして送出する

RuntimeException 一辺倒ではなく、適切な非検査例外にラップして送出できるとなお良い。以下によく使う非検査例外とその非常にざっくりした使いどころを挙げる。

よって実は、上述の RuntimeException にラップしているコードは、UncheckedIOException を使うよう書き直したほうがより適切である。

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    // 正常系
    ...
} catch (IOException e) {
    throw new UncheckedIOException(e); // より適切な非検査例外にラップして送出する
}

初級者向け

ここでは、現場で誰かの指示のもとに開発・運用を行う上で理解しておくべきことについて述べる。

例外処理の禁じ手を知る

上述の通り、とりあえず非検査例外にラップして送出することだけを覚えておけば、多くの場合で最低限に無難なコードは書ける。だが、なぜか Java プログラマ各位は余計なことをしたり肝心なことを忘れたりして、最低限のラインから逸脱しがちである。以下によくある不適切な例外処理を挙げる。くれぐれも真似をしないでほしい。

(禁じ手) 握り潰す

禁じ手の筆頭は握り潰しである。握り潰した結果、本来実行される処理の正しい結果を期待していた後続の処理で問題が発生する。典型的な問題は NullPointerException である。一般的に Java において NullPointerException が悪者扱いされる場面が散見されるが、ここでは何のことはない、NullPointerException ではなくあなた自身のコードが悪いのである。

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    // 握り潰す
}
lines.stream().forEach(...); // ここで lines が null のままになっているため NullPointerException が発生する

ただし、意志を持って握り潰すべきケースは少ないながら存在する。その場合はログなりコメントなりでその意志をわざとらしいくらいに明示しておくと良い。

(禁じ手) スタックトレースやログを申し訳程度に出力する

入門書でよく見るのが catch 節で e.printStackTrace() を呼び出して良しとするコードであるが、これは現実世界の Java アプリケーションにおいてはほとんどの場合許容されない。e.printStackTrace() でメッセージを出力しようが、処理を継続すればそれは例外を握り潰しているのとあまり変わらない。また、e.printStackTrace() の出力先は一般的には標準エラー出力であり、ログと違って日時やリクエスト ID のような原因究明に役立つメタ情報が欠落している。このようなコードを放置しておくと、運用時に解決の手がかりがつかめない謎のスタックトレースに悩まされることになる。

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    e.printStackTrace(); // 申し訳程度にスタックトレースを出力する
}
lines.stream().forEach(...); // 結局ここで NullPointerException が発生する

上述の e.printStackTrace()log.warn("Could not read items: " + e, e) のようにログ出力に置き換えたところで、出力のメタ情報の問題は解決したとしても、処理を継続すれば例外を握り潰しているのとあまり変わらない点は同じである。

(禁じ手) 自前でロギングしてから再送出する

几帳面な初級者がやってしまいがちなのがこの禁じ手である。自前のログが個別に出力された後、後述する大域の例外ハンドラで再度同じ問題に起因するログが出力される。これらは運用時には別々の問題に関する二種類の例外ログのように見え、混乱を引き起こす。大人の世界では、几帳面なことは無条件に良いことではないのだ。

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    log.warn("Could not read items: " + e, e); // 几帳面さが裏目に出る
    throw new UncheckedIOException(e);
}
lines.stream().forEach(...);

メッセージを込めたければ、例外メッセージとして送出すれば良い。

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    throw new UncheckedIOException("Could not read items", e); // 几帳面さをさりげなくアピールする
}
lines.stream().forEach(...);

(禁じ手) 別の例外を送出する

「非検査例外にラップして送出する」の「ラップ」を忘れて、単に別の非検査例外を送出してしまう誤りも散見される。この場合の悪影響は、運用時に例外の根本原因がわからなくなることだ。例えば、ファイルアクセス時に発生する例外の原因としては、ファイルが存在しないことや、ファイルへのアクセス権がないことなどさまざまな事象が考えられる。こうした根本原因を示す例外をラップし忘れて別の例外を送出した場合、運用時の障害解析は困難になる。

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    throw new IllegalStateException("Could not read items"); // 元の例外にあったはずの情報が失われる
}
lines.stream().forEach(...);

(禁じ手) リソースのクローズ処理を忘れる

ファイルやデータベース接続などのリソースのオープン処理を実行した場合、その後で原則としてクローズ処理を行わなければならない (厳密にクローズ処理が必要か否かについては、オープン処理メソッドの Javadoc を参照して判断すること)。クローズ処理を忘れた場合のよくある悪影響としては、俗に言うメモリリークが挙げられる。ただし、多くの場合問題はリリース直後には発覚せず、その後の運用に伴って時限爆弾のように顕在化する。一旦発覚すると、犯人探しや定期再起動のような暫定運用で現場は疲弊することになる。

try {
    BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"));
    in.lines().forEach(...);
    // in がクローズされていない
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

代わりに try-with-resources 構文を使おう。

try (BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"))) {
    in.lines().forEach(...);
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

try-with-resources 構文導入以前は finally 節でクローズ処理が行われていたが、冗長で読みにくく、かつ finally 節でさらに例外が発生した場合の対応など考えるべきことが多く煩雑である。初級者の段階では避けたほうが無難だ。

BufferedReader in = null;
try {
    in = Files.newBufferedReader(Paths.get("items.csv"));
    in.lines().forEach(...);
} catch (IOException e) {
    throw new UncheckedIOException(e);
} finally {
    try {
        in.close();
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

参考までに、try-with-resources は、対象クラスが java.io.Closeable インタフェースもしくは java.lang.AutoCloseable インタフェースを実装している場合に限って利用できる。詳しくは公式ドキュメントを参照すること。

(禁じ手) 複数の例外に対して同じ処理を繰り返す

この禁じ手に運用上の実害はほぼないが、やはりコードの可読性が低いのは良くない。

try {
    Item item = createItem();
    // 正常系
    ...
} catch (ItemCreationException e) {
    throw new IllegalStateException(e);
} catch (InvalidProcessingException e) {
    throw new IllegalStateException(e);
}

代わりに multi-catch を使おう。

try {
    Item item = createItem();
    // 正常系
    ...
} catch (ItemCreationException | InvalidProcessingException e) {
    throw new IllegalStateException(e);
}

非検査例外を防御的に活用する

非検査例外は、「ここにはこんなパラメータしか来ないはず」「この処理は実行されないはず」といった条件を防御的に表現する用途にも活用できる。

よくある適用例として、メソッド冒頭のガード節での不正なパラメータに対する IllegalArgumentException の送出が挙げられる。

public void processItem(Item item) {
    if (!item.isValid()) {
        // パラメータが不正
        throw new IllegalArgumentException("Invalid item: " + item);
    }
    // 正常系
    ...
}

上記コードは、GuavaPreconditions を使うと以下のように一行で書ける。大きな声では言えないが、「念のため」レベルの分岐で単体テストカバレッジが下がるのを嫌うのであればこちらの方法でも良いだろう。

public void processItem(Item item) {
    Preconditions.checkArgument(item.isValid(), "Invalid item: %s", item);
    // 正常系
    ...
}

同じように、パラメータの null チェックを行う場合も、自前の if 文ではなく、標準クラスライブラリの java.util.Objects.requireNonNull(T) を使うと良い。ちなみに、ここで送出される例外はあの悪名高い NullPointerException である。若干抵抗を感じさせるところではあるが、Guava の類似機能の Preconditions.checkNotNull(T) も同様に NullPointerException を送出している。長い物には巻かれておこう。

public void processItem(Item item) {
    Objects.requireNonNull(item, "Item is null");
    // 正常系
    ...
}

さらに、switch 文の通常は到達し得ない default 部分で例外を送出することもある。

Result result = processItem(item);
switch (result) {
    case SUCCEEDED:
        ...
        break;
    case FAILED:
        ...
        break;
    ...
    default:
        throw new IllegalStateException("This would not happen");
}

例外に大域で対応する

ここまでは主に例外を送出する側に焦点を置いて記述してきたが、次に送出された例外をどう処理すべきかについて述べる。

(ほぼ禁じ手) 呼び出し階層最上位で自前の try-catch 処理を実装する

最もシンプルでわかりやすい例外捕捉は、呼び出し階層最上位 (コマンドラインアプリケーションの場合の main メソッドや、Web アプリケーションの場合の Presentation 層 Controller) で最後の砦的な自前の try-catch 処理を書くことである。だが実際にはこの方式を使う機会は少ない。現実世界の Java アプリケーション開発では、main メソッドからフルスクラッチで実装するようなケースはかなりの少数派であり、また Presentation 層 Controller に関しては Web フレームワークにもっと良い代替手段がある。

Web フレームワークの例外処理機構を利用する

モダンな Web フレームワークであれば、発生した例外クラスに応じた例外ハンドラを実行する機構を提供している。こうした機構の利点は、複数 Controller の共通処理を Controller クラスの継承構造を使わずに柔軟に組み合わせられる点である。以下に代表的な Web フレームワークでの例外処理機構を示す。

DI コンテナの Interceptor 機構を利用する

より汎用的な仕組みとして、DI コンテナの Interceptor 機構がある。Interceptor 機構を使えば、DI コンテナの管理下にあるクラスの任意のメソッド呼び出しに対して前後処理を差し込める。以下に代表的な DI コンテナでの Interceptor 機構を示す。

例外に関して Interceptor 機構を利用して実現される典型的な処理はトランザクション制御 (特定のメソッド配下の例外発生時に自動的にロールバックする) である。以下に代表的な DI コンテナでのトランザクション制御機構を示す。

独自の例外を定義する

大域で例外をクラスに応じて処理できるようになると、独自の例外を作りたくなる。例えば特定のビジネスロジックや外部 API アクセスロジックに特化した個別のエラーレスポンスを返したい場合などである。独自の例外を作るには、既存の例外クラスを継承してコンストラクタをオーバーライドすれば良い。

public class ItemCreationException extends RuntimeException {
    public ItemCreationException() {
    }

    public ItemCreationException(String message) {
        super(message);
    }

    public ItemCreationException(Throwable cause) {
        super(cause);
    }

    public ItemCreationException(String message, Throwable cause) {
        super(message, cause);
    }

    public ItemCreationException(String message, Throwable cause, boolean enableSuppression,
            boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

一見するとコード量は多く見えるものの、実際は IDE によって生成されるため、作業は軽微である。例えば Eclipse の場合は、新規クラス生成ダイアログで、スーパークラス名を指定しコンストラクタの生成にチェックを入れるだけである。

中級者向け

ここでは、現場で自身の裁量のもとに開発・運用を行う上で理解しておくべきことについて述べる。

例外ログを適切なレベルで出力して運用する

大域の例外ハンドラで扱う例外については、適切なレベルでログを出力し、日々の運用を回さなければならない。以下にレベルの使い分けと運用に関する方針の一例を示す。これはあくまで一例であり、チームによって方針は異なる。

  • FATAL
    • システム全体に影響するような深刻な例外ログを出力する
    • 発生したら緊急対応が始まる
  • ERROR
    • 発生したら原則としてインシデントとして扱わなければならない例外ログを出力する
    • 発生したらその場で監視警告が飛ぶ
    • インシデント分析後、問題が軽微な場合はログレベルを WARN や INFO に落とす
  • WARN
    • 発生しても対応が必要とは限らない例外ログを出力する
    • 発生頻度が高まったら監視警告が飛ぶ
    • 例外型別の集計を日次や週次でレポートする
    • 定期的にレポートを確認し、問題が無視できる場合はレベルを INFO に落とす
  • INFO
    • 運用上発生しても問題ない例外ログを出力する
  • DEBUG
    • 通常は例外ログ出力には使用しない
  • TRACE
    • 通常は例外ログ出力には使用しない

とはいえ、リリース直後から厳密にルールを適用することは一般的には難しく、実際はフェーズを切って段階的に上記のような運用を整備していくことになる。

例外に個別対応する

現実世界の Java アプリケーションにおいては、大域の例外処理ではなくある機能に特化した例外処理を求められることがある。代表的には以下のようなケースが挙げられる。

別の戻り値を返す

下位層が例外を送出することが設計上適切とは言い難い場合に、例外を捕捉して別の戻り値を返すことがある。例えば以下のコードでは、データベースアクセス時にレコードが存在しないことを示す例外を捕捉して、代わりに Optional な戻り値を返している。

public Optional<Item> findByName(String name) {
    try {
        Item item = entityManager.createQuery("...", Item.class).setParameter(1, name).getSingleResult();
        return Optional.of(item);
    } catch (NoResultException e) {
        return Optional.empty();
    }
}

なお、ここで Optional ではなく null を返す設計を選択してはならない。詳しい理由は 「null 殺す」の Twitter 検索結果を見て察してほしい。

リトライする

外部 API 呼び出しのような処理で、偶発的な通信エラー等を示す例外を捕捉してリトライ処理を実行することがある。ただし、リトライ処理を真面目に実装しようとすると、タイムアウト時間設定、リトライ回数設定、バックオフアルゴリズム、リトライ同時多発時の対処、リトライ発生状況のモニタリング、…など考えるべきことが意外に多い。自前で実装するよりは、Microprofile RetrySpring Retry のようなライブラリを利用したほうが無難だろう。また将来的にはこうしたロジックはアプリケーションの外の Istio のような Service Mesh 機構に巻き取られて行くと予想される。

その他の代替処理を実行する

その他、要件に従って代替処理を実行することがある。処理内容はケースバイケースであるため詳細は省略する。

検査例外を適切に活用する

最後に、ここまで避けていた検査例外の使いどころについて述べる。

まず、低レベルのライブラリやフレームワークについては、例えば java.io パッケージがそうであるように、検査例外を適切に活用すべき場面がある。

また、ビジネスロジックについても、呼び出し元に異常系の処理を強制させたい場合は、検査例外の利用が選択肢になる。以下に検査例外を利用する方式と enum を返す方式の例を示す。検査例外を利用する方式は、メソッドシグネチャがシンプルであり、また例外の継承構造を活用すれば類似処理を共通化できる。一方、enum で返す方式は、呼び出し元が戻り値を網羅的に処理していることがわかりやすい。どちらを選ぶかはケースバイケースである。

public void sendNotification(Member member, Notification notification) throws InvalidMemberException {
    // 検査例外を利用する方式
    ...
}
try {
    sendNotification(member, notification);
    // 正常系
    ...
} catch (InvalidMemberException e) {
    // 代替処理
    ...
}
public NotificationResult sendNotification(Member member, Notification notification) {
    // enum で返す方式
    ...
}
NotificationResult result = sendNotification(member, notification);
switch (result) {
    case SUCCEEDED:
        // 正常系
        ...
        break;
    case INVALID_MEMBER:
        // 代替処理
        ...
        break;
    ...
}

おわりに

以上、現実世界の Java アプリケーションの例外処理に関して理解しておくべきことを、入門者向け・初級者向け・中級者向けの 3 段階に分けて示した。この記事によって非生産的なコードレビューやつらいアプリケーション運用が少しでも減ることを願う。

C#で例外処理実装時に意識するべき3つのポイント

C#での例外処理について学んだのでその中でも特に意識しておくべきことを挙げていきます。

1.リソースの使用時にはusingを使う

プログラム中でファイルやデータベースなど、他のアプリケーションと共有するようなリソースを使用する際にはそのリソースを開放する必要があります。
例えば、ファイルに書かれている内容を読み込むStreamReaderを使用した場合はClose()メソッドを呼び出す必要があります。
しかし、下記のような書き方をした場合ある問題が発生します。

Sample.cs
            string filePath = @"c:\sample.txt";

            StreamReader reader = null;
            try {
                reader = new StreamReader(filePath);

                // 読み込んだファイルに対して何かしらの処理を行う

                reader.Close();

            } catch(FileNotFoundException e) {
                Console.WriteLine(e.StackTrace);
            }

上記プログラムの問題点は読み込んだファイルに対して何かしらの処理を行うの部分で例外が発生した場合にreader.Close();が呼ばれないところです。
これを防ぐための1つの手段としてfinallyを使用することがあげられます。

Sample.cs
            string filePath = @"c:\sample.txt";

            StreamReader reader = null;
            try {
                reader = new StreamReader(filePath);

                // 読み込んだファイルに対して何かしらの処理を行う

            } catch(FileNotFoundException e) {
                Console.WriteLine(e.StackTrace);

            } finally {
                reader.Close();
            }

finallyブロックでClose()メソッドを呼び出すことで例外が発生しても確実に呼び出すことができます。
しかし、例外が発生した際にファイルをクローズする前に何かしら処理を行いたいということがなければこの書き方は冗長です。
C#ではusingメソッドを使用して下記のように省略した書き方が可能です。

Sample.cs
            string filePath = @"c:\sample.txt";

            StreamReader reader = null;
            try {
                using (reader = new StreamReader(filePath)) {

                    // 読み込んだファイルに対して何かしらの処理を行う
                }
            } catch(FileNotFoundException e) {
                Console.WriteLine(e.StackTrace);
            }

usingの内部で生成されたオブジェクトはusingブロックを抜けるタイミングで自動的に破棄されます。これによって処理の途中で例外が発生しても確実にリソースを開放することができます。

ちなみに、Closeメソッドとusingで自動的にリソースを開放する場合でもどちらもDispose()メソッドを呼び出します。なのでDispose()メソッドを実装していないクラスのリソースをusingで取得しようとするとコンパイルエラーが発生します。
StreamReaderクラスはIDiposesableインターフェイスを実装しているのでusing内で使用することができるわけです。

2.例外フィルターを使ってマルチキャッチを行う

例えば指定されたパスに存在するcsvファイルを読み込み、カンマ区切りで2つ目に記述された文字を出力する処理を書く場合、少なくとも下記の3つの例外を考慮する必要があります。

  • 指定されたパスがnullか空文字("")
  • ディレクトリが存在しない
  • ディレクトリは存在するがファイルが存在しない
  • ファイルを読み込むことはできるが、カンマ区切りの2つ目が存在しない

これらをの例外を全て考慮したプログラムは下記のようになります。
(実際にここまで厳密にcatchするかは別問題です。)

sample.cs
        public void sample(string filePath) {

            StreamReader reader = null;

            try {
                using (reader = new StreamReader(filePath)) {
                    string str = reader.ReadLine().Split(',')[1];
                    Console.WriteLine(str);
                }
            } catch (ArgumentException ex) {
                // 引数がnullか空文字("")の場合に発生
                Console.WriteLine("ファイルを開けませんでした。");
                Console.WriteLine(ex.StackTrace);
            } catch (DirectoryNotFoundException ex) {
                // ディレクトリが見つからなかった場合に発生
                Console.WriteLine("ファイルを開けませんでした。");
                Console.WriteLine(ex.StackTrace);
            } catch (FileNotFoundException ex) {
                // ファイルが見つからなかった場合に発生
                Console.WriteLine("ファイルを開けませんでした。");
                Console.WriteLine(ex.StackTrace);
            } catch (IndexOutOfRangeException ex) {
                // CSVのフォーマットが不正な場合に発生
                Console.WriteLine("指定されたcsvファイルのフォーマットが不正です。");
                Console.WriteLine(ex.StackTrace);
            }
        }

ここで、ArgumentExceptionとFileNotFoundExceptionとDirectoryNotFoundExceptionは同じ処理をしています。
つまり本来は同じ処理を何度も書きたくないのに例外をキャッチするために仕方なく同じ処理を複数回書いている状態です。
これを回避する1つの方法としてExceptionをcatchで指定する方法があります。

sample.cs
        public void sample(string filePath) {

            StreamReader reader = null;

            try {
                using (reader = new StreamReader(filePath)) {
                    string str = reader.ReadLine().Split(',')[1];
                    Console.WriteLine(str);
                }
            } catch(IndexOutOfRangeException ex) {
                Console.WriteLine("指定されたcsvファイルのフォーマットが不正です。");
                Console.WriteLine(ex.StackTrace);
            } catch(Exception ex) {
                Console.WriteLine("ファイルを開けませんでした。");
                Console.WriteLine(ex.StackTrace);
            }
        }

Exceptionをキャッチすることで同じ処理を2度書く必要はなくなりました。
しかし、こう書いてしまうと新たな問題が発生します。
それは例外処理の対処があいまいになってしまったり不適切な例外処理をしてしまうからです。
原則として例外をキャッチする際にはExceptionクラスではなくてExceptionクラスから派生した詳細な例外クラスを指定すべきです。

では結局のところ詳細に例外クラスを指定するために最初の例のように同じ処理でも複数回記述する必要があるのでしょうか?
これに対応するためにC#6以降では例外フィルターというものが使用できるようになり、catch文に例外の種類に加えて条件を指定できるようになっています。
これを使用して下記のようにマルチキャッチを行うことで上記の問題は解決できます。

sample.cs
        public void sample(string filePath) {

            StreamReader reader = null;

            try {
                using (reader = new StreamReader(filePath)) {
                    string str = reader.ReadLine().Split(',')[1];
                    Console.WriteLine(str);
                }
            } catch (IndexOutOfRangeException ex) {
                Console.WriteLine("指定されたcsvファイルのフォーマットが不正です。");
                Console.WriteLine(ex.StackTrace);
            } catch (Exception ex) when (ex is ArgumentException || ex is DirectoryNotFoundException || ex is FileNotFoundException) {
                Console.WriteLine("ファイルを開けませんでした。");
                Console.WriteLine(ex.StackTrace);
            }
        }

catch(Exception ex)の後ろにwhenで条件を指定することでその条件に一致した時のみ例外処理を行うことができます。
このように1つのcatch文で複数の例外を処理することをマルチキャッチと言います。
これによって例外処理の対象を明確にした上で同じ処理を複数回記述する必要がなくなります。

3.例外処理のオーバーヘッドに注意する

基本的には意識する程ではないのですが、例外処理はオーバーヘッドが大きく、レスポンスに影響を及ぼす可能性がある処理です。
レスポンスに問題がある場合、まず見直すのはデータベースやネットワーク関係ですが、それでも解決できない場合はプログラムの処理を見直す必要が出てきます。
その際には例外処理が頻発していないかをチェックすべき1つのポイントになります。
ここでは、例外処理を頻発させないための実装パターンを2つ紹介します。

1.Tester-doerパターン

これは実行できるかどうかテストをしてから実行するパターンです。
例えば、Dictionaryクラスでは存在しないキーを指定するとKeyNotFoundExceptionが発生します。

sample.cs
        public void sample() {

            var myDictionary = new Dictionary<string, string>() {
                ["リンゴ"] = "Apple",
                ["バナナ"] = "Banana",
                ["ミカン"] = "Orange"
            };
            try {
                Console.WriteLine(myDictionary["パイナップル"]);
            } catch(KeyNotFoundException ex) {
                Console.WriteLine("指定されたキーに対応する値は存在しません。");
                Console.WriteLine(ex.StackTrace);
            }
        }

上記の例だとConsole.WriteLine(myDictionary["パイナップル"]);で例外が発生してcatchブロックへ飛びます。
これが頻発すると処理の遅延が発生する可能性があるので事前に「パイナップル」というキーが存在するかチェックしてから処理を行うのがTester-doerパターンです。
今回例で使用しているDictionaryクラスの場合は下記のように書き換えることができます。

sample.cs
        public void sample() {

            var myDictionary = new Dictionary<string, string>() {
                ["リンゴ"] = "Apple",
                ["バナナ"] = "Banana",
                ["ミカン"] = "Orange"
            };
            string value;
            if (myDictionary.TryGetValue("パイナップル", out value)) {
                Console.WriteLine(value);
            } else {
                Console.WriteLine("指定されたキーに対応する値は存在しません。");
            }
        }

TryGetValue()メソッドは第一引数で指定したキーが存在するかをtrue/falseで判定するメソッドです。キーが存在する場合は第2引数で指定した変数に対応する値を代入してくれます。
ContainsKey()メソッドを使用してチェックもできますが、TryGetValueを使うほうが効率がいいそうです。こちらのサイト様で詳しく検証されていました。

このように処理を実行しても例外が発生しないか事前に確認し、例外が発生する場合はelseで処理
することでtry-catch構文にする必要がなくなり、処理効率が上がります。

Try-parseパターン

このパターンはParseメソッドを呼び出して変換を行う際に使われるパターンです。
例えば、下記の例は文字列クラス⇒日付クラスに変換する処理です。
時間に0~24以外の数値を指定しているので例外が発生します。

        public void sample() {

            var date = "2019/1/5 25:06:30";
            try {
                Console.WriteLine(DateTime.Parse(date));
            } catch (FormatException ex) {
                Console.WriteLine("フォーマットが不正です");
                Console.WriteLine(ex.StackTrace);
            }
        }

これを実行前に事前に判定しようとしても判定しようとした時点で例外が発生するので事前のチェックができません。
よって、処理の失敗は避けられないのですが、処理に失敗しても例外を返さないメソッドを使用することで例外による遅延を防ぐことができます。
今回の例だとTryParse()メソッドを使用すれば変換できない際に例外ではなくfalseを返すのでif-elseで処理することができます。

sample.cs
        public void sample() {

            var date = "2019/1/5 25:06:30";
                DateTime dt;
            if (DateTime.TryParse(date, out dt)) {
                Console.WriteLine(dt);
            }else{
                Console.WriteLine("フォーマットが不正です");
            }
        }

このように、安全のためにとにかくtry-catchで囲むのではなく、そもそも例外が発生しないように設計することでレスポンスを向上させることができるかもしれません。

まとめ

  • ファイルなどの共有リソースを開放する際の注意点
    • 開放前にやりたい処理があるならfainallyでClose()を呼び出して開放する
    • そういうものがなければusingを使用して自動で開放する
  • catchによる例外処理の注意点
    • Exceptionをcatchすると例外処理の対象があいまいになるので原則使用しない
    • 複数の例外で同じ処理を行いたい場合は例外フィルターを使用してマルチキャッチを行う
  • そもそも例外はなるべく発生させないようにする
    • 事前にチェック可能な場合はTester-doerパターンを使用する
    • 事前のチェックが不可能な場合はTry-parseパターンを使用する

rubyでraiseのバックトレースを空にする(ベンチマーク付き)

概要

処理をバスッと切ってユーザーにエラーを表示させるようなシチュエーション、例えばAjaxのレスポンスなどで、ロールバックのことなんかも考えるとraiseを使うと便利だと思ったのですが、バックトレースが必要ないので、生成しないようにできたら多少でも軽くなるかなと思って調べてみた。

コード

raise StandardError, 'そんなことしたらあかん!', []

raiseの3番目の引数に空の配列を渡すと生成されないっぽい。

https://docs.ruby-lang.org/ja/2.6.0/method/Kernel/m/raise.html

Catch/Throw

Catch/Throwのことを思い出し試してみた。

ただ、catchの返り値が正常終了時はブロックの返り値、throwするとthrowした値というのが使い勝手が悪かったのと、railsのrescue_fromで処理できないのでアクションごとにごにょごにょ書かいないとダメなのがいまいちだった。

message = catch :error do
  throw :error,'そんなことしたらあかん!'

  # ここで何か処理をするとその返り値がmessageに入り`if message.nil?`がfalseに
end

ベンチマーク

本当にバックトレースが生成されてないのか(かどうかはわからないが早くなってるかどうか)チェックしてみた。

                      user     system      total        real
Backtrace         0.098349   0.001307   0.099656 (  0.099900)
EmptyBacktrace    0.060658   0.000319   0.060977 (  0.061093)
Catch/Throw       0.032181   0.000067   0.032248 (  0.032283)

Backtrace         0.107247   0.001669   0.108916 (  0.109663)
EmptyBacktrace    0.062382   0.000346   0.062728 (  0.063138)
Catch/Throw       0.035800   0.000467   0.036267 (  0.036668)

                      user     system      total        real
Backtrace         0.102345   0.001523   0.103868 (  0.104327)
EmptyBacktrace    0.059909   0.000370   0.060279 (  0.060521)
Catch/Throw       0.032488   0.000130   0.032618 (  0.032768)

3回取ってみた感じはこんな感じ。やっぱりCatch/Throwの方が早いですね。ただ、空バックトレースも一定の効果はありそうです。

参考までにベンチマークのコードはこんな感じ。

require 'benchmark'

count = 100000
Benchmark.bm 15 do |r|
  r.report "Backtrace" do
    count.times do
      begin
        raise StandardError, 'FOOOOOOOOO'
      rescue

      end
    end
  end
  r.report "EmptyBacktrace" do
    count.times do
      begin
        raise StandardError, 'FOOOOOOOOO', []
      rescue

      end
    end
  end
  r.report "Catch/Throw" do
    count.times do
      catch :foo do
        throw :foo, 'FOOOOOOOOO'
      end
    end
  end
end

ベンチマークって平等にやるの意外に難しいですよね。問題に気づいたら教えてください。

「例外を投げない」という選択肢をとる言語

新しめの言語では例外を投げることを推奨しない言語が出てきているように思えるが、そうした言語が例外をどう考え、例外の代わりにどのようなアプローチを奨励しているかを調べてみた。

本稿での「例外」とは、Javaのthrow構文のようにスコープを脱出してcatchされるまでエスカレートされる「投げる例外」のことを指し、エラーを表現したオブジェクト(エラーオブジェクト)については「例外オブジェクト」と呼び区別するものとする。(この2つを同一に扱うと、例外を使わないということは、エラーオブジェクトは使わないの?という話になるため)

Go言語 - 例外はコードを複雑にする

Go言語では、通常、エラーは戻り値として扱われる。(本当の本当に例外的なエラーのためにpanic, recoverがあるが、ほとんど使われることがないように見受けられる。)

例外がないGoでは、どう呼び出し元にエラーを伝えているかというと、多値返却と呼ばれる複数の値を返す構文を使う。これはエラーハンドリングだけのためのものではない。値を複数返すうちのひとつに、エラーも返すことができるというだけだ。つまり、エラーもまた普通の戻り値と同じ地位で扱われるわけだ。

func Open(name string) (file *File, err error)
                     // ↑多値返却の宣言

file, err := os.Open("filename.ext") // ファイルとエラーの2つが値が同時に返る
if err != nil { // 他の言語のcatchのようなエラーハンドリング専用制御構文は使わない
    log.Fatal(err)
}

公式ドキュメントになぜ例外を推奨しない言語設計にしているのか、その理由が書いてある:

Why does Go not have exceptions?
[なぜGoには例外がないのですか?]

We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.
[例外をtry-catch-finallyのような制御構造に結びつけることは結果的に複雑なコードにつながると考えております。そして、それはファイルを開くのに失敗したなどの数多くの普通のエラーに例外としてラベル付することをプログラマーに推奨するものです。]

Frequently Asked Questions (FAQ) - The Go Programming Language

また、Go言語ではエラーハンドリングは発生した箇所で明示的に行うことを推奨している:

In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them). In some cases this makes Go code verbose, but fortunately there are some techniques you can use to minimize repetitive error handling.
[Goではエラーハンドリングは重要です。言語の設計と規約では、エラーが発生した箇所で明示的にチェックすることを推奨しています(例外を投げ、キャッチすることがあるような他の言語の規約とは異なります)。そのためGoのコードが冗長になることがありますが、幸いながらエラーハンドリングの繰り返しを最小化するテクニックがあります。]

Error handling and Go - The Go Blog

Scala - 例外は副作用をもたらす

Scalaにはthrow、try-catchがあるが、Scalaのベストプラクティスではそれらの使用を推奨しない。

Scalaはオブジェクト指向言語でもあるが、同時に関数型言語の色も強い言語だ。関数型言語が「副作用がない」ことを重視するように、Scalaも副作用の無さを大事にする。副作用がないとは、関数(メソッド)がその戻り値以外に影響を与えない性質のことだ。関数型言語ではthrowされる例外は戻り値とは考えない。もし、関数が例外をthrowすると、戻り値以外に影響を与えてしまう。したがって、例外がthrowされる関数は副作用があり、良いものではないと考えられている。

Scalaでは、例外を投げる代わりに、例外を戻り値にする方法がよくとられる。例外を戻り値にする点では、Go言語と似ているが、Scalaは多値返却ではなく、「成功したときの値、もしくは、エラーを返す」といった形で戻り値を表現する。

ScalaではEitherTryを使う。Eitherは「どちらか」という意味の通り、2種類のうちどちらかの型になることを表現できるクラスだ。Tryもクラスで、SuccessまたはFailureどちらかの値になることを表現できる。

下記のコードのEitherは、Exception(エラー)もしくは、Fileオブジェクトを返す関数を表現している。RightLeftEitherのサブクラスで、慣習的にRightには成功時の結果、Leftにはエラーオブジェクトを格納する。

def openFile(filename: String): Either[Exception, File] = {
  ...ファイルにアクセス...
  if (...ファイルが開けたか...) Right(new File(...))
  else Left(new Exception("..."))
}

この関数の呼び出し元は、match構文(他の言語のswitch構文に近い)を使いエラーの場合の処理と、成功した場合の処理を分けて行うことができる。

openFile(filename) match {
  case Left(exception) => println(exception.toString) // エラー処理
  case Right(file) => // 成功した場合の処理
}

TryEitherと似ているが、例外をthrowする関数をTry { ... }で囲むことで、例外をcatchし、戻り値に変換する働きがある。ScalaではJavaの資産を再利用することがあるが、Javaコードは例外を投げることがあるので、それを副作用のないScalaのスタイルに適合するためにこのTryがよく使われる。

def toInt(s: String): Try[Int] = Try {
    Integer.parseInt(s) // この関数は例外をthrowすることがある
}
toInt("100") match {
  case Failure(exception) => println(exception.toString) // パースエラーなどの処理
  case Success(value) => value * 2 // 成功時の処理
}

まとめ

  • 例外を投げないことを推奨する言語としてGoとScalaの例を見た。
  • それらの言語では、例外を投げることをどう考えているかに触れた。Goは例外は複雑なコードにつながると考え、Scalaは副作用のない関数のために例外を避ける傾向があった。
  • それらの言語では、どういうエラーハンドリングをしているかを調べた。Goでは多値返却、ScalaではEitherなどを使うのを見た。

yet another 「例外を投げない」言語

「例外を投げない」という選択肢をとる言語

という記事がトレンドに乗ってたので・・

Rustなのか?Rustなのかい?

って思って開いてみた所・・・

ok, err := foo()

ってやった場合と

val, ok := bar["key"]

みたいにやった時に何がokなんだかよくわかんなくなる方の言語でした。
いや、使い方が悪いんでしょうけど。
最初、if val, ok := bar["key"]; ok{ ...みたいなコード見て混乱した覚えがあります。keyがあれば値が入るし、keyがないとfalseが返ってくる。ちょっと気持ち悪い。

ってごめんなさい。Goも好きです。disってないです。

Rustの場合

で、自分的に「例外を投げない」でピンと来た方がこちら。
Rust初心者なのですが生意気にすいません。
Rustも例外のない言語です。
仕様的にはGoに近くて多値返却ではあるんですが、その多値を受け取る型が用意されています。
失敗する可能性のある関数だとすんなりとString型とかを返却する事はなくて、そのStringをなんらかの形でwrapして返す。
なので受け取ったStringを使いたかったらそのなんらかの形から取り出して使うしかない。

Rustのエラーハンドリング
https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/error-handling.html

Result型

こういう感じの列挙型です

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

例えばファイルを開くstd::fs::File::open は開こうと思ったファイルの存在やら権限やらによっては失敗する関数です。
シンプルに開くだけの例だとこんな感じです。開こうとするとResultのenumが返却されるので、OkのパターンとErrのパターンに対応できる。

use std::fs;

fn main() {
    //returns `io::Result<File>`
    match fs::File::open("foo.txt") {
        Ok(_f) => println!("opened foo.txt "),
        Err(_e) => panic!("couldn't open foo.txt "),
    }
}

Option型

Someってなんやねん。判りづらい。。
正常な値が存在すればstd::option::Option::Some を介して取り出せるって考えてればいいんだと思います。

pub enum Option<T> {
    None,
    Some(T),
}

例えば、こんな感じでゼロ除算するとNoneの値が返って、それ向けに書いた処理が実行されます。
正常に割り切れたものは当然正常な処理が実行される。

// An integer division that doesn't `panic!`
fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {
    if divisor == 0 {
        // Failure is represented as the `None` variant
        None
    } else {
        // Result is wrapped in a `Some` variant
        Some(dividend / divisor)
    }
}

// This function handles a division that may not succeed
fn try_division(dividend: i32, divisor: i32) {
    // `Option` values can be pattern matched, just like other enums
    match checked_division(dividend, divisor) {
        None => println!("{} / {} failed!", dividend, divisor),
        Some(quotient) => {
            println!("{} / {} = {}", dividend, divisor, quotient)
        },
    }
}

fn main() {
    try_division(4, 2);
    try_division(1, 0);
}

//https://doc.rust-lang.org/rust-by-example/std/option.html

実行するとこんな感じに。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rusttest`
4 / 2 = 2
1 / 0 failed!

こんな感じでいちいち値がwrapされる。
ちなみにこういう感じでwrapされててもunwrapとか使うと簡単に取り出す事はできる。
アンチパターンになりがちだけど、あっさり書いてもいい場所には使える。
expect でそのままpanicにしちゃったりもできる。
https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap

let x = Some("air");
println!("{}", x.unwrap());

std::collections::HashMap.get()

HashMapでgetしたときもOptionが返ってきます。
unwrap()しちゃうとこんな感じ。

use std::collections::HashMap;

fn main(){
    let mut map = HashMap::new();
    map.insert("foo", "bar");

    println!("value is {}",map.get(&"foo").unwrap());
}

Rustややこしい

Rustの 3大ややこしい のうちの一つがこのエラー処理なのではなかろうか。
他の言語でぜんぜん馴染みないし。
でもプログラマがエラーを強制的に意識して書かなくてはいけないってのは安全なコードにつながるし、Rustの思想なんだと思う。

補足

例外はないけど、Dropトレイトっていうデストラクタはあるみたいです。try~catch的な事もできるかもしれないです。原則として後始末用なんだと思いますが。

🔵 TypeScript - Goみたいに「例外を投げない」ためのライブラリを作った

先日この記事を読みました。
「例外を投げない」という選択肢をとる言語

Go や Scala では、例外を JavaScript のように throw して catch することが推奨されず、多値返却や Either でエラーも戻り値として返すアプローチがとられるという内容です。

TypeScript でも同じようなことがしたかったので fp-ts を使って作ってみました!!!🎉🎉

TrySafe
https://github.com/yarnaimo/trysafe

yarn add trysafenpm i -S trysafe でインストールできます🙃

どっかで見たことあるような名前ですが気のせいです。

使い方

import { Try, TryAsync } from 'trysafe'

const result = Try(() => {
    return document.querySelector('a')!.href // 要素が見つからない場合はエラーが throw される
})

// この場合 result の型は Either<Error, string>

if (result.isLeft()) {
    // ここでは result.value の型は Error
    console.error(result.value.message) // -> エラーメッセージ
    return
}

// ここでは result.value の型は string
console.log(result.value) // -> href の値

Try には引数のない関数 () => T を渡します。

Try の戻り値の型は fp-ts の Either<Error, T> で、これは Left 型と Right 型の Union Type です。

渡された関数の中でエラーが throw された場合は Left、成功した場合は Right が返され、.isLeft().isRight() で判別することができます。

async 関数を渡したい場合は TryAsync を使います。

const result = await TryAsync(async () => {
    return await fetch('http://example.com')
})

普通の try-catch と比べていいところ

「エラーが起きる可能性」を明示できる (try-catch を忘れることがない)

例えばこんな関数があったとします。

async function getUsers() {
    return fetch('/users')
}

この関数はエラーを throw する場合があるので上流で catch すべきですが、特にネストが深い場合などは忘れてしまう可能性もあります。

async function getUsers() {
    return TryAsync(async () => fetch('/users'))
}

Try で囲むと、エラーが発生した場合も外には throw されずに戻り値で返されるのでその心配はありません。

エラーが握りつぶされにくい

エラーを上流で処理するような設計にしやすいので、下流でエラーを握りつぶしてしまう可能性も減るのではないかと思います。

あとがき

今回の内容とは関係ないですが、fp-ts といえば実行時に型チェックをするための io-ts っていうライブラリもあります。

終わりだよ〜

Rubyで例外を捕捉する

例外を捕捉して処理を続行する場合

もっとも単純な構文

ruby.rb
begin
 # 例外が起きうる処理
rescue
 # 例外が発生した場合の処理
end

例外オブジェクトから情報を取得する

Rubyでは発生した例外もオブジェクトであるため、例外オブジェクトのメソッドを呼び出すことで、発生した例外に関する情報を取得することができる。代表的なものとして例外発生時のエラーメッセージを返すmessageメソッドとメソッド呼び出し履歴を配列で返すbacktraceメソッドがある。
こんな感じの構文

ruby.rb
begin
 # 例外が起きうる処理
rescue => 例外オブジェクトを格納する変数(e,exがよく使われる)、捕捉したい例外クラス
 # 例外が発生した場合の処理
end

具体例

ruby.rb
begin
 1 / 0
rescue => e
 puts "エラークラス: #{e.class}"
 puts "エラーメッセージ: #{e.message}"
 puts "バックトレース ----"
 puts e.backtrace
 puts "----"
end

#こんなエラーが帰ってくる
エラークラス: ZeroDivisionError
エラーメッセージ: divided by 0
バックトレース ----
(irb):2:in '/'
(irb):2:in 'irb_binding'
省略/irb:11:in '<main>' 

例外クラスの継承関係

Rubyではすべての例外クラスはExceptionクラスを継承している。その下にたくさんのサブクラスがぶら下がっている。ここで重要になるのがExceptionクラスの下にあるStandardErrorとそれ以外の例外クラスの違いを理解すること。
StandardErrorは通常のプログラムで発生する可能性の高い例外を表すクラスです。これのサブクラスとしてNoMethodErrorやZeroDivisionError、StandardErrorクラスが含まれている。
StandardErrorクラスを継承していないものは、通常のプログラムでは発生しない特殊なエラーが起きていることを表す。
rescue節に何もクラスを指定しなかった場合に捕捉されるのはStandardErrorとそのサブクラスです。StandardErrorを継承していない例外クラスは捕捉されない。

ruby.rb
begin
 # 例外が起きそうな処理
rescue
 # StandardErrorとそのサブクラスのみ捕捉される
end

[Java] コンパイルエラーと実行時エラーの分類メモ

コンパイルエラーになるケースと実行時エラーになるケースの分類。

コンパイルエラーになるケース

ラムダ式の型の不一致

BiFunction<Integer, Double, Integer> function = (x, y) -> x + y;
function.apply(1, 2.5);

(int)(x + y) もしくは、 BiFunction<Integer, Double, Double> に修正する必要がある。

実行時になるケース

FileInputStreamでreset()を呼び出し

FileInputStreamはreset()を呼び出せるがサポートはしていない。子クラスのBufferedInputStreamでサポートされる。

new FileInputStream("src/a/a.txt").reset();
//=> java.io.IOException: mark/reset not supported

親クラスのInputStreamのreset()

public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

子クラスのBufferedInputStreamのreset()

public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
        if (markpos < 0)
            throw new IOException("Resetting to invalid mark");
        pos = markpos;
    }

シェルでtry~catchをやろうとしたら、それっぽい書き方になった

模擬的にエラーを起こすスクリプトを用意する

error.sh
#!/bin/bash
exit 1

例外処理を呼びたいスクリプト

like_try_catch.sh
set -e
function if_trap
{
    # エラーになった時の処理
    echo "TRAP ERROR" !
}
trap if_trap ERR
{
    # メインの処理
    /bin/bash error.sh
    echo "正常終了"
}
unset if_trap

trap if_trap ERRの後の{ }には特に意味は無い。
try~catchのある言語のtry{}の中括弧にように見えるだけ。
(変数のスコープとかに影響しちゃう?)

契約による設計、例外、表明の関係について個人的なまとめ

はじめに

契約による設計を知ると、よりよいプログラミングができそうだったので個人的にまとめました。

契約による設計とは

契約による設計(けいやくによるせっけい、Design By Contract)とは、プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法。

契約プログラミングより

具体的には、「もしそちらが事前条件及びクラス不変条件1を満たした状態で私を呼ぶと約束して下さるならば、お返しに、事後条件及びクラス不変条件を満たす状態を最終的に実現することをお約束します。」2という契約を結ぶことを言います。

事前条件、事後条件、クラス不変条件の意味

  • 事前条件は、呼び出し側が守らなければならない条件。
  • 事後条件は、呼ばれる側が守らなければならない条件。
  • クラス不変条件は、呼び出し前、呼び出し後で維持されなければならない性質。

契約による設計のメリット

事前条件、事後条件、クラス不変条件で期待されている条件が明示されるため、

  • 仕様がコード内で表現される
  • 落ちる場所(例外3が挙げられるタイミング)が早いためデバッグがしやすくなる

といったメリットがあると思います。

契約による設計のコード例

コード4で表すと下記のようになります(あくまで説明用のコードです)。

class Rectangle {
  int _height;
  int _width;

  Rectangle({int height, int width}) {
    if (height <= 0) throw new ArgumentError("height must be positive integer."); // 事前条件
    if (width <= 0) throw new ArgumentError("width must be positive integer."); // 事前条件
    this._height = height;
    this._width = width;
    assert(_height == height); // 事後条件
    assert(_width == width); // 事後条件
    _invariant();
  }

  set height(int height) {
    _invariant();  // クラス不変条件
    if (height <= 0) throw new ArgumentError("height must be positive integer."); // 事前条件
    _height = height;
    assert(_height == height); // 事後条件
    _invariant(); // クラス不変条件
  }

  int get height {
    _invariant(); // クラス不変条件
    return _height;
  }

  set width(int width) {
    if (width <= 0) throw new ArgumentError("width must be positive integer."); // 事前条件
    _width = width;
    assert(_width == width); // 事後条件
  }

  int get width {
    _invariant(); // クラス不変条件
    return _width;
  }

  area() {
    _invariant(); // クラス不変条件
    assert(_width > 0 && _height > 0); // 事前条件
    var area = _width * _height;
    assert(area > 0);
    _invariant();
    return area; // 事後条件
  }

  _invariant() {
    assert(_width > 0 && _height > 0);
  }
}

本来クラス不変条件は、インスタンス作成時とメソッド呼び出しの前後ですが、メソッド呼び出しの後の一部(getter部分)は書き足しても意味がないので省略しています。
                                                  

表明とは

表明とは、

プログラミングにおける概念のひとつであり、そのプログラムの前提条件を示すのに使われる。アサーションとも呼ばれる。表明は、プログラムのその箇所で必ず真であるべき式の形式をとる。

表明より

下記のようにassertが記述されている部分が表明です。

assert(_width == width);

例外とは

ここでの例外は、一般的な例外5 (exception)のことを表します。

下記コードのthrow new ArgumentError("height must be positive integer.")の部分が例外です。

if (height <= 0) throw new ArgumentError("height must be positive integer.");

表明と例外の使い分

「表明6は、起こり得ないことにたいして記述するもの。」
「例外は、起こり得ることに対して記述するもの。」
という形で使い分けてます。     

終わりに

  • ライブラリや言語のサポートがない場合には、クラス不変条件や事後条件は必要に応じて書く
  • assertがサポートされている場合には、事前条件、事後条件だけでも書く
  • テストコードを書くときに事前条件、事後条件、クラス不変条件を意識して網羅性や仕様の読み取りやすさを上げる

といったことを考えるとよりよいプログラミングにつながるのではないかと思いました。

参考


  1. 必ずしも表明ではないと考えているので、クラス不変表明と表現せず、クラス不変条件と記述しています。 

  2. 「オブジェクト指向入門 第2版 原則・コンセプト」では、直接的にクラス不変条件の記述はされていません。クラス不変条件は、暗黙的に事前条件、事後条件に追加されると記述されています。 

  3. この部分での例外には表明違反も含めています。 

  4. このコードは本来の契約による設計とは意味が異なると思いながら書いています。というのは、本家(オブジェクト指向 第2版 原則・コンセプト)の契約による設計では条件に表明が使われており、呼び出す側が値をチェックした状態で呼ぶことが前提になっているように読み取れるからです。(つまりラッパー相当あるいは呼び出し側モジュールがバリデーション(引数の検査)を行うような作りになっており、内部の処理するモジュールが引数として取るものは必ず正しいという意味で事前条件としての表明が存在する作り。)表明は、起こり得ない事に対して使うものという立場に立って考えると、どこからでも呼び出せる(publicな)メソッドは事前条件に相当するものとして検査して例外を上げるほうがよいのではないかと思い変更しております(Effective Javaではpublicなメソッドの引数は通常のバリデーション(引数の検査を行い条件に適合しない場合には例外を上げる作り)、privateなメソッドの引数の検査には表明という作りになっている箇所がありました)。 

  5. 「オブジェクト指向入門 第2版 原則・コンセプト」では、表明違反が例外に含まれている記述が見られるので、その部分の例外とは異なるという意味で、ここでは一般的な例外という言葉を使用しています。 

  6. 「達人プログラマー 職人から名匠への道」の中で「もし起こり得ないというのであれば、表明を用いてそれを保証すること」という記述があります。また、別の箇所で「本来のエラー処理に表明を使ってはいけません。表明は起こり得ないことをチェックするためのものです」と記述されています。このことから、起こり得ないことは表明。起こり得ることは例外と使い分けています。  

Railsのトランザクションと例外処理

Railsのトランザクションと例外処理がよく分からなかったので、調べました。

app/controllers/user_controller.rb
def create
  ActiveRecode::Base.transaction do
    # createではなく、create!にすると保存できなかったときに例外が発生します。
    @user = User.create!(user_params)

  recue => e 
  # バリデーションエラーだけ拾いたい場合は次の行
  # (ActiveRecord:RecodeInvalid => e) 
    # error処理
  end

end

recueはphpでいうとexceptionみたいな感じでした。

参考サイト

rails save! create! update!のバリデーション例外を捕捉する - Qiita

【Rails】例外処理の書き方(begin, rescue, raise,retry, ensure) - Qiita

rubyの例外についてまとめてみた - Qiita

ActiveRecord::Base.transactionで囲うタイミング - Qiita

【PHP8.0】PHP8で警告のエラーレベルが軒並み厳しくなる

多くの警告について、PHP8.0でエラーレベルが変更されます。

これはReclassifying engine warningsというRFCで受理されたものです。
提案者はいつものNikita。
影響の大きい未定義変数アクセスについては個別に紹介しましたが、ここではそこで紹介しなかった細かい警告について見ていきます。

これまでE_NOTICEだった警告の一部がE_WARNINGに、これまでE_WARNINGだった警告の一部が例外になります。
E_WARNINGを抑制するような書き方をしている場合、PHP8では動かなくなる可能性が高いので気をつけましょう。
現在E_NOTICE以下であればいきなり動かなくなることはありませんが、そもそも抑制する書き方がよくないので、なるべく修正した方がよいでしょう。

エラーレベルの変更がない警告も並んでいるので、もしかしたら全警告が列挙されてるのか?と思ったのですが、expected to be a %s, %s givenとか色々無いものもあるので、全てを出しているわけではないようです。
どういう基準なんだろうか?

Reclassifying engine warnings

Attempt to increment/decrement property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティをインクリメント/デクリメントすると発生する。

    $a = 1;
    $a->b++;

Attempt to modify property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティを変更すると発生する。

    $a = 1;
    $a->b['c'] = 1;

Attempt to assign property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数にプロパティを追加すると発生する。

    $a = 1;
    $a->b = 1;

このへん全部同じでいいんじゃないか?

Creating default object from empty value

E_WARNING → Error exception

未定義の変数にプロパティを追加すると発生する。

    $a->b = 1;

PHP7.3では$aが定義されるのだが、PHP8では何も定義されなくなると思われる。

ところで未定義の変数のプロパティを変更しようとすると何の警告もなくオブジェクトが生成されるのだが、こっちは今後もいいのだろうか。

    $a->b['c'] = 1; // エラー出ない
    var_dump($a); // object(stdClass)#1 (1) { ["b"]=> array(1) { ["c"]=> int(1) } }

Trying to get property '%s' of non-object

E_NOTICE → E_WARNING

オブジェクトではない変数のプロパティを参照すると発生する。

    $a = 1;
    $a->b;

Undefined property: %s::$%s

E_NOTICE → E_WARNING

オブジェクトの未定義プロパティを参照すると発生する。

$a = new stdClass();
$a->b;

PHPの場合、入力として外部引数やらAPIやらを使うことが多いため、読み取りの失敗については書き込みより寛容気味。

Cannot add element to the array as the next element is already occupied

E_WARNING → Error exception

配列の自動挿入による整数キーがPHP_INT_MAXを超えたときに発生する。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[] = 2;

ちなみに計算値で指定すれば、PHP_INT_MAXを超えていてもいける。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[PHP_INT_MAX+1] = 2; // -2147483648とかになる

Cannot unset offset in a non-array variable

E_WARNING → Error exception

エラーの出し方がわからない

Cannot use a scalar value as an array

E_WARNING → Error exception

文字列型ではないスカラー型の変数に配列値を追加すると発生する。

    $a = true;
    $a[] = 1;

文字列型の場合は文字単位アクセスという正しい文法。

ちなみにnullで初期化した場合は問題なく動く。

    $a = null;
    $a[] = 1; // [ 0 => 1]

なぜかfalseでも動く。

    $a = false;
    $a[] = 1; // [ 0 => 1]

Trying to access array offset on value of type %s

E_NOTICE → E_WARNING

文字列型ではないスカラー型の変数を配列形式で読み込もうとすると発生する。

    $a = true;
    $a[1];

このE_NOTICE自体PHP7.4で追加されたもので、それ以前は何も出さずにnullを返していた。

Only arrays and Traversables can be unpacked

E_WARNING → TypeError exception

関数呼び出し時の引数展開にiterableでない値を渡すと発生する。

    var_dump(...1);

unpackとは特に関係ない。

Invalid argument supplied for foreach()

E_WARNING → TypeError exception

iterableでない値をforeachすると発生する。

    $a = 1;
    foreach($a as $loop){}

Illegal offset type

E_WARNING → TypeError exception

配列のキーに配列やオブジェクトを指定すると発生する。

$a = [
    new stdClass() => 1,
    [] => 2,
];

Illegal offset type in isset or empty

E_WARNING → TypeError exception

issetおよびemptyでチェックする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    isset($a[new stdClass()]);

ちなみに$aが未定義やスカラー型の場合は何のエラーも起こらない。

    isset($a[new stdClass()]); // エラー出ない
    $a = 1;
    isset($a[new stdClass()]); // エラー出ない

未定義やint型等であれば配列形式アクセスした時点でfalseだから中身を見る必要もないというのはわかるが、文字列型でもエラーが出ない理由はよくわからない。

    $a = 'a';
    isset($a[1]); // true
    isset($a[new stdClass()]); // false エラー出ない

Illegal offset type in unset

E_WARNING → TypeError exception

unsetする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    unset($a[new stdClass()]);

$aが未定義の場合Illegal offset typeは発生しないが、かわりにUndefined variableのE_NOTICEが出る。
文字列以外のスカラー型には何のエラーも出さず、文字列型やオブジェクトにはFatal errorが発生する。

    unset($a[new stdClass()]); // E_NOTICE: Undefined variable
    $a = 1;
    unset($a[new stdClass()]); // エラー出ない
    $a = 'a';
    unset($a[new stdClass()]); // Fatal error: Cannot unset string offsets
    $a = new stdClass();
    unset($a[new stdClass()]); // Fatal error: Cannot use object of type stdClass as array

このあたりの法則はさっぱりわからない。

Indirect modification of overloaded element of %s has no effect

E_NOTICEのまま

SplFixedArrayに突っ込んだ配列の値を直接変更すると発生する。

    $a = new SplFixedArray(1);
    $a[0] = [1];
    $a[0][0] = 2;

値を変更しているつもりだが、実際には変更されていないという注意。

SplFixedArrayに限らず、ArrayAccessをimplementsしたクラスに一般的に発生する症状のようだ。

Indirect modification of overloaded property %s::$%s has no effect

E_NOTICEのまま

マジックメソッド__getが配列を返す場合、その返り値を直接変更すると発生する。

    class A{
        private $value = ['a' => 1, 'b' => 2];
        public function __get($k){
            return $this->value;
        }
    }

    $a = new A;
    $a->value['a'] = 3;

こちらも値を変更したつもりだが、実際には変更されていない。

なお配列ではなくオブジェクトであれば、エラーも出ないし値を直接変更できてしまう。

    class A{
        private $obj;
        public function __construct(){
            $this->obj = new stdClass();
        }
        public function __get($k){
            return $this->obj;
        }
    }

    $a = new A;
    $a->obj->b = 1;

    var_dump($a); // { 'obj' => stdClass{ 'b'=>1 } }

Object of class %s could not be converted to int/float/number

E_NOTICEのまま

オブジェクトをスカラー型にキャストすると発生する。

    (int)new stdClass();

緩い比較が内部的にこのキャストを使用しているため、オブジェクトとスカラー型を緩く比較するとE_NOTICEが発生する。

    $a = new stdClass();
    var_dump($a == 1); // E_NOTICE
    var_dump($a === 1); // エラー出ない

比較ではエラーが出るべきではないので、こちらの問題がどうにかなるまでエラーレベルを変更しない。

A non-numeric value encountered

E_WARNINGのまま

次項で一緒に解説する。

A non well formed numeric value encountered

E_NOTICEのまま

非数値文字列を数値演算すると発生する。

    1 + '1';  // エラー出ない
    1 + '1a'; // E_NOTICE: A non well formed numeric value encountered
    1 + 'a';  // E_WARNING: A non-numeric value encountered

完全に数値形式の文字列ではエラーは出ず、一部だけ数値として評価できるときはnon well formed numeric value、完全に数値でない場合はnon-numeric valueになる。
今回はエラーレベルが変わらないが、数値形式文字列の計算は安全のためキャストしておいた方がよいだろう。

    1 + (int)'a'; // エラー出ない

Accessing static property %s::$%s as non static

E_NOTICEのまま

staticプロパティにインスタンスからアクセスすると発生する。

    class A{
        public static $property = 1;
    }

    $a = new A();
    $a->property;

正しくは$a::$property、もしくはA::$property
インスタンス内部からであればself::$propertyもいける。

Array to string conversion

E_NOTICE → E_WARNING

配列を文字列型にキャストすると発生する。

    (string)[];

変換前の配列の中身がどうなっていたとしても変換後の文字列はArrayになるので、実質的に機能していない状態なのでExceptionでもいい気がする。

Resource ID#%d used as offset, casting to integer (%d)

E_NOTICE → E_WARNING

リソースIDを配列のキーとして使用すると発生する。

    $fp = fopen('hoge', 'w+');
    $array = [$fp => $fp];
    var_dump($array); // []

リソースIDは整数っぽい値であり、かつプログラム中ではユニークなので、このような使い方ができそうではあるが実際は動いていない。
明示的にキャストするとint型になるため警告は発生せず、正しく動作する。

    $fp = fopen('./hoge', 'w+');
    $array = [(int) $fp => $fp];
    var_dump($array); // [1=>resource]

そもそも動いてないので、これもいきなりExceptionでいい気がしないでもない。

String offset cast occurred

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスのキーに整数ではない数値を使ったときに発生する。

    'string'[1.5];
    'string'[true];

下の項目と同じような内容なのでエラーレベルを揃えたという話のようだ。

Illegal string offset '%s'

E_WARNINGのまま

文字列への角括弧オフセットアクセスのキーに数値ではない値を使ったときに発生する。

    'string'['a'];

Uninitialized string offset: %d

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスで範囲外の値を読み込もうとしたときに発生する。

    'string'[10];

Illegal string offset: %d

E_WARNINGのまま

文字列への角括弧オフセットアクセスでマイナスの範囲外の値を変更したときに発生する。

    $str = 'string';
    $str[-10] = 'a';

正の範囲外を変更したときは単に文字列が伸びるだけでエラーは発生しない。

    $str = 'string';
    $str[10] = 'a'; // 'string    a'

Cannot assign an empty string to a string offset

E_WARNING → Error exception

文字列への角括弧オフセットアクセスで値を空文字に変更しようとしたときに発生する。

    $str = 'string';
    $str[1] = '';

2文字以上与えた場合は2文字目以降が無視されるだけでエラーは発生しない。

    $str = 'string';
    $str[1] = 'abcde'; // 'saring'

Only variables should be passed by reference

E_NOTICEのまま

リファレンス関数に値を直接渡すと発生する。

    sort([2, 1]);

Only variable references should be returned by reference

E_NOTICEのまま

リファレンス返しで値を直接返すと発生する。

    function &ref(){
        return 1;
    }
    ref();

Only variable references should be yielded by reference

E_NOTICEのまま

リファレンス返しで値を直接yieldすると発生する。

    function &ref(){
        yield 1;
    }
    foreach(ref() as $v);

リファレンス返しは百害しかないので使用してはならない。

Only variables should be assigned by reference

E_NOTICEのまま

リファレンスではない関数をリファレンスで受け取ろうとすると発生する。

    function ref(){
        return 1;
    }
    $x = &ref();

Attempting to set reference to non referenceable value

E_NOTICEのまま

出し方がわからないどころか、事例すら一切出てこない謎の警告。

Cannot pass by-reference argument %d of %s%s%s() by unpacking a Traversable, passing by-value instead

E_WARNINGのまま

参照渡し関数にTraversableな値を引数アンパックして渡すと発生する。

    function ref(&$var){}
    ref(...new ArrayIterator([1]));

いみがわからない。

Division by zero

E_WARNING → DivisionByZeroError exception

数値を0で割ると発生する。

    1 / 0;

PHP7.4までは計算結果がfloat(INF)になる。

Undefined variable

E_NOTICE → E_WARNING

未定義変数を参照すると発生する。

    echo $a;

詳細は個別記事を参照のこと。

Undefined array index

E_NOTICEのまま

配列の未定義キーを参照すると発生する。

    $a = [];
    echo $a[1];

詳細は個別記事を参照のこと。

感想

そもそもどうすれば出せるのかすらわからないエラーがあった。

警告に寛容なプログラミングをしている場合、Invalid argument supplied for foreachCreating default object from empty valueあたりはよく見かけるのではないかと思います。
これらはPHP8では例外になって完全に動かなくなるので注意しましょう。

それ以外でも、ゆるふわぺちぱーに対する締め付けは年々厳しくなる一方で、彼らの肩身はどんどん狭まりつつあります。
かつてはPHP以上にアバウトで破壊と慈悲の混沌だったJavaScript界も、最近は型に嵌まっていないゆるふわJavaScripterを完全排除する流れができあがっています。
やがて彼らの居場所が完全に失われてしまったとき、難民たちはいったいどこに行くのでしょうね。

SpringBootの例外ハンドリング

SpringBootの例外ハンドリング

概要

SpringBootにおいては、SpringMVCの機能を用いた例外ハンドリングやSpringBootのAutoConfigurationと組み込みサーバーを利用した例外ハンドリングなど複数の方法で例外を取り扱うことができる。
公式リファレンスで述べられているが、ハンドリングが可能な範囲や仕組みが若干わかりにくいので主要な方法をまとめる。

例外ハンドリングの方法

一覧

機能名レイヤ
HandlerExceptionResolverSpringMVC
ErrorPageSpringBoot
ErrorControllerSpringBoot

HandlerExcepitonResolver

HandlerExceptionResolverはSpringMVCの機能を利用した例外ハンドリングの方法である。
SpringMVCの機能を利用しているため、FilterやViewで例外が生じた場合はハンドリングできない。

ハンドリングの概略図を下記に示す。
image.png

HandlerExceptionResolverは下記に示す複数の実装クラスがデフォルトで用意されている。
なお、下記の表において有効/無効はSpringBootのデフォルト設定で有効か無効かを示し、上に来るExceptionResolverが優先される。

クラス名有効/無効機能
ExceptionHandlerExceptionResolver有効@ExceptionHandlerが付与されたメソッドにより例外ハンドリングを行う
ResponseStatusExceptionResolver有効@ResponseStatusが付与されている例外クラスが発生した際にハンドリングを行う
DefaultHandlerExceptionResolver有効SpringMVCで定義されている例外クラスのハンドリングを行う
SimpleMappingExceptionResolver無効例外クラスとViewを直接マッピングする

下記に一番オーソドックスなExceptionHandlerExceptionResolverによる例外ハンドリングの方法を示す。

ExceptionHandlerExceptionResolverによるハンドリング

例外発生時に、発生した例外クラスのメタ情報が付与された@ExceptionHandlerアノテーションを持つメソッドを走査し、処理が移譲される。

@ExceptionHandlerを付与したメソッドは下記に示すように@Controllerもしくは@ControllerAdviceを付与したクラス内に定義できる。

ThrowExceptionController.java
@Controller@RequestMapping("web")publicclassThrowExceptionController{@GetMapping("original")publicvoidthrowOriginalException(){thrownewOriginalWebException("thrown in ThrowExceptionController");}@ExceptionHandler(OriginalWebException.class)publicStringhandleOriginalWebException(OriginalWebExceptionexception){return"OriginalWebException.html";}}
TransverseExceptionHandler.java
@ControllerAdvicepublicclassThrowExceptionController{@ExceptionHandler(OriginalWebException.class)publicStringhandleOriginalWebException(OriginalWebExceptionexception){return"OriginalWebException.html";}}

@Controllerの場合、例外ハンドリングが可能なのは同一Controllerで発生した例外だけだが、@ControllerAdviceの場合は全てのControllerを横断的にハンドリングできる。

また、@Controller@ControllerAdviceどちらでもハンドリング可能な場合は@Controllerに付与された方が優先される。

ErrorPage

SpringBootにおけるErrorPageは従来のSpringFWのWeb.xmlで定義していたErrorPage要素とほぼ等しい。

ErrorPageはステータスコードか例外クラスとパスをマッピングし、ハンドリングを行えるようにする。
概略図を下記に示す。
image.png

ErrorPageの登録の仕方は下記に示すようにErrorPageRegistrarの実装クラスを作成することで行える。

AddErrorPage.java
@ConfigurationpublicclassAddErrorPageimplementsErrorPageRegistrar{@OverridepublicvoidregisterErrorPages(ErrorPageRegistryregistry){registry.addErrorPages(newErrorPage(exception,path));// new ErrorPage(HttpStatusCode, path)でも可能}}

ErrorController

ErrorControllerはSpringBootによって提供される例外ハンドリングの手法であり、SpringBootの例外ハンドリングの中で最も一般的にとられる手法である。
概略図を下記に示す。
image.png

SpringBootのアプリケーションで生じる例外がハンドリングされなかった場合、上述のErrorPage機能によって全て/errorのパスへディスパッチされることになっている。
/errorはデフォルトではBasicErrorControllerクラスがマッピングされており、そこでハンドリングが行われる。

BasicErrorControllerでは、ハンドリングはリクエストヘッダーによってHTMLかJSONを返却する。
HTMLの場合はWhiteLabelErrorPageが返却され、返却される内容はErrorAttributesによって規定されている。
JSONの場合はErrorAttributesが返却される。

ErrorControllerによるハンドリング方法をカスタマイズする場合はErrorControllerの実装クラスを作成する。
また、返却内容自体をカスタマイズする場合はErrorAttributesクラスを実装する。

まとめ

SpringBootアプリケーションにおける例外ハンドリングは、基本的にはErrorControllerを用い、より細かいハンドリングを行いたい場合はHandlerExceptionResolverを用いると良い。

Blazorを使うのであれば早めに例外対応をするのがオススメ

この内容は Blazor Advent Calendar 2019の9日目の記事です

最初は例外なんて考えずバリバリ作る人もBlazorでは例外対応をしよう!

Blazorを使うなら早めの例外対応をお進めします。特に最初は例外とか深く考えず、バリバリ作ってみるタイプの人でもやった方が良いと思います。実際、僕も新しい技術をさわる際に言語仕様を読み込んだり、例外を意識したプログラムを書いたりは殆どしない方です。まずサンプルさわってみて、そのサンプルを自分の作っているものに当て込んで、そこからバリバリ作って、色々落ち着いてきてから初めて例外を意識しました。でも、ことBlazorに関してはそこまで例外をほったらかしたのは、結構な時間を無駄にしたと感じました。

Blazorのサンプル通りに作っていく場合の典型的な進め方

Blazorでデータベースなどから値を取得する場合、サンプルを参考にするならWebAPI経由で値を取ってくる形になると思います。

protectedoverrideasyncTaskOnInitAsync(){//GetJsonAsyncでサーバーから値を取得forecasts=awaitHttp.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");}

で、html側は値が取れるまでLoadingを表示していて、取れたらその値を表示するプログラムになると思います。

@if(forecasts==null)//←値が取れない時はローディング表示{<p><em>Loading...</em></p>}else//←値が取れたらその値を表示{@foreach(varforecastinforecasts){<tr><td>@forecast.Date.ToShortDateString()</td></tr>}}

実際僕もこの作りのままで自作サービスを作り続けていました。

この状態で例外が起きると

さて開発中は色々例外が起きます。例えばURLの打ち間違えとか、インターネットにつなげない状態でアクセスするとか、単純な実装ミスとか。そうなるとどうなるか。GetJsonAsyncで例外が出てforecastsnullのままになります。

forecasts=awaitHttp.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");

つまりhtmlはずーっとローディング表示になります。

@if(forecasts==null)//←値が取れない時はローディング表示{<p><em>Loading...</em></p>}

例外が起きている時=ローディングから次に進まない時です。これではローディング画面がいつもより長いなと思うまで例外と気づきません。またどこで例外が起きたかも分かりません。開発者ツールを使えばサーバー側の例外は分かりますが、クライアント側の例外の場所は特定できません。Windowsの開発なら例外が出た時点でVisual Studioが勝手に実行時例外が出る箇所を捕まえてくれます。この環境でしか開発したことがなかったので、自分で例外に対して何かするのは思いもしなかったというのが正直なところです。でもBlazorは現在実行時例外が起きても特に何もしてくれません。

Blazorでの例外対応

で、対応ですが、一番のオススメは(特に開発中であれば)、エラーページに飛ばしてそのページに例外のスタックトレースを書いてしまうことです。その際少しはまったので、コツを書きます。先ずHttp.GetJsonAsyncを例外で囲んで、例外が起きたらNavigationManager.NavigateToでエラーページに飛ばします。例外のメッセージをパラメータに投げる際にはWebUtility.UrlEncodeしましょう。これでエラーページにパラメータで例外内容を渡します。

try{forecasts=awaitHttp.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");}catch(Exceptionex){NavigationManager.NavigateTo(WebUtility.UrlEncode($"error?message="+ex.Message+Environment.NewLine+ex.StackTrace));throw;}

そしてエラーページで、パラメータを表示する際にWebUtility.UrlDecodeします。このままだとスタックトレースが改行されずに表示するので\nから<br>に置き換えます。

protectedoverridevoidOnInitialized(){varurl=NavigationManager.Uri;Message=WebUtility.UrlDecode(url.ToValue("message"));Message=Message.Replace("\n","<br>");}

さらにhtmlに書く際に、@((MarkupString)Message)してマークアップ文字をそのまま表示してやります。

<div><h1>ERROR</h1>
        @((MarkupString)Message)
        <br><br><ahref="/">Topに戻る</a></div>

そうするとこんな感じで例外が表示されます。例外によっては戻るボタンでは永遠に戻れないことがある(例外が解消していなければまた例外が起きて、同じページに飛ぶ)のでTOPに戻るリンクも用意すると良いと思います。
error.png

これでOKです。さあ、あとはバリバリ作りましょう!

この内容はゴミ箱行き?

asp.net core 3.1が出て、最新のBlazorのポストを見たらAttach to process debugging from Visual Studioという一言がありました。ここに書いた内容はもういらなくなるかもと思ったですが、少なくとも何も考えずに未設定でattach出来るわけではなさそうでした。分かったら追記するかも。

[初級-中級向け]Scala基本APIを完全に理解するシリーズ② -Either編-

はじめに

[初級-中級向け]Scala基本API紹介シリーズ① -Option編-の続きです。

EitherはScalaの中心的な役割を担うクラスです。
Optionなどの他のクラスと違って少し癖があるため最初は戸惑うかも知れません。
しかしマスターすれば強力なバグ抑制機構になってくれます。

では行きましょう!

使用頻度・重要度

メソッドが多すぎするとどれが重要なのか分かりづらいので各メソッドの横にランク付けをしておきます。

☆☆☆: 非常によく使う。Scalaを書くなら必須レベル
☆☆: 使いどこでは威力を発揮する。これを使いこなすかどうかで、コードの綺麗さが変わる。
☆: あまり使わない。使いたいときにどうぞ。場合によっては使用しないほうがいいことも。

Either/Right/Left objectメソッド

apply ☆☆☆

RightとLeftに生えてるコンストラクタ。
普通に生成されます。

scala>Right(1)res0:scala.util.Right[Nothing,Int]=Right(1)scala>Left(1)res1:scala.util.Left[Int,Nothing]=Left(1)

cond ☆☆☆

隠れていますが、こいつを使いこなすかどうかで結構コード量が変わってきます。
Either生成のよくあるケースとしては条件によってRightかLeftかを振り分けるというものだと思います。
このメソッドに条件、Rightのときの値、Leftのときの値を渡せば勝手にやってくれます。

// こういうやつがscala>if(true)Right("ok")elseLeft("fail")res0:scala.util.Either[String,String]=Right(ok)// こんな感じで書ける。scala>Either.cond(true,"ok","fail")res1:scala.util.Either[String,String]=Right(ok)scala>Either.cond(false,"ok","fail")res2:scala.util.Either[String,String]=Left(fail)

Either クラスメソッド

map ☆☆☆

Rightの場合のみに中身を変換したいときによく使います。
挙動としてはOptionのSomeに似ています。
for式でも書くことができます。
Option同様非常によく使用するので必ずマスターしましょう。

scala>Right(1).map(_*2)res0:scala.util.Either[Nothing,Int]=Right(2)scala>vall:Either[Int, Int]=Left(1)l:Either[Int,Int]=Left(1)scala>l.map(_*2)res1:scala.util.Either[Int,Int]=Left(1)// for式で同じことが書けるscala>for{|r<-Right(1)|}yieldr*2res2:scala.util.Either[Nothing,Int]=Right(2)

flatMap ☆☆☆

Eitherにはflattenはありませんが、flatMapはOption同様Eitherになる処理を行った後に一つEitherを剥がしてくれます。
直接的に使用することもあれば、for式のジェネレータ構文で間接的に使用することもあり、非常によく使うメソッドです。
個人的にはfor式のほうがスッキリして好みです。
ScalaはmapとflatMapでできています。

// 2つの式は同じものを返すscala>Right(1).flatMap(r=>Right(2).map(rr=>r+rr))res0:scala.util.Either[Nothing,Int]=Right(3)scala>for{|r<-Right(1)// ここの<-はflatMap|rr<-Right(2)// ここの<-はmap|}yieldr+rrres1:scala.util.Either[Nothing,Int]=Right(3)

foreach ☆

Rightのときのみなにか処理を行いときに使用します。
Optionの時同様Unitが返るので、関数型を好むScalaではあまり使わないようにしましょう。

scala>Right(1).foreach(println)1scala>Left(1).foreach(println)// 何も表示されない

isRight/isLeft ☆☆

RightかLeftかを判定します。
基本的にはmapを使用して、それができないときにこれらのメソッドを使用しましょう。

scala>valr=Right(1)r:scala.util.Right[Nothing,Int]=Right(1)// こういう返り値が多様なものはmapじゃ厳しいscala>if(r.isRight)relseprintln("left")res0:Any=Right(1)

getOrElse ☆☆

Rightのときに中身を取り出し、Leftのときにデフォルト値を取得します。
Optionと同じでEiitherであること自体がそもそも意味を持つ(処理が成功したのか失敗したのかとか)ので、無闇に使用しないようにしましょう。
一連の処理の最後で呼び出すなどが望ましい使い方です。

scala>valr:Either[Int, Int]=Right(1)r:Either[Int,Int]=Right(1)// こういうやつがscala>rmatch{|caseRight(v)=>v|caseLeft(_)=>2|}res0:Int=1// こう書けるscala>Right(1).getOrElse(2)res1:Int=1scala>Left(1).getOrElse(2)res2:Int=2

filterOrElse ☆☆

Rightでフィルターを通る、もしくはLeftのときにそのまま値を返します。
逆にRightでフィルターを通らないときにはデフォルトで設定した値がLeftで返ります。
RightをフィルターしてLeftにしていきたいときなどに使用できます。
連続で適用すればすべてのフィルターを生き残ったRightを取得、みたいな使い方もできます。

scala>Right(12).filterOrElse(_>10,-1)res0:scala.util.Either[Int,Int]=Right(12)scala>Right(7).filterOrElse(_>10,-1)res1:scala.util.Either[Int,Int]=Left(-1)scala>Left(7).filterOrElse(_=>false,-1)res2:scala.util.Either[Int,Nothing]=Left(7)scala>Right("ab").filterOrElse(_.endsWith("b"),"not end").filterOrElse(_.startsWith("a"),"not start")res3:scala.util.Either[String,String]=Right(ab)

swap ☆☆

RightとLeftを入れ替えます。
Rightな値とLeftな値同士を用いた処理などを書くときに利用できます。

scala>valright=Right(2)right:scala.util.Right[Nothing,Int]=Right(2)scala>valleft=Left(3)left:scala.util.Left[Int,Nothing]=Left(3)scala>for{|r1<-right|r2<-left.swap// ここでLeftをRightに変換してあげないと中の値を取り出せない|}yieldr1*r2res0:scala.util.Either[Nothing,Int]=Right(6)

contains ☆☆

Rightかつ引数の値と等しいかのチェックを行います。
Leftの場合には即falseです。
つまり、Rightかつ値と等しい場合のみtrueを返します。

// こういうのがscala>ematch{|caseRight(v)=>v==1|caseLeft(_)=>false|}res0:Boolean=true// こう書けるscala>e.contains(1)res1:Boolean=true

exists ☆☆

Rightのときに検査を行い結果をBooleanで返却します。
Leftのときは問答無用でfalseを返します。
Containsよりも柔軟にRightの中身を検査できます。
ただし同値比較のときはcontainsのほうが意味が明確になるため、そちらの方を使用したほうがいいでしょう。

// containsの方が意味が明確scala>Right(1).contains(1)res0:Boolean=truescala>Right(1).exists(_==1)res1:Boolean=true// existsのほうが柔軟scala>Right(1).exists(_%2==1)res2:Boolean=true

forall ☆☆

Rightのときに検査を行うのはexistsと同様ですが、Leftのときにはtrueが返ります。
検査対象が無いときにはそもそもテストが通っても通らなくても真というのがforallの意味です。
トリッキーな動きをするので使用する機会は少ないですが、使用できる場面で使うとかっこいいです。

scala>vale:Either[Int, Int]=Right(1)e:Either[Int,Int]=Right(1)scala>valp=(a:Int)=>a%2==0p:Int=>Boolean=$$Lambda$6157/1332242319@7351b15a// こういうのがscala>ematch{|caseRight(v)=>p(v)|case_=>true|}res0:Boolean=false// こう書けるscala>eforallpres1:Boolean=false

fold ☆☆☆

RightかLeftによって処理を振り分けることができます。
特に関数型でまともに書くと、一連の処理の最後に結局Right/Leftどっちなのかによってmatch caseを書くことはザラにあるためfoldを使用することをおすすめします。

scala>vale:Either[Int, Int]=Right(1)e:Either[Int,Int]=Right(1)scala>valfa=(_:Int)=>print("right")fa:Int=>Unit=$$Lambda$6232/529040566@7cafa6ebscala>valfb=(_:Int)=>print("left")fb:Int=>Unit=$$Lambda$6233/209463406@2e6905f9// こういう処理がscala>ematch{|caseRight(v)=>fa(v)|caseLeft(v)=>fb(v)|}right// こうやって書けるscala>e.fold(fb,fa)right

toOption/toSeq/toTry ☆☆☆

RightをSome/要素が一つのSeq/Successへmappingし、LeftをNone/空のSeq/Failureへと変換します。
Scalaはこれらのクラスを処理の流れの中で使用した結果、各クラスを一つにまとめ上げるという処理が頻発します。
そのときにこれらのメソッドで変換することでシンプルに変換処理を記述することができます。

scala>vale:Either[Int, Int]=Right(1)e:Either[Int,Int]=Right(1)// こういうのがscala>Some(e)flatMap{|caseLeft(_)=>None|caseRight(v)=>Some(v)|}res0:Option[Int]=Some(1)// こうかけてシンプルscala>Some(e).flatMap(_.toOption)res1:Option[Int]=Some(1)// forを使うことも可能scala>for{|ee<-Some(e)|v<-ee.toOption|}yieldvres2:Option[Int]=Some(1)

joinLeft/joinRight ☆☆

Eitherがネストした場合にネストを一つ剥がしてくれます。
Eitherは右と左という対等な概念があるせいで、単純にflattenができません。
そのためこのjoin系のメソッドで型に応じて適切にネストを消してくれます。
joinLeftはLeftがネストした場合にのみLeftを返し、joinRightはRightがネストした場合にのみRightを返します。

/** joinLeft */// LeftとRightがネストしたときはRightscala>Left[Either[Int, String], String](Right("flower")).joinLeftres0:scala.util.Either[Int,String]=Right(flower)// Leftがネストした場合のときのみLeftscala>Left[Either[Int, String], String](Left(12)).joinLeftres1:scala.util.Either[Int,String]=Left(12)// そもそもRightのときにはRightscala>Right[Either[Int, String], String]("daisy").joinLeftres2:scala.util.Either[Int,String]=Right(daisy)/** joinRigth */// RightがネストしたときのみRightscala>Right[String, Either[String, Int]](Right(12)).joinRightres3:scala.util.Either[String,Int]=Right(12)// RightとLeftがネストしたり、scala>Right[String, Either[String, Int]](Left("flower")).joinRightres4:scala.util.Either[String,Int]=Left(flower)// そもそもLeftのときにはLeftscala>Left[String, Either[String, Int]]("flower").joinRightres5:scala.util.Either[String,Int]=Left(flower)

left/rightメソッドとProjectionクラス ☆☆☆

ここまで読まれた方でEitherの使用方法に疑問を感じた方もいるかもしれません。
なぜならEitherにおいてLeft/Rightは対等なはずなのにRightがあまりにも優遇され過ぎだからです。
mapを始めとし、containsやforAllまでRightを中心としてAPIが組み立てられています。
理由は単純です。
Optionなどと違い、EitherはLeftの中身・Rightの中身という2つの対等な値がある以上、どちらかを選択してmapなどの処理を組み建てる必要があるからです。
Scalaでは慣習的にRightを正常系として扱うため、Rightが優先されているのでしょう。
ではLeftを中心として処理を組み立てたい時、Rightの様に優秀なAPIを利用して処理を組み立てることはできないのでしょうか?
もちろんあります。
それがleft/rightメソッドを介して生成されるLeftProjection/RightProjectionクラスです。
これらは明示的にどちら側の値にmapなどの処理を適用するかを定めたクラスです。
RightProjectionクラスは普通のEitherクラスと挙動が変わりませんが、LeftProjectionクラスはLeftを中心としてAPIが組み直されています。
そのためLeftな値に対して

  • map
  • get
  • exists
  • filter
  • flatMap
  • foreach
  • forall
  • getOrElse
  • toOption/toSeq

メソッドを使用したいときにはEitherをleftにprojectionして使用するようにしましょう。

終わりに

EitherはOption同様Scalaの中心を担うライブラリです。
必ずマスターするようにしましょう。

君も今日からScalaマスター!

[java] 例外をスローする

例外処理はjavaの基本であるが、認識違いをしていたので備忘録として残す。

発生した問題

メソッドの呼び出し元に例外を投げたかったので、catchした例外をスローしようとすると、なぜかコンパイルエラーになっていた。

実装

例えばタイムアウトエラーを投げる場合は以下の通りである。

publicvoidfoo(){try{fetch();}catch(SocketTimeoutExceptione){}}publicIntegerfetch(){try{//Http通信}catch(SocketTimeoutExceptione){throwe;//ここでコンパイルエラー}finally{}returnnumber;}

認識違い

throwのほかに
thorws句というのがあるがこれはcatch文と同じ働きがあると思っていた。しかし実際それがあるメソッドは例外を投げる可能性がるメソッドという意味であった。

そこで例外をスローしているメソッドにthrows句を付け足すとコンパイルエラーが消え、foo()でキャッチできるようになった。

感想

基礎の基礎で躓くとは思わなかったが、今のうちに知れてよかった…

【PHP8.0】例外をcatchしたいけど何もしたくない

例外をcatchしたいけど何もしたくない。

try{foo();}catch(Throwable$e){// 何もしない}

何もしないのにわざわざ変数に受け取るのって無駄じゃありませんか?

というわけでnon-capturing catchesというRFCが提出されました。

PHP RFC: non-capturing catches

Introduction

今のところ、PHPは例外を明示的に変数でキャプチャする必要があります。

try{foo();}catch(SomeException$ex){die($ex->getMessage());}

しかしながら、ときにはその変数を使わない場合もあります。

try{changeImportantData();}catch(PermissionException$ex){echo"You don't have permission to do this";}

これを見た人は、プログラマが意図してこの変数を使わなかったのか、あるいはバグなのかがわかりません。

Proposal

例外を変数に受け取らずcatchできるようにします。

try{changeImportantData();}catch(PermissionException){// catchした変数を使わないという意図が明白echo"You don't have permission to do this";}

Prior art

7年前にあった似たようなRFCは、以下のように例外名も省略可能にするという理由で否定意見が多数でした。

try{foo();}catch{bar();}

いっぽう、このRFCは肯定的な反応が多く、再検討の余地があります。

Backward Incompatible Changes

後方互換性を壊す変更はありません。

Proposed PHP Version(s)

PHP8.0

RFC Impact

特になし。

Vote

投票は2020/05/24まで、投票者の2/3の賛成で受理されます。
このRFCは賛成48反対1の圧倒的多数で受理されました。

Patches and Tests

https://github.com/php/php-src/pull/5345

References

メーリングリスト

感想

このRFCが意図しているところは決して例外の握り潰しなどではなく、例外処理のロジックに例外の中身を使わないときに省略できるというものです。
変数に受け取る処理がなくなるので速度も速まることでしょう。

しかしですな、こんな機能があったら絶対にこんなコードを書く奴が出てくるわけですよ。

PHP8
try{foo();}catch(Exception){}

いやまあ、こんなの書いてしまう人は今でもやってると思いますけどね。

PHP7
try{foo();}catch(Exception$e){}

なら別にあってもいいか。

劇的に便利になるというわけではないですが、ちょっと気が利く書き方ができるようになりますね。

最後にもう一度言いますが、冒頭のコードは悪い例なので決して真似してはいけません。

Rails によるカスタム例外の設定とエラーハンドリング

Rails で例外を発生させたい際は,raise...つまり RuntimeError をよく使用するかと思います。

しかし,サービス上の制約から,特定の状況下で例外を発生させる場合,raiseだけでは物足りなくなる時があります。
raiseでは「何かまずいことが起きてしまいました!」程度のことしか伝えてくれません。まぁ,引数に渡す message を見れば理解できるかもですが...

兎にも角にも,特定の状況下に対する例外が存在するなら,その例外に対して名前を付けてあげましょう。

カスタム例外を設定すると,発生時に「何に対する例外か」がパッと理解できるようになりますし,特定の動作に誘導することも容易になりますので,良いことづくめです!

□ 本文

■ 前提情報

使用するアプリケーション

Railsチュートリアルで作成する SampleApp における、users_controller のusers#editに着目して実装します。

app/controllers/users_controller.rb
classUsersController<ApplicationControllerbefore_action:correct_user,only: [:edit,:update]#...private#...# 正しいユーザーかどうか確認defcorrect_user# GET   /users/:id/edit# PATCH /users/:id@user=User.find(params[:id])redirect_to(root_url)unlesscurrent_user?(@user)end#...end

カスタム例外の設定規則

  • StarndardErrorを継承する
  • クラス名の末尾にErrorを付ける

■ カスタム例外の設定方法

実装自体は,とても単純なのですが,設定場所にいくつか種類がありますので,紹介していきます。

実装例1: 発生ファイルに直接設定

その名の通り,例外が発生するファイル自身に設定します。

特定のクラスに強く結びつける方法であることから,Model 層や Service 層で見かけたりします。

app/controllers/users_controller.rb
classUsersController<ApplicationControllerclassNotPermittedError<StandardError;endbefore_action:correct_user,only: [:edit,:update]#...private#...defcorrect_user@user=User.find(params[:id])raiseNotPermittedError,"あなたにリクエスト権限がありません"unlesscurrent_user?(@user)end#...end

実装例2: app/ 配下に設定

自作の module を app/ 配下に設定します。

validators や services と同じ考え方で配置する感じですかね。

app/errors/application_error.rb
moduleApplicationErrorclassNotPermittedError<StandardError;endend
app/controllers/users_controller.rb
classUsersController<ApplicationControllerbefore_action:correct_user,only: [:edit,:update]#...private#...defcorrect_user@user=User.find(params[:id])raiseApplicationError::NotPermittedError,"あなたにリクエスト権限がありません"unlesscurrent_user?(@user)end#...end

実装例3: lib/ 配下に設定

lib ディレクトリの存在目的から見ると,王道パターンかも。

なお,lib 直下の配置が気になる場合,適宜ディレクトリを挟んで設定してください。

config/application.rb
#...Bundler.require(*Rails.groups)# ↓ 追加コードrequire_relative'../lib/exception.rb'moduleSampleApp#...end
lib/exception.rb
moduleApplicationclassError<StandardError;endclassNotPermittedError<Error;endend
app/controllers/users_controller.rb
classUsersController<ApplicationControllerbefore_action:correct_user,only: [:edit,:update]#...private#...defcorrect_user@user=User.find(params[:id])raiseApplicationError::NotPermittedError,"あなたにリクエスト権限がありません"unlesscurrent_user?(@user)end#...end

■ エラーハンドリング

さて,これでカスタム例外は作成完了ですが,仕上げが残っています。

このままでは,例外を発生させたままです。
ユーザ側から見ると 500 エラー画面が出てきて,なぜ強制終了したのか理由がわかりませんし,サービスの操作感として連続性が失われるのも避ける必要があります。

元のコードでは,不正なアクセスをしたユーザに対して,root_url にリダイレクトさせていますので,カスタム例外が発生した際は,同じようにリダイレクトさせましょう。

また、例外を握り潰さないために、サーバ側に理由を説明するためのログを残しましょう。

app/controllers/application_controller.rb
classApplicationController<ActionController::Base#...rescue_fromApplication::NotPermittedError,with: :redirect_root_pagedefredirect_root_pageRails.logger.info"ルート URL にリダイレクト: #{exception.message}"ifexceptionredirect_toroot_url,flash: {danger: "閲覧権限がありません"}end#...end

※ シンプルに表記することを目的として application_controller.rb に記入していますが,色々追加されるファイルでもあるため,concerns に切り出すと尚可読性が高まるでしょう。

□ 余談

OSS におけるカスタム例外の設定方法も調べてみると,見事にバラバラだったので,プロジェクト毎に設定方法が異なるかもしれない :thinking:

具体的な命名も設定場所も異なるため、プロジェクトに合わせて、柔軟に対応しましょう。

今回使用した PR です→カスタム例外の設定 by masayuki-0319 · Pull Request #9 · masayuki-0319/sample_app

Browsing Latest Articles All 29 Live