Rust研究：トレイトによる既存の型の拡張

Rust 初心者です。なんか色々発見があって楽しんでやってます。

Rust のトレイトを使って既存の型にメソッドを追加できるのでやってみました。

# お題

* std:: io::File と std:: io::BufReader に行単位で読みだすメソッドを追加したい。
* こんなイメージ

```rust
fn process(file:&str) -> Result<(), Box<dyn std::error::Error>> {
    let mut r = File::open(file)?;
    // もしくは let mut r = BufReader::new(File::open(file)?);

    // 空の行となるまで繰り返し表示
    loop {
        //print!(":");
        let line = r.read_line()?; // 一行読み出し
        if line.len() < 1 {
            break
        }
        println!("line = \"{}\"", line)
    }

    Ok(())
}

```

* また、行に含まれる、空白で区切られた文字列のリストを取得するメソッドを追加したい。
* こんなイメージ

```rust
fn process2(file:&str) -> Result<(), Box<dyn std::error::Error>> {
    let mut r = File::open(file)?;
    // もしくは let mut r = BufReader::new(File::open(file)?);

    // 空の行となるまで繰り返し表示
    loop {
        let items = r.read_items_in_line()?; // 一行分の文字列リストを取得
        if items.len() < 1 {
            break
        }
        println!("{} items {:?}", items.len(), items)
    }

    Ok(())
}
```

* 例えば入力ファイル test.txt が以下の内容のとき

```
Paint It, Black / The Rolling Stones 
    I see a red door 
        And I want it painted black
            No colors anymore
                Iwant them to turn black    
            I see the girls walk by
        Dressed in their summer clothes 
    I have to turn my head
Until my darkness goes
```

* process("test.txt") の実行結果は次のようにしたい

```
line = "Paint It, Black / The Rolling Stones "
line = "    I see a red door "
line = "        And I want it painted black"
line = "            No colors anymore"
line = "                Iwant them to turn black    "
line = "            I see the girls walk by"
line = "        Dressed in their summer clothes "
line = "    I have to turn my head"
line = "Until my darkness goes"
```

* process2("test.txt") の実行結果は次のようにしたい

```
7 items ["Paint", "It,", "Black", "/", "The", "Rolling", "Stones"]
5 items ["I", "see", "a", "red", "door"]
6 items ["And", "I", "want", "it", "painted", "black"]
3 items ["No", "colors", "anymore"]
5 items ["Iwant", "them", "to", "turn", "black"]
6 items ["I", "see", "the", "girls", "walk", "by"]
5 items ["Dressed", "in", "their", "summer", "clothes"]
6 items ["I", "have", "to", "turn", "my", "head"]
4 items ["Until", "my", "darkness", "goes"]
```

* なお、入力ファイルはテキストファイル形式として、文字コードは UTF8、改行コードは CRLF (0x0D + 0x0A) とする。

# 実装

行を意識した Read トレイトということで LineConsciousRead と名付け、以下のように実装してみました。

```rust
use std::io::{
    Read,
    BufReader,
};
use std::fs::{
    File,
};

// 使用する制御コード等
const CR:u8 = 0x0D; // Carriage Return
const LF:u8 = 0x0A; // Line Feed
const HT:u8 = 0x09; // Horizontal Tabulation
const SP:u8 = 0x20; // SPace

// LineConsciousRead トレイトを File と BufReader に適用
impl LineConsciousRead for File{}
impl<R:Read> LineConsciousRead for BufReader<R>{}

// 行を意識した Read トレイト
trait LineConsciousRead: Read {

    // 一行の文字列を読みだすデフォルトメソッド
    fn read_line (&mut self) -> Result<String, Box<dyn std::error::Error>> {

        // CRLF までのバイト列を取得
        let bytes = self.read_bytes_ends_with_crlf()?;
        
        // 文字列に変換
        let line = String::from_utf8(bytes)?;
        // [MEMO] 入力が画像データなどのバイナリの場合はここで FromUtf8Error になるはず

        Ok(line)
    }

    // CRLFまでのバイト列を読みだす、つまり一行分のバイト列を読みだすデフォルトメソッド
    // 返り値 Ok(bytes) の末尾には CRLF は含まれない
    fn read_bytes_ends_with_crlf (&mut self) -> std::io::Result<Vec<u8>> {
        let mut bytes:Vec<u8> = vec![];
        loop {
            // 1バイト読み出し
            if let Some(c) = self.read_1byte()? {

                // 読みだした 1 バイトが CR でも LF でもないときは保存しループを継続
                if c != CR && c != LF {
                    bytes.push(c);
                    continue
                }

                // CRだったときは次の 1 バイトを読み出し
                if c == CR {
                    if let Some(c) = self.read_1byte()? {
                        if c != LF {
                            // CR の次が LF ではなかったときはバイト列に保存しループを継続
                            // CR は捨てる
                            bytes.push(c);
                            continue
                        }
                    }    
                }
            }

            // 1 バイト目が読みだせなかったとき、1 バイト目が LF だったとき、CR の次のバイトが読みだせなかったとき、
            // CRLF が読みだせたとき、はループを抜ける
            break
        }

        Ok(bytes) // bytes には行末の CRLF は含まれない
    }

    // 1バイトを読みだすデフォルトメソッド
    // エラーではないが読みだせなかった時は Ok(None) が返ることに注意
    fn read_1byte (&mut self) -> std::io::Result<Option<u8>> {
        // 1バイトの領域
        let mut buf = [0u8; 1];
        // 1バイト読み出し
        let n = self.read(&mut buf)?;

        if n == 0 {
            // エラーはないが読みだせなかったときは None を返す
            Ok(None)
        } else {
            // 読みだせた1バイトの値を返す
            Ok(Some(buf[0]))
        }
    }

    // 一行に含まれる文字列のリストを読みだすデフォルトメソッド
    // 文字列の区切りはひとつ以上の空白による。先頭と末尾のひとつ以上の空白は無視される
    // 例： " a bc  def      ghij " => ["a", "bc", "def", "ghij"]
    fn read_items_in_line (&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    
        // 文字列のリストを保管する変数
        let mut result:Vec<String> = vec![];
        // 途中のバイト列を保管する変数
        let mut bytes:Vec<u8> = vec![];

        loop {
            if let Some(c) = self.read_1byte()? { // [1]

                if c == SP || c == HT {
                    // 読みだしたのが空白のときはそこまでのバイト列を文字列に変換してリストに保管
                    if bytes.len() > 0 {
                        result.push(String::from_utf8(bytes)?);
                        bytes = vec![];
                    }
                    continue

                } else if c == LF {
                    // 読みだしたのがLFのときはそこまでのバイト列を文字列に変換してリストに保管
                    if bytes.len() > 0 {
                        result.push(String::from_utf8(bytes)?);
                        bytes = vec![];
                    }

                } else if c == CR {
                    // 読みだしたのがCRのときはそこまでのバイト列を文字列リストに追加
                    if bytes.len() > 0 {
                        result.push(String::from_utf8(bytes)?);
                        bytes = vec![];
                    }

                    // 次の 1 バイトが LF かチェック
                    if let Some(c) = self.read_1byte()? { // [2]
                        if c != LF {
                            // CR の次が LF ではなかったときはバイト列に保存しループを継続。CR は捨てる
                            bytes.push(c);
                            continue
                        }
                    }

                } else {
                    // その他のときはバイト列に保存しループを継続
                    bytes.push(c);
                    continue
                }

            } // [1] の if 文の終わり

            // [1] の 1 バイト目が読みだせなかったとき、1 バイト目が LF のとき、CR LF が読みだせたとき、
            // CR の次の [2] で読みだせなかったとき、はループを抜ける
            break
        }

        // 保管しているバイト列があるときは文字列リストに追加
        if bytes.len() > 0 {
            result.push(String::from_utf8(bytes)?);
        }
        Ok(result)
    }

}
```

感想など

* Rust はオブジェクト指向ではないので継承ができないかわりに、トレイトを使うことで既存の型を直接的に機能拡張できるのですね。
* 既存の型にメソッドを追加するとはなんて危険なことを！と最初は思いましたが、作ったトレイトが有効なスコープでしか機能しないので副作用はないという。
* 慣れるまでは少し気持ち悪いですが、これはこれで便利だと思いました。