Chapter 10

アサート

とが
とが
2023.01.27に更新

assert! マクロ

アサートは,プログラムの異常に気付きやすいようにするための手段です.次のコードを見てください.

use proconio::input;

fn main() {
    input! {
        x: i32,
    }
    let r = x % 10;
    assert!(0 <= r && r < 10);
    println!("あまりは {}", r);
}

input! マクロで整数 x を受け取り, 10 で割った余りを変数 r に代入しています.このとき, 0 \leq r < 10 は成り立つでしょうか.これをチェックしているのが, assert! マクロです.

assert!( ) の括弧内に条件を書き, assert!(0 <= r && r < 10); とします.

  • ここでもし 0 <= r && r < 10 が成り立っていれば,何事もなかったかのようにプログラムの続きが実行されます.
  • 一方,もし 0 <= r && r < 10 が成り立っていなければ,プログラムはパニックし,その続きを実行することなく終了します.

まず,このプログラムに標準入力として 123 を与えて実行してみましょう. x = 123 なので r = 3 となり, 0 \leq r < 10 が成り立つはずです.やってみると あまりは 3 と出力され,ちゃんと assert! の続きが実行されていることが分かります.

次に,標準入力を -123 に変えてみましょう.おや?標準出力に何も出力されません(println! が実行されていません).代わりに標準エラー出力に

thread 'main' panicked at 'assertion failed: 0 <= r && r < 10', src/main.rs:8:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

と出力されます.これが,assert! で括弧内の条件が成り立っていなかったときに起こる「パニック」です.

つまり,このプログラムでは x = -123 のときに 0 \leq r < 10 が成り立たなくなることが分かります.

実は,/ / % 演算子を使うときは,負の場合の挙動に注意する必要があります. xy の正負を変えながら x / yx % y を計算すると次のようになります.

x y x / y x % y
123 10 12 3
-123 10 -12 -3
123 -10 -12 3
-123 -10 12 -3

x < 0 のときの結果が,「(-x) \div y の結果の符号を反転させたもの」になっています.

これを避け, (-123) \div 10 = -13 \cdots 7 という割り算を行うには,代わりに x.div_euclid(y) / x.rem_euclid(y) と書きます.

use proconio::input;

fn main() {
    input! {
        x: i32,
    }
    let r = x.rem_euclid(10);
    assert!(0 <= r && r < 10);
    println!("あまりは {}", r);
}
x y x.div_euclid(y) x.rem_euclid(y)
123 10 12 3
-123 10 -13 7
123 -10 -12 3
-123 -10 13 7

今回は println! で余りを出力するだけでしたが,もしこの前後に膨大な処理が書かれていた場合,どこに異常があるのか特定するのが困難になります.アサートを使うことで,どこで異常が発生しているのか気付きやすくなります.

メッセージ

assert!(0 <= r && r < 10);

の代わりに

assert!(0 <= r && r < 10, "割った余りが想定の範囲を超えています");

と書くと,失敗したときの文面を

thread 'main' panicked at '割った余りが想定の範囲を超えています', src/main.rs:8:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

のように変えられます. println! マクロと同じようにプレースホルダー {} を置いて

assert!(0 <= r && r < 10, "割った余りが {} で,想定の範囲を超えています", r);

と書くこともできます.

assert_eq! / assert_ne! マクロ

assert_eq! マクロを使って assert_eq!(a, b) と書くと, assert! マクロで assert!(a == b) と書いたのと同じことになります.

use proconio::input;

fn main() {
    input! {
        x: i32,
        y: i32
    }
    let rounded = x / y * y;
    assert_eq!(rounded % y, 0); // rounded % y == 0 をチェック
}

同様に, assert!(a != b) の代わりに assert_ne! マクロを使って assert_ne!(a, b) と書くことができます.

assert_eq! / assert_ne! の場合も, assert_eq!(a, b, "メッセージ"); のようにして失敗時のメッセージを追加することができます.

パニックの起こる他の場面

例えば以下のようなソースコードを考えます.

use proconio::input;

fn main() {
    input! {
        x: i32,
    }
    let y = 100 / x;
    println!("{}", y);
}

与えられた整数 x に対し,100 / x を計算して出力しています.たとえばこのプログラムに入力として 4 を渡せば,25 と出力されます.

では,入力が 0 だったらどうでしょうか.0 で割ることはできないので,エラーとなります.

複雑なプログラムだと,0 割りが発生するかどうかコンパイル時に検出することはできないため,0 割りはコンパイルエラーにならず,その後のプログラムの実行時にパニックが起こります.標準エラー出力に 0 割りを知らせる旨のエラーメッセージが出て,続きを実行することなく終了します.

実行中に 0 で割る場面が登場してしまう可能性があるならば,それはプログラム自体のバグであり,書き方に問題があります.たとえば,上のプログラムにおいて入力が 0 となる可能性があるならば,ソースコードを以下のように修正するべきです.

use proconio::input;

fn main() {
    input! {
        x: i32,
    }
    if x != 0 {
        let y = 100 / x;
        println!("{}", y);
    } else {
        println!("0で割ることはできません");
    }
}

if を使って x が 0 の場合を除外しています.

このように,プログラム自体の問題によって起こるが,コンパイル時には検出できないようなエラーは,パニックとなります.