OSのエラーに対してパニックで対応

本連載ではHello Worldを取り上げた後は「数当てゲーム(の一部)」を読みながらRustの機能を調べている。これまでに読んできた数当てゲームのソースコードは次のとおり。

数当てゲーム(の一部)のソースコード guessing_game.rs

use std::io;

fn main() {
    println!("数当てゲーム");

    println!("どの数だとおもう? = ");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("読み込み失敗");

    println!("入力値: {}", guess);
}

このソースコードを通じてこれまでに「標準ライブラリ」「変数」「型の関数」「標準入力」「参照」に触れてきた。前回取り上げたソースコードは次の部分で、このソースコードで標準入力と参照を読んだ。

標準入力から入力を得るコード

    io::stdin()
        .read_line(&mut guess)
        .expect("読み込み失敗");

この3行のソースコードは興味深い。今回は3行目の次の処理を追っていく。

パニック処理

        .expect("読み込み失敗");

.expect()はパニック処理時に出力されるメッセージを設定するというものだ。Rustはオペレーティングシステムで発生したと見られるエラーに対してパニックを起こす。オペレーティングシステム側がエラーを起こしたと見られる場合は、Rust側はパニックを起こす。処理としては妥当だ。

例えば、この数当てゲームを「標準入力を閉じた」状態で実行してみると、次のスクリーンショットのようにmain関数内でパニックが発生する。

  • 標準入力を閉じて実行するとパニックが発生

    標準入力を閉じて実行するとパニックが発生

上記スクリーンショットの環境ではインタラクティブシェルとしてbashが使われているので、cargo runを実行する段階で「0<&-」を指定している。これは標準入力を閉じるためのリダイレクトだ。この指定を行うとそこから生成されるプロセスの標準入力はクローズ状態となる。標準入力から読み込みができないので、Rust側としてはパニックを引き起こすという流れになっている。

Result expect()でパニック処理

どのような流れでこの処理が行われるのかをソースコードを追っていこう。まず、前回読んだread_line()の処理を見てみよう。次のような実装になっている。read_line()の返り値はio::Resultだ。

read_line()の実装

impl Stdin {
    ...略...
    #[stable(feature = "rust1", since = "1.0.0")]     
    pub fn read_line(&self, buf: &mut String) -> io::Result<usize> {
        self.lock().read_line(buf)          
    }                        
}

read_line().expect()という処理は、つまりread_line()の返り値であるio::Resultのexpect()という関数をコールしているということになる。io::Resultの実装を調べると、libstd/io/error.rsに次の定義を見つけることができる。

io::Resultの定義部分

pub type Result<T> = result::Result<T, Error>;

read_line()で帰ってきているのは結局result::Resultという型ということになる。result::Resultの定義を追っていくと、libcore/result.rsの次のように定義されていることがわかる。

result::Resultの定義

pub enum Result<T, E> {
    /// Contains the success value
    #[stable(feature = "rust1", since = "1.0.0")]
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    #[stable(feature = "rust1", since = "1.0.0")]
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

つまり、OkとErrを含む列挙型がResultということだ。read_line()を実行した結果(Result)として、成功と失敗(OkとErr)を情報として持つことができる列挙型のデータが返ってきている、ということになる。

Resultの実装を調べていくと、次のようにexpect()という関数の定義を見つけることができる。

expect()関数の実装部分

impl<T, E: fmt::Debug> Result<T, E> {
    /// Returns the contained [`Ok`] value, consuming the `self` value.
    ///
    /// # Panics
    ///
    /// Panics if the value is an [`Err`], with a panic message including the
    /// passed message, and the content of the [`Err`].
    ///
    /// [`Ok`]: enum.Result.html#variant.Ok
    /// [`Err`]: enum.Result.html#variant.Err
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```{.should_panic}
    /// let x: Result<u32, &str> = Err("emergency failure");
    /// x.expect("Testing expect"); // panics with `Testing expect: emergency failure`
    /// ```
    #[inline]
    #[track_caller]
    #[stable(feature = "result_expect", since = "1.4.0")]
    pub fn expect(self, msg: &str) -> T {
        match self {
            Ok(t) => t,
            Err(e) => unwrap_failed(msg, &e),
        }
    }
    ...略...
}

ここからは参考程度となるが、expect()の実装は次のようにパニックの実装につながっている。

expect()から呼ばれるunwrap_failed()

// This is a separate function to reduce the code size of the methods
#[inline(never)]
#[cold]
#[track_caller]
fn unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
    panic!("{}: {:?}", msg, error)
}

unwrap_failed()で呼ばれているpanic!の基本実装であるpanic()の実装

/// The underlying implementation of libcore's `panic!` macro when no formatting is used.
#[cold]
// never inline unless panic_immediate_abort to avoid code
// bloat at the call sites as much as possible
#[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))]
#[track_caller]
#[lang = "panic"] // needed by codegen for panic on overflow and other `Assert` MIR terminators
pub fn panic(expr: &str) -> ! {
    if cfg!(feature = "panic_immediate_abort") {
        super::intrinsics::abort()
    }

    // Use Arguments::new_v1 instead of format_args!("{}", expr) to potentially
    // reduce size overhead. The format_args! macro uses str's Display trait to
    // write expr, which calls Formatter::pad, which must accommodate string
    // truncation and padding (even though none is used here). Using
    // Arguments::new_v1 may allow the compiler to omit Formatter::pad from the
    // output binary, saving up to a few kilobytes.
    panic_fmt(fmt::Arguments::new_v1(&[expr], &[]));
}

最後まで追っていくとRustのエラー処理の根幹部分へ到達していく。ここまで理解する必要はないが、このサンプルからは次の実装慣例を知ることができる。

  • 処理の結果としてResultを返す。
  • Resultはexpect()でパニック時のメッセージを指定することができる。

Resultを返すというのはRustではよく見られる実装だ。そしてexpect()にエラー発生時に出力させるエラーを書いておく。数当てゲームでは説明を簡素化するためにそのままパニックさせているが、パニックを回避する処理を記述することもできる。回避処理の書き方は、ザ・ブックのもうちょっと後の章で紹介されている。

オブジェクト指向的な書き方

こうした処理の記述方法はオブジェクト指向のプログラミング言語ではよく見られるものだ。返り値に対してそのまま関数のコールを記述することができる。Rustはオブジェクト指向のパラダイムも取り込んでいるので、こうした書き方ができる。

ザ・ブックで取り上げられている数当てゲームのソースコードは短いサンプルだが多くの機能を示してくれる。よくわからない場合にはこのように書くものだと思っておけばよいと思う。C++やJava、C#の経験があるならRustのソースコードを追っていってみるとよいと思う。より深く、より早く仕組みを理解できるようになる。

参考資料