数当てゲームを読み進む

前回は、Rustの数当てゲームのソースコード(途中版)を読みながら、標準ライブラリ、変数(不変、可変)、関数、型に関連付けられた関数などを見ていった。たった数行読んだだけだが、これだけでも多くの概念が使われていることがわかったと思う。

数当てゲームソースコード

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("読み込み失敗");

より正確には、次の処理が標準入力からのデータ読み込み処理になっている。

より正確にはこの部分

    io::stdin()
        .read_line(&mut guess)

「io::stdin()」という表記は、「標準ライブラリのioモジュールにあるstdio()関数を呼び出す」という意味になる。ファイルの先頭で「use std::io;」と書いてあるので、io::stdin()のように書くことができる。この指定がなければ「std::io::stdin()」と書く必要がある。

では、実際にstdio()関数がどういったものか、ソースコードを見てみよう。該当する関数はrust/src/libstd/io/stdio.rsで作成されている。該当部分を抜粋すると次のようになっている(本稿執筆時点)。stdin()関数を読むと、stdin()関数は現在のプロセスに結び付けられた標準入力を操作するためのハンドラを返す関数であることがわかる。

std::io::stdin()のソースコード

/// Constructs a new handle to the standard input of the current process.
///
/// Each handle returned is a reference to a shared global buffer whose access
/// is synchronized via a mutex. If you need more explicit control over
/// locking, see the [`Stdin::lock`] method.
///
/// [`Stdin::lock`]: struct.Stdin.html#method.lock
...略...
#[stable(feature = "rust1", since = "1.0.0")]
pub fn stdin() -> Stdin {
    static INSTANCE: Lazy<Mutex<BufReader<Maybe<StdinRaw>>>> = Lazy::new();        
    return Stdin {                                                                                   
        inner: unsafe { INSTANCE.get(stdin_init).expect("cannot access stdin during shutdown") },
    };

    fn stdin_init() -> Arc<Mutex<BufReader<Maybe<StdinRaw>>>> {
        // This must not reentrantly access `INSTANCE`                             
        let stdin = match stdin_raw() {           
            Ok(stdin) => Maybe::Real(stdin),
            _ => Maybe::Fake,
        };                                                                

        Arc::new(Mutex::new(BufReader::with_capacity(stdio::STDIN_BUF_SIZE, stdin)))
    }                                                                              
}
  • rust/src/libstd/io/stdio.rsに記載されているstdin()関数

    rust/src/libstd/io/stdio.rsに記載されているstdin()関数

io::stdin()を呼ぶと、Stdinが返ってくるようだ。Stdinも同じファイルで実装されており、該当部分を抜き出すと次のようになっていることがわかる。

Stdinの宣言部分

/// A handle to the standard input stream of a process.    
///    
/// Each handle is a shared reference to a global buffer of input data to this     
/// process. A handle can be `lock`'d to gain full access to [`BufRead`] methods    
/// (e.g., `.lines()`). Reads to this handle are otherwise locked with respect    
/// to other reads.                        
///    
/// This handle implements the `Read` trait, but beware that concurrent reads    
/// of `Stdin` must be executed with care.    
///                                                              
/// Created by the [`io::stdin`] method.    
///      
/// [`io::stdin`]: fn.stdin.html                                                                             
/// [`BufRead`]: trait.BufRead.html                                                    
///                                   
...略...
#[stable(feature = "rust1", since = "1.0.0")]    
pub struct Stdin {    
    inner: Arc<Mutex<BufReader<Maybe<StdinRaw>>>>,    
}

標準入力は現在のプロセスに結び付けられたものとして準備されており、標準入力から得られたデータを保持するグローバルなバッファに対する共有参照という形でStdinが実装されているようだ、といったこともこのソースコードからなんとなく見えてくる。

参照

次のソースコードで、stdin()によって得られたStdinに実装されているread_line()という関数が呼ばれていることになる。

より正確にはこの部分

    io::stdin()
        .read_line(&mut guess)

Stdinの実装も先程のファイルに記載されており、次のようになっている。

Stdinの実装部分

impl Stdin {
    ...略...
    pub fn lock(&self) -> StdinLock<'_> {
        StdinLock { inner: self.inner.lock().unwrap_or_else(|e| e.into_inner()) }
    }

    /// Locks this handle and reads a line of input, appending it to the specified buffer.
    ///
    /// For detailed semantics of this method, see the documentation on
    /// [`BufRead::read_line`].
    ///
    /// [`BufRead::read_line`]: trait.BufRead.html#method.read_line
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use std::io;
    ///
    /// let mut input = String::new();
    /// match io::stdin().read_line(&mut input) {
    ///     Ok(n) => {
    ///         println!("{} bytes read", n);
    ///         println!("{}", input);      
    ///     } 
    ///     Err(error) => println!("error: {}", error),
    /// }
    /// ```                                  
    ///                  
    /// You can run the example one of two ways:                           
    ///           
    /// - Pipe some text to it, e.g., `printf foo | path/to/executable`                          
    /// - Give it text interactively by running the executable directly,
    ///   in which case it will wait for the Enter key to be pressed before
    ///   continuing                                           
    #[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()関数の引き数が「&mut String」となっている点だ。数当てゲームでは「&mut guess」と記述されている。宣言した変数の名前はguessなのだが、指定されているのは&mut guessとなっており、変数名とは異なっている。

これは変数ではなく、参照が渡されていることを意味している。正確には「&guess」がguessの参照だ。しかし、ここで注意が必要だ。Rustでは変数がデフォルトで不変(immutable)だったように、参照もデフォルトで不変だ。一度作成したら変更することができない。read_line()では指定された参照が標準入力から得られたデータを指すようにしたいのだから、不変では都合が悪い。ここで「&mut guess」という表記になる。これは可変な参照を意味している。これでようやく参照がCやC++のような感覚で使えるようになる。

Rustの「参照」はRustの利点の1つと言われている。ポインタや参照は問題を引き起こす原因となりやすいが、Rustでは参照は簡単であり、かつ、安全であるとされている。C/C++で参照に慣れていると、Rustの参照はきついような印象を受けるが慣れればそんなことはなく、かえって便利に思えてくると思う。

Rustのソースコードを読みながら理解しよう

Rustはオープンソース・ソフトウェアとして開発されており、すべてのソースコードが閲覧できるようになっている。しかも、GitHub.comでホストされているので、Webブラウザがあれば検索することも閲覧することもできる。

  • GitHub.com Rust でソースコード検索

    GitHub.com Rust でソースコード検索

  • stdin()関数の実装部分

    stdin()関数の実装部分

ザ・ブックを読むだけで進めていってもよいが、ある程度プログラミング言語の経験がある方は、ザ・ブックを読みながらRustのソースコードを読んでいったほうが理解の進みが早くなると思う。新しいプログラミング言語の概念は、説明だけでは身に入ってこないところがある。実際に動かしてみたり、実際にその機能が実装されている部分を読んでみたりしたほうが理解が早い。

変数と参照については、後ほど詳しく取り上げる。明示的に参照を使っていないプログラミング言語しか使ったことがなくても大丈夫だ。ゆっくりと読み進めていこう。以下は、参考資料だ。