Panic を恐れるべからず

Rust で panic!assert! の利用を躊躇うべきでないという話。 個人の見解マシマシでお送りします。

この記事は Rustその3 Advent Calendar 2019 の18日目の記事である[0]

TL;DR

  • 不正な値の存在の存在を許してはいけない。 不正な値が存在できてしまう時点で、未定義動作を覚悟するくらいのつもりでいるべきである。

  • 満たされるべき条件を満たさない時点で、プログラムの内部的な整合性は既に破綻しており、未定義動作も同然の状態である。 これ以上余計なことをする前にさっさとクラッシュせよ。

  • 整合性破壊バグから「うまく復帰」できると思うのは甘え (極論)。

もうちょっと詳しくは 本題大雑把な指針まとめ を参照。

いろいろな panic

Rust で panic させるにも様々な方法がある。 まずはそれらを見ていこう。

Option::expect()
Result::expect()

selfOption::Some(_)Result::Ok(_) であればその中身の値を返し、そうでなければ&str のメッセージを表示して panic するResult の場合は、エラー値のデバッグ表示も行われる。

Option::unwrap()Result::unwrap() の変種だが、情報量のない既定のメッセージを表示する unwrap よりも、文脈の情報を提示できる expect を常に使うべきである。

&str では固定のメッセージしか表示できないため、 foo_option.unwrap(&format!("Field {} not found", field_name)) のように format マクロを使う用法もありえるが、これは unwrap が成功する場合にも常に実行されてしまうため原則として避けるべきである。 後述の unwrap_or_else() の利用の方が望ましい。

panic!()

引数をフォーマットして表示し、 panic する

最も汎用的な panic 手法である。

unreachable!()

unreachable である旨に加えて引数をフォーマットして表示し、 panic する

到達不可能なコードであることを表現するのに使う。

assert 系

特定の条件が必ず満たされているはずであることを表現する。

assert!()
debug_assert!()

引数に与えられた条件式が成り立つか確認する。 成り立たなければ、残りの引数をフォーマットして表示し、 panic する

debug_assert!() はリリースビルドでは無効化される。

assert_eq!()
debug_assert_eq!()

与えられた2つの引数が等しいか確認する。 等しくなければ、残りの引数をフォーマットして表示し、 panic する

debug_assert_eq!() はリリースビルドでは無効化される。

assert_ne!()
debug_assert_ne!()

与えられた2つの引数が等しくないことを確認する。 等しければ、残りの引数をフォーマットして表示し、 panic する

debug_assert_ne!() はリリースビルドでは無効化される。

Option::unwrap_or_else()Result::unwrap_or_else() と別の panic 手段の併用

たとえばこんな感じ: foo_result.unwrap_or_else(|e| panic!("Failed to foo: {}\nbacktrace: {:?}", e, e.backtrace()))expect()&str とエラーのデバッグ表示を一緒に表示するだけという原始的なものであるため、エラーから追加で何かしらの情報を得たり整形して表示したうえで panic したいという場合にこのような手法を用いることがある。

panic は回復不可能なエラーを意味する

Rust にはネイティブで Result 型によるエレガントなエラー処理機構が入っている。 Result はいくつもの都合の良い性質を持つ。

  • 正常終了した場合の値を取り出すのに、必ず matchifunwrap() 等による検査が必要になる
    • エラーとして返ってきた NULL-1 を誤って正常終了した結果であるかのように利用してしまう心配がない。
  • ? 演算子 により、エラー処理を呼び出し元へ伝播あるいは丸投げすることが可能
    • もちろん、 match 等でエラー処理を行った上で改めて呼び出し元へ投げることもできる。

すなわち Rust のエラー処理は、 C のような戻り値による成功・失敗の返却と他言語における例外機構のような仕組みの、どちらとしても機能するようにできている[1]

では panic とは一体何なのかという話だが、そもそも原則的に panic は通常のエラー処理のための機構ではない

This allows a program to terminate immediately and provide feedback to the caller of the program. panic! should be used when a program reaches an unrecoverable state.

std::panic - Rust (Rust 1.39.0 リファレンス), 強調は引用者による

公式リファレンスにもあるように、 panic はプログラムが回復不可能な状態 (unrecoverable state) に陥ったときに発生させるべきものである。 通常、一般の「エラー」からは通常のプログラム状態へと復帰可能なものである (特にライブラリ自身は回復可能なエラーから復帰すべきかどうか自ら決定すべきでない) から、 panic ではなく Result でエラーを通知して呼び出し元に判断を委ねたり、然るべき回復処理を行うということになる。

逆に、「ユーザが通常状態への回復処理を行いようのないエラー」について、ライブラリが利用者にその扱いを委ねる必要がないともいえる。

Sometimes, bad things happen in your code, and there’s nothing you can do about it. In these cases, Rust has the panic! macro. (中略) This most commonly occurs when a bug of some kind has been detected and it’s not clear to the programmer how to handle the error.

Unrecoverable Errors with panic! - The Rust Programming Language (Rust 1.39.0 TRPL), 強調は引用者による

プログラムの不整合

では、通常のエラー状態と、 panic すべき「回復不可能な状態」は何が違うのか。 これを考えるためには、プログラム状態の整合性[2]について気にしたい。

端的には、不整合とは「設計時の想定と違う状況」のことである。

事前条件検査や不整合の std における扱い

プログラムの各コードは (あるいは各式に至るまで) 基本的に、事前条件、事後条件、不変条件のような必ず満たされるべき条件や前提を持っている。 これらは暗黙であったり明示的であったり、あるいは検査されたりされなかったりする。 こういった条件や検査が std ライブラリでどのように行われているか、いくつか例を挙げる。

  • 条件: &str が参照するスライスは妥当な UTF-8 バイト列である。

    • この条件は std::str::from_utf8 などによる &str の作成時に検査される。 また、 &mut str 経由で行われる safe な操作でも検査されるか、そもそも条件が破壊されない操作しか許可されていない。

    • std::str::from_utf8_unchecked などは検査を行わないため unsafe 関数である

      This function is unsafe because it does not check that the bytes passed to it are valid UTF-8. If this constraint is violated, undefined behavior results, as the rest of Rust assumes that &strs are valid UTF-8.

      std::str::from_utf8_unchecked - Rust (1.39.0), 強調は引用者による

      また、 &mut str 経由で行われる unsafe な操作では、事前条件を満たさない使い方をした場合、危険な結果を生む。

      Failing that, the returned string slice may reference invalid memory or violate the invariants communicated by the str type.

      str - Rust (1.39.0), 強調は引用者による
  • 条件: std::num::NonZeroUsize は非ゼロの値を持つ。

  • 条件: 参照 (&T) は生存しているオブジェクトを指す。

    • この条件は &val のような形での参照の作成では、コンパイラによって検査される。

    • unsafe { &*ptr } (なお ptr はポインタとする) のような形での参照の作成では、 ptr の指す値が参照より長く生存していることを、実装者が保証する必要がある。 保証が破られた場合、未定義動作となる。

他にも 「char は有効な Unicode コードポイントを持つ」や「RefCell の中身の借用は参照と同じ規則に従う」など、多数の型や機能について「満たしていなければならない条件」が設定されている。 それらの保証の方法は概ね次のように分類できる。

  • コンパイラが検査する。 検査が通らなければコンパイルエラー
    • 参照の生存期間、値のスレッド間共有・転送の許可など
  • 保証を呼び出し元に一任する unsafe 関数として、呼び出される関数での検査を行わない。
    • std::str::from_utf8_uncheckedstd::num::NonZeroUsize::new_unchecked など
  • 処理の時点で検査する。 検査が通らなければエラーや Option を返す
    • std::str::from_utf8 (エラーを返す) や std::num::NonZeroUsize::new (Option を返す)、 <[T]>::get など
  • 処理の時点で検査する。 検査が通らなければ panic する
    • std::cell::RefCell::replacestd::sync::Mutex::lock[T] 型の [] 演算子など

不整合の存在は許されない

さて、こういった検査の徹底によって、safe なコードでは整合性の破壊された不正な値は存在を許されていないといえる。 また、不正な値の作成を含め 不正な処理の実行は許されていない (阻止される)

  • コンパイルエラー
    • コンパイルが通らないため、不正な処理は実行されない。

  • 値の作成でエラーや None を返す
    • このとき不正な値は呼び出し元の手に渡ることがない。 よって (もし仮に内部で一時的に不正な値が存在していたとしても) ユーザに不正な値は見えず、扱うこともできない。

  • panic する
    • コードの実行はここで中断される。 そのため、何らかの正当な値や状態を期待した続くコードが、不正な値や不正な状態とともに実行されることはない。

では unsafe でマークされたコードはどうかといえば、これはコンパイラによる検査は行われないが、開発者 (呼び出し元コード) が期待される条件を満たすことを保証する義務を負う。 unsafe なコードで条件破りを行った場合、そのようなコードは未定義動作となる。 すなわち、メモリ破壊などと同程度に危険のリスクを負うことになる。

unsafe は「整合性を破れる場所」ではなく「整合性検査をコンパイラでなく人間が行わなれけばならない場所」であることを肝に命じなければならない。

その他一般的な不整合

不整合は標準ライブラリでだけ発生するものではない。 正常な動作を行うために必要な条件が破れるのも不整合だし、コードが保証しようとした性質が満たされなくなるのも不整合である。

例として、 safe な双方向連結リストを提供するライブラリを考えよう。 Vec はノード型を持っており、各ノードは要素 (値) と前後要素のインデックスを持っているとする。

        // 想定: フィールドは外部から変更されない。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct NodeIndex(usize);

// 想定: フィールドは外部から変更されない。
// 想定: Option<NodeIndex> に対する safe な操作は未定義動作を引き起こさない。
#[derive(Default, Debug, Clone)]
struct Node<T> {
    // 想定: data は不正な値ではない。
    data: T,
    next: Option<NodeIndex>,
    prev: Option<NodeIndex>,
}

// 想定: フィールドは外部から変更されない。
// 想定: Vec<Node<T>> と Option<NodeIndex> に対する safe な操作は未定義動作を引き起こさない。
#[derive(Default, Debug, Clone)]
struct SafeLinkedList<T> {
    nodes: Vec<Node<T>>,
    first: Option<NodeIndex>,
    last: Option<NodeIndex>,
}

impl<T: Clone> SafeLinkedList<T> {
    pub fn push_back(&self, val: T) -> NodeIndex {
        let new_index = NodeIndex(self.arena.len());
        // 想定: push された要素のインデックスは new_index.0 である。
        self.arena.push(Node {
            data: val,
            // 保証したい条件0: 最後尾ノードの next は None である。
            next: None,
            // 想定: リストがノードを持つとき self.last は Some であり、ノードを持たないとき None である。
            // 保証したい条件1: 新しいノードの前のノードは、最後尾だったノードである。
            // 保証したい条件2: prev->next と next->prev でノードを参照すると、元のノードに戻ってくる。
            prev: self.last,
        });
        // 想定: リストがノードを持つとき self.last は Some であり、ノードを持たないとき None である。
        if let Some(last) = self.last {
            // 保証したい条件3: 最後尾だったノードの次のノードが、新しいノードである。
            // 保証したい条件2: prev->next と next->prev でノードを参照すると、元のノードに戻ってくる。
            // 想定: last は arena の要素を指す妥当なインデックスである。
            self.arena
                .get_mut(last)
                .expect("Should never fail: the last element must be in the arena")
                .next = Some(new_index);
        } else {
            // 想定: self.last が `None` だったとき、 self.first も `None` である。
            self.first = Some(new_index);
            self.last = Some(new_index);
        }
    }

    pub fn get(&self, i: NodeIndex) -> &T {
        // 想定: i は (他の SafeLinkedList ではなく) self によって作られたものである。
        // 想定: i は self.push_back() で作成されて以降、ユーザによって中身を不正に改変されていない。
        // 想定: i は self.push_back() で作成されて以降、 SafeLinkedList によって中身を変更されていない。
        // 想定: i が self.push_back() で作成されて以降、 self.arena から i 番目のノードは削除されていない。
        &self.nodes[i.0]
    }
}
      

単純なコードではあるが、保証したい条件や、成り立つと暗黙に想定している条件 (つまりコードが正しく動くために必要な前提) はこのように多く存在していることがわかる。 ある条件は安全なメモリアクセスに必要だし、ある条件はデータ構造を破壊せず意味のある値を返すために必要だし、ある条件は開発者の意図に反した panic が発生しないために必要である。 また、保証したい条件も、ユーザやライブラリ内部が想定している条件を常に満たすために必要なものである。 こうした多くの条件のひとつでも破れればそれは不整合である。 整合性を失ったプログラムは未定義動作に突入したり、 panic したり、あるいは誤った値や不正な値を返したりなど、不正な処理をすることになるだろう。

不整合、バグ、回復可能性

不整合からの回復の試みは一般に無意味

そもそも開発者やユーザが想定していない状態である不整合からは、実行時に回復を考えることに意味がない。 何故なら、ひとつでも前提が破れていることに気付いた時点で、その実は成り立っていない条件を根拠に正しく動く他の部分のコードも、誤った動作をしているかもしれないためである。 更に、そういった誤った条件に基く動作は、保証したかったはずの他の条件まで連鎖的に破っていくかもしれない。 壊れた状態を無理矢理に解釈して情報を復元しようにも、気付いた箇所以外にも不整合が発生していることが考えられるため、何も想定を持つことができないし、そもそも復帰しようとして集めた情報が正しいという保証もないということである。

また、不整合に気付いた時点で既に誤りは予想できない範囲まで伝播している[5]ことがありえる。 つまり、プログラムが不整合状態になったとき、プログラムは全体として既に想定を満たさない状態で動いている可能性があり、その結果として発生する処理やデータも基本的に無意味である[6]。 その無意味な処理で無意味なデータを解釈して「復帰」しようとする行為も、当然無意味である。

不整合はバグである

整合性破壊はバグ (semantic error) である。 このようなバグこそ、 The Rust Programming Language で言われている a bug of some kind であると考えるべきである。

Sometimes, bad things happen in your code, and there’s nothing you can do about it. In these cases, Rust has the panic! macro. (中略) This most commonly occurs when a bug of some kind has been detected and it’s not clear to the programmer how to handle the error.

Unrecoverable Errors with panic! - The Rust Programming Language (Rust 1.39.0 TRPL), 強調は引用者による

不整合を検出したとき、プログラムは panic すべきである

さて、やっとこの記事の本題である。

プログラムは、不整合を検出したとき panic すべきである

これまでの説明を踏まえれば簡単な話で、「不整合に気付いたとき、不整合に基く誤った値や状態は既に他へ伝播している可能性が高いため、回復は基本的に不可能で、これ以上何をしても無意味な行為や破壊行為しかできない」ということである。 傷口を広げないために、そのような修復不可能な不整合に気付き次第、プログラムは速やかに異常終了してこれ以上の破壊を阻止するしかない。 プログラムが不正な状態で実行を継続すべきではない

The panic! macro signals that your program is in a state it can’t handle and lets you tell the process to stop instead of trying to proceed with invalid or incorrect values.

To panic! or Not To panic! - The Rust Programming Language (Rust 1.39.0 TRPL), 強調は引用者による

panic の使い方

以下での説明には筆者の個人的な考えが多分に含まれることに留意されたい。

どこで何を使うべきか

冒頭でさまざまな panic の方法を挙げたが、どれをどこで使うべきか。 名前から明らかといえば明らかだが、筆者の個人的な考えも含めて解説する。

assert

「満たしていることを保証したい条件」や「満たされていることを保証したい条件」が満たされていることを可視化・検査するのに用いる。 たとえば事前条件、事後条件、不変条件などである。

assert 失敗時のメッセージを必ず用意しておくこと。

debug_assert

コストの高い検査に用いる。

また、どう考えても成り立つ条件や、ちょっと前に既に検査済みの条件を、念のため書いておくのにも用いる (後述)。

assert 失敗時のメッセージを必ず用意しておくこと。

unreachable!

到達不可能な場所で使う。

また、 unwrap_or_else と組み合わせて使うこともできる。 主に、固定でないメッセージとともに panic したいときに有用。

            dict.get(key)
    .unwrap_or_else(|| unreachable!("`dict` should have an entry for the key {:?}", key))
          
unwrap()

使ってはいけない[7]expect() を使うべし。

expect()

ResultOptionOkSome である確信があるときに使う。 説明のメッセージを動的に生成したい場合は、代わりに .unwrap_or_else() などを合わせて使う。

panic!

汎用。 他がいずれも適していないと思ったとき使う。

「念のためもう一回 assert」

assert は、「その場で成り立つ条件」を表明するものである。 つまり、同じ不変条件であっても、別々の場所で表明することには価値がある。

          fn foo(mut v: Foo) {
    assert!(some_condition_holds(v), "Some condition should hold for {:?}", v);
    // Do complex processing here.
    // Do more complex processing here.
    // Complex processing done.

    // It seems obvious for now...
    // でも念のためもう一回検査しとく
    debug_assert!(some_condition_holds(v), "Some condition should still hold for {:?}", v);
    // Do another processing.
}
        

また、開発初期にそのような条件が持続することが自明に見えても、コードが膨らんでいくにつれ、間に多くの処理が挟まるかもしれない。 そのとき、追加・変更された全てのコードがまだ先に確認した条件を壊さないと、本当に確信できるだろうか? こういった検査は、不完全な人の目だけによらず、 assert で機械的に検査するのが安全である。

unsafe ブロックのお供に

unsafe な処理は、何らかの条件を検査なしに前提として受け入れるようなものが多い。 そういった unsafe な処理は基本的に、要求される条件を満たす確信のある場面でしか使われない。 しかし検査がないと、もし何かの事故 (バグ) で条件が不成立だった場合、発覚が非常に遅れる可能性がある。 そこで debug_assert 系が便利。

          unsafe {
    // I'm pretty sure `i` is positive (and nonzero).
    debug_assert!(i > 0_isize);
    NonZeroUsize::new_unchecked(i)
}
        

debug_assert 系はリリースビルドでは消えるので、パフォーマンスが重要な場面で問題になることはない。 また、そもそもデバッグビルドはデバッグしやすさを前提にしており非常に遅い (たとえば加算にいちいちオーバーフロー検査が入ったりする!) ため、安全性の検査のための多少の追加コストは許容されるだろう。

assert の無効化は期待されるコードの挙動を変えるべきでない

これも筆者の個人的な考えだが、assert の有無は、期待されるコードの挙動を変えるべきでない。 つまり、あらゆる assert は、プログラムが完全に想定 (仕様) 通りに動いたとするなら全く存在しなくても構わないように使うべきである。

        pub struct Id(NonZeroU32);

impl Id {
    /// Creates a new ID from the given index.
    ///
    /// # Panics
    ///
    /// Panics if the given value is larger than or equal to `std::u32::MAX`.
    pub fn from_index(index: usize) -> Self {
        assert!(index < std::u32::MAX as usize); // <- This assert should never be removed!
        Self(unsafe {
            NonZeroU32::new_unchecked(index + 1)
        })
    }
}
      

この assert は「std::u32::MAX 以上の値を渡されたら panic する」という仕様を実現するために必須のコードとなっている。 仕様ということはすなわちそのような大きな値が渡されることも想定内であり、そこで panic するのは期待される挙動ということになる。 こういった場面で、省略不可能な形で assert を使うのは危険ではないかと、私は思うわけである。

assert は名前の通り何事かを主張する、あるいはそのはずであると断言することであって、「条件が満たされなかったら panic」というのはその結果に過ぎず本来の目的ではないと考えるべきである。 つまり「検査なんかなくても最初から成り立ってるはずなんだけど念のため言っとくと、この条件が成り立つよ」というくらいの気持ちで使うものである。 assert は不整合状態を防ぐ安全性確保のためのストッパーであり、となれば、単に実行フローを制御するための if 文のように使うべきではないのではないだろうか。

assert を常に省略可能に保つことは、 debug_assert との切り替えを安全にするという意味でも有用である。

先のセクションで述べた例のように、同じ条件を近い場所で複数回検査したいということがあるかもしれない。 ある assert の手前に同じ条件を検査する assert を挿入したとき、後続の assertdebug_assert に置き換えたいという欲求が生じるかもしれない。 あるいは、開発初期には assert で検査する条件が単純だったが、開発が進むにつれ条件の計算が重くなり、リリースビルドで何度も実行したくなくなるかもしれない。 assert 系マクロの無効化が期待される挙動に影響を与えないという確信を持っていれば、このような切り替えを躊躇なく、エンバグしないという確信を持って行うことができる。

「サーバでは panicしたく ないんです」 #n575

サーバだからといって、不整合がプログラム全体へ伝播していないと思えるなどということがあろうか? そんなことはない。 不整合に気付いたら panic すべきである。

……と筆者は思うわけだが、そうはいっても panic したくない、サーバは panic するべからず、という思想を持つ人々もいるらしい。 そこで妥協案として、 mutex と thread を使うという方法がある。

thread で panic を検知する

If the panic is not caught the thread will exit, but the panic may optionally be detected from a different thread with join. If the main thread panics without the panic being caught, the application will exit with a non-zero exit code.

std::thread - Rust (1.39.0)

メインスレッド以外が panic して、 catch_unwind でキャッチされず、かつ panic で即座に abort する設定になっていなかった場合、スレッドの panic を別スレッドから (JoinHandle を通じて) 検知することができる。

もちろんこの場合にも、 panic 以前に既にスレッドは不整合状態になっていたし、その状態での出力や処理は信用できたものではない。 が、たとえばサーバが新規コネクションを受け付けるスレッドと個々のクライアントへの応対を行うスレッドを別々に持っており、かつスレッド間での通信が極めて限定されているか行われていないなどの場合であれば、クライアント応対のスレッドが落ちても新規コネクション受け付け処理は依然として整合のとれた状態で稼動しているという保証をすることはできるかもしれない。

すべては「不整合が漏れ出していないことを確信できるか」次第である。

mutex で panic 由来の潜在的な不整合を扱う

また、 thread と panic に関連した mutex の面白い機能として、 mutex を書き込み可能な状態で lock していたスレッドが panic したとき、 mutex は poisoned (汚染された) 状態になる。 これは、壊れているかもしれないデータに、壊れているかもしれないと想定しているコードのみがアクセスできるようにする仕組みである。

データ更新の途中でスレッドが panic した場合など、更新が未完了のままのデータが mutex に残されてしまうことがありえる。 panic したスレッドから見れば、 panic 以降で基本的に不整合のデータで困るような処理は行われないため問題はない。 しかし、外部のスレッドがその中途半端な状態を観測できた場合、不整合状態のデータを観測してしまうリスクがある。 そこで、このようなとき mutex はデータが poisoned であるとしてエラー状態の一部として扱うことで、「不整合の可能性があると認識したコードにしかデータを触らせない」という形で不整合の意図せぬ伝播を阻止しているのである。

その他の話題

ensure! マクロ

エラーハンドリングを支援する crate には、 ensure! マクロを提供するものがある[8]。 これは assert と同様に与えられた条件が満たされているか検査するが、満たされていなかった場合に panic するのではなく与えられたエラーを返すというマクロであり、これこそ、仕様の一部として実行フローの制御に使うことのできる assert である。

ただし、このマクロは事前条件を満たさない引数などの通知には使えるが、整合性検査ではあまり使うべきではない点には留意すべきである。 基本的に整合性エラーが起きた時点でライブラリのユーザに取れるアクション (取って意味のあるアクション) はほとんどないため、ユーザにそのような不整合を無視させる選択肢を与えるべきでない。

たとえば「有向非巡回グラフだと思ってたのにいつのまにか循環が入ってました!」などとエラーで報告されても、ユーザに打つ手はないし、何事もなかったかのように無視させてはいけない。 こういった不整合の検出は、再三述べたように Result ではなく panic によって通知されるべきだ。

unwrap() するな」

unwrap() を使うなというのは、文字通りに解釈すれば確かにその通りである。 テストや書き捨てるコードを除き、ほぼすべての場合において unwrap() よりも expect() が優先されるべきである。

では「expect() するな」というのが常に言えるかといえば、私はそうは思わない。 何らかの値が SomeOk であることを当然に期待できる状況というのは、現実的に存在する。

        fn hoge(&mut self) -> &Node {
    let child = self.generate_child();
    self.last_child = Some(child);
    // self にいろいろな処理
    // self にもっといろいろな処理
    // いろいろな処理が終了

    // last child への参照を返す
    // self.last_child は `None` にされていない確信がある
    self.last_child
        .as_ref()
        .expect("Should never fail: previously set to `Some`")
}
      

このように expect() を使えばシンプルに assert 相当の確信を表明できるところで、わざわざ match と panic を使うべきだろうか?

            // last child への参照を返す
    // self.last_child は `None` にされていない確信がある
    match &self.last_child {
        Some(v) => v,
        None => panic!("Should never fail: previously set to `Some`"),
    }
      

何かが良くなったとは思えない。

それでもまだ悩んだら

  • プログラムが完全に掌握しているはずのデータが誤っていれば、 panic せよ

    • 大抵の場合、プログラムのロジックが壊れている。 壊れたロジックと壊れた状態をもとに、プログラムが正しく動作できるわけはないし、ユーザにできることもない。 さっさと異常終了すべきである。

      「ライブラリのユーザ (つまり開発者) がドキュメントを読んでねえのが悪いだろ」などもこれに含む。 (NonZeroUsize::new に 0 を渡すなど。)

  • プログラムのバグが原因なら、 panic せよ

    • 「バグがなければ起きないはず」の状況が起きたとき、プログラムは既にバグを踏み抜いた後である。 プログラムは不整合状態に突入しており、以降の一切の実行は信頼できない。 さっさと異常終了すべきである。

  • 環境や外部データに問題があるなら Result を返すべし

    • プログラムは、自身の完全な制御下にないデータを警戒する義務がある。

  • エンドユーザに問題があるなら Result を返すべし

    • エンドユーザは、プログラムの完全な制御下にないため、当然、不正な操作の拒否を仕様に含むべきである。

      エンドユーザは環境や外部データの一部であるともいえる。

  • ありえないことが起きたなら panic せよ

    • ありえないことが起きたとき、プログラムは既に想定外の不整合状態に突入しており、以降の一切の実行は信頼できない。 これは復帰できることを期待すべきでないバグであり、プログラムをさっさと異常終了すべきである。

      これは「プログラムのバグが原因」ということでもある。 認めたくないかもしれないが、「起きうることを想定できていない」という時点で仕様レベルのバグがあるか、思った通りにプログラム状態を更新できないという実装バグのどちらかである。

まとめ

  • 不整合について
    • 不整合から復帰しようとするべきでない

      • 不整合が、発見した瞬間の発見した場所のみに留まっていると思わないこと。 プログラム全体が既に汚染されていると思うこと。

        不整合の発見は手遅れのサイン

    • それはバグです

      • 不整合状態は実行時エラーではなく仕様バグか実装バグである。

        バグをエラーとして通知してユーザに処理させようとしてはいけない。 ユーザはバグを仕様とは見做していない。

  • panic について
    • 不整合状態を発見したら panic せよ

      • 不整合の発見は手遅れのサイン。

    • panic を使うべき場面で Err を返すべからず

      • 不整合状態をユーザに無視させてはいけないし、復帰を試みるべきでない。

    • assert を特定の意味で使うべし

      • 不整合状態になっていないはずであることを表明するのに使うこと。

        assert を省略可能にする (省略が仕様を変えないようにする) のが良い。

    • panic を躊躇うべからず

参考文献