私的即席プラクティスパターン (※古い内容です)

szktty

SUZUKI Tetsuya

Posted on January 23, 2020

私的即席プラクティスパターン (※古い内容です)

※この記事は 2015年12月25日 に Qiita に投稿したものです。

※ Rust 1.4.0 時の内容でありかなり古く、 Rust 的に褒められた作法ではありません。消そうか残そうか迷いましたが、いまだそこそこ閲覧があり、こういう形で学習した人間もいるということで残しておきます。ご了承ください。


Rust のコードを書いていて、個人的に躓きがちだった点をまとめてみました。あくまで私的な意見で、 Rust の公式的な作法ではありません。

想定する Rust のバージョンは 1.4.0 stable です。 1.5.0 でも特に問題ないとは思いますがわかりません。また、 unstable な API には触れません。

方針

これらの私的なパターンもしくは Tips の方針は、 「コードが汚かろうが無駄な処理が多かろうが、動けば正義」 です。

Rust はリソース管理の複雑さと強い型付けによってコンパイルエラーが出る機会が多く、慣れないうちは修正方法すらわかりません。始めからすべての仕様を把握しようとすると、コンパイルエラーの修正でいっぱいいっぱいになって、トライ&エラーどころじゃなくなると思います(ただ、エラーメッセージはかなり親切です)。そこで、まずはコンパイルエラーを最小限にして少しずつ慣れていこう、というスタンスです。そういう私も、きっと闇の軍団にコードレビューされたら葬られるようなコードを書いてると思います。

最初から礼儀正しいコードを書きたい人にとっては害悪なだけかもしれません。まあ、それはそれで清く正しくコンパイルの闇に飲まれてみるのもありかなと思います。

基本的な型

&str と String

文字列を表す型は二つあります。 str と String です。 str がプリミティブな型で、 String は Rust 自身で実装されている型です。 どちらの型も相互変換が可能です。

文字列型が二つもあるなんて面倒だなあと思いますが、積極的に使うのは String です。 str の方は、基本的に &str という借用の形で使います。

プリミティブな型である str は、普通使わないし使えないと考えていいと思います。 Rust のコンパイラは、コンパイル時にサイズが決定できない型を変数の型に指定するとエラーにします。で、 str もコンパイル時にサイズを決定できない型です。この意味がまだわからなくても大丈夫です。とにかく、次のような変数は定義できません。

let s: str;

しかし、 &str という借用の型であればコンパイル時にサイズを決定でき、変数の型として使えます。とにかくそういうものなのです。

それで &str がどこで使われるのかと言うと、一つは文字列リテラルです。文字列リテラルの型は &'static str で、 'static は生存期間を示しますが、それはさておき &str です。これ以外で str を生成する機会はまずないと考えていいと思います。文字列を生成したり編集したりする場合は String を使います。

String は、動的に生成できる可変の UTF-8 の文字列です。この文字列は動的にサイズを伸縮でき、内容を変更できます。コンパイル時にサイズを決定できるため、変数の型に指定できます。とりあえずの理解は「 String を使え」でいいと思います。

で、自分から文字列を生成・操作するなら String でいいんですが、 &str は引数や戻り値にたびたび登場し、 String への変換を行う必要も出てきます。また、文字列リテラルを String 型の変数にセットしたい場合も、 &str から String の変換が必要です。

&str から String の変換は to_string() メソッドで可能です( API に ToString トレイト実装済みと書かれていれば使えます)。人のコードでやたらと見かける to_string() は、それだけ &str が引数や戻り値として使われていることを示しています。

let s: String = "hello".to_string()

--

ただ、 to_string() はゼロコストというわけにはいきません。具体的には、フォーマット化によって文字列化するために、メモリーアロケーション+フォーマット化のコストがかかります。 &str に限って言えば、 to_owned() を使えばメモリーアロケーションのみで済むので、余裕があれば頭の隅にでも置いておくといいと思います。

--

「 String を使え」と言っておきながら &str が頻出するのですが、 &str を使うのには理由があります。ただ、慣れないうちは &str と String の関係を理解できるまで String で済ませれておけばいいんじゃないかと思います。とにかく &str を受け取る場面では to_string で String に変換することにして、 &str と String について理解する労力を他に回せばいいと思うのですよ。

で、次の問題は、引数に &str を受け取る関数はあるけど String を受け取る関数がない状況です。例えば String がいきなりそれです。文字列を追加するメソッドで、 &str を受け取る push_str() はありますが、 String を受け取って欲しい push_string() はありません。「オメー何で自分を使わねえんだよ」と当初思いました。

そういう場合は String を &str に変換すればいい、とだいぶ後になって理解しましたが、その変換方法がまたトリッキーで、「 String の借用 = &str 」の関係が成り立ちます。つまり、 String の型の変数の先頭に & をつければ &str の型の引数に渡せます。

fn print_str(s: &str) {
    println!("str = {}", s)
}

fn main() {
    print_str("hello");
    let world: String = "world".to_string();
    print_str(&world);
}

まとめます。相互変換は次の方法で行います。

  • &str -> String の変換は to_string() で行う
  • String -> &str の変換は & をつける

&str と String の使い分けの方針は、

  • 引数として受け取る文字列は &str
  • 構造体のフィールドとする文字列は String
  • 戻り値にする文字列は String

と大雑把に捉えておいていいのではないかと思います。混乱しそうであれば、すべて String でもいいのではないかと思います。

&'static str

文字列リテラルの型です。 'static は特殊な生存期間で、静的にメモリが確保される文字列です。プログラムの実行中はずっと生存します。

Vec<T>

リストです。 T にはリストの要素の型が入ります。

Vec を使うのに何らかのモジュールをインポートする必要はありません。

HashMap<K, V>

キーがハッシュ値であるマップです。 K がキーの型で、 V が値の型です。HashMap を使うには、 std::collections::HashMap モジュールをインポートする必要があります。

ハッシュ値を算出できなければならないため、キーの型は Eq トレイトと Hash トレイトを実装する必要があります。これは型の定義時に #[derive(PartialEq, Eq, Hash)] を指定すれば簡単です。 Hash トレイトを適用できない型が含まれる場合は自前で実装しなければなりませんが、慣れないうちはハードルが高いかもしれません。その場合はハッシュ値を算出できる他の型をキーにすることを検討してはどうでしょうか。

HashSet<K, V>

要素が重複しない集合です。重複の検査にハッシュ値を使うため、 HashMap と同様に Eq トレイトと Hash トレイトを実装する必要があります。 HashSet を使うには、 std::collections:: HashSet モジュールをインポートする必要があります。

Option<T>

「あるのかないのか」を表す型です。何に使うんだ?と言うと、他の言語で「値がない」ことを nil や null で表していた処理を None と Some(T) に置き換えればいいと思います。

Result<T, E>

処理の成否を表す型です。成功時の値の型が T 、失敗時の値の型が E です。 Rust には捕捉可能な例外処理機構はなく、エラーの可能性を示したいときはこの型を使います( panic! マクロがエラーメッセージの表示に使えますが、表示後にプログラムが終了します)。積極的に使っていくべきです(「あ、この処理は引数の値次第でエラーにすべきだな」と思ったときが使いどころです)。 try! マクロと組み合わせて使うと便利です。

ちなみに、同名の型が std::io モジュールにもあります。こちらの Result は、入出力に関するエラー io::Error をエラー値に持つ Result として定義されています。簡単に言うと、 io モジュールの関数の戻り値に毎度 Result<T, io::Error> と書くのが面倒になった(のと、エラー値の型を隠蔽したい)ので、一つの型として定義しておいた、と考えていいと思います。

type Result<T> = Result<T, std::io::Error>;

Box<T>

任意のデータ型 T の値をラップする型です。一体何の役に立つのかと言うと、再帰的な構造体や列挙体の定義に使います。こんな感じです。

#[derive(Clone)]
struct Employee {
    name: String,
    boss: Box<Option<Employee>>
}

このオブジェクトを生成する例:

Employee { name: "John".to_string(), boss: Box::new(None) }

この構造体では Option を Box でラップしていますが、ラップをしないと生成できなくなってしまうからですね。 None の位置に何を入れるか考えてみるとわかると思います。

Box でラップされた中身を取り出すには、式の前に * をつけます。

let jack = Employee { name: "Jack".to_string(), boss: Box::new(None) };
let john = Employee { name: "John".to_string(), boss: Box::new(Some(jack)) };
match *john.boss {
    None => println!("boss is none"),
    Some(ref boss) => println!("boss is {}", boss.name)
}

よく使うマクロ

print!, println!

標準出力にフォーマットしたテキストを出力します。とりあえず覚えておくといいフォーマットは "{}" です。こんな感じで使います。当面は "{}" だけ覚えてればなんとかなります。

let p = "hello";
println!("data = {}", p)

ただし、これで出力できる値には制限があり、任意のすべての値を出力できるわけではありません。コンパイル時に Display がどうのというエラーが出た場合は、 String に変換するメソッドを実装するなどして、 String に変換した値をフォーマットに指定すれば大丈夫です。

format!

引数は print! と同じですが、こちらはフォーマットしたテキストを String で返します。他の言語で言う sprintf です。文字列の結合代わりに使うのも便利です。

write!, writeln!

これも print! と同じくテキストをフォーマットします。ただし print! と違って出力先はバッファです。具体的には std::io::Write トレイトを実装したオブジェクトです。わかりやすい例は、文字列バッファである String です。 Vec も対応しています。

use std::fmt::Write;

fn main() {
    let mut buf = String::new();
    write!(buf, "hello, ");
    write!(buf, "world!");
    println!("formatted: {}", buf);
}

なお、 write! は戻り値が Result<usize> です。上記のコードのように戻り値を処理しない場合は、コンパイラの設定次第で警告が出ます。

panic!

回復不能なエラーを発生させます。このエラーは捕捉できず、プログラムはエラーメッセージを表示して終了します。エラーメッセージは、 print! と同様のフォーマットで指定します。

panic!("error")

unimplemented!

"not yet implemented" というメッセージを表示する panic! です。未実装の処理を後回しにしたいときに使ってもいいし、使わなくてもです。使う場合は、引数なしでこのように呼びます。

unimplemented!()

unreachable!

"internal error: entered unreachable code" という、到達不可能を示すメッセージを表示する panic! です。到達不可能のはずの箇所で使ってもいいし、使わなくてもいいです。

このマクロの引数は print!/format! と同じか、 unimplemented! と同様に引数なしでも構いません。フォーマットを与えた場合は、上記のメッセージに続けて表示されます。

// thread '<main>' panicked at 'internal error: entered unreachable code: test', <anon>:3
unreachable!("{}", "test")

try!

Result 型の値に対して使うマクロです。値が Err であれば、それを戻り値として関数から脱出し、 Ok であれば、 Ok が保持する値を返します。何やら複雑そうですが、 Result がエラーを示したら関数を終了したい、そうでなければ処理を続けたい、という状況は間々あります。その度にいちいち match で

let value = match exp {
   Err(e) => return Err(e),
   Ok(v) => v
};

と書くのは面倒です。 try! を使うと、上のコードはこう書けます。

let value = try!(exp);

このマクロのポイントは return で、関数では「現在の関数から脱出する処理」を実装できません。 Result と併用すると、エラー処理を書きやすくなるでしょう。

ちなみに、 Option に対して同等の処理を行うマクロはないようです。もちろん自分で定義するのは自由ですが、あったらあったで、意外と使う機会があるようでないんじゃないかと思います。参考までに、 None であれば戻り値を None として関数を脱出するマクロの実装例を挙げておきます。

macro_rules! expect {
    ($e:expr) => ({
        use std::option::Option::{Some, None};

        match $e {
            Some(e) => e,
            None => return None,
        }
    })
}

fn test() -> Option<String> {
    let opt1: Option<String> = Some("hello".to_string());
    let opt2: Option<String> = None;
    let s = expect!(opt1);
    println!("opt1 = {}", s);

    let s = expect!(opt2);
    // 実行されない
    println!("opt2 = {}", s);

    None
}

fn main() {
    let _ = test();
}

vec!

任意の数の式の並びから Vec オブジェクトを生成するマクロです。文章だとわかりにくいですが、要はこういう使い方をします。

// 要素が 1, 2 , 3 である Vec オブジェクトを生成する
let x: Vec<u32> = vec![1, 2, 3];

このマクロの引数は中括弧 [..] で囲むので注意です。

assert!, assert_eq!

アサーションです。引数の値が false であれば panic を発生させます。

debug_assert!, debug_assert_eq!

こちらもアサーションですが、コンパイルオプションに -C debug-assertions を指定したときにのみ有効になります。

コーディングスタイル

一度に 5 行以上書かない

Rust は他の言語と比べると、一度に(コンパイルを挟まずに)長いコードを書きにくいと思います。型推論のある言語ではそうなりがちだと思いますが、 Rust はそれに加えて所有権のエラーがつきものです。うっかりすると修正するうちに実装の目的を忘れてしまう量のエラーが出るので、少しずつコンパイル&修正を繰り返しながら進めるといいと思います。

コンパイル時のエラーメッセージはよく読む

Rust でのコンパイル時のエラーメッセージは長いです。エラーの原因に加えて、その他の情報もたくさん表示されます。しかし、わかりにくいように見えて、 help から始まるメッセージはかなり親切だったりします。

特に生存期間に関するエラーでは修正例のコードが表示されることがあるので、詰まったらエラーメッセージをよく読むべきです。エラーメッセージや修正例のコードを読んでもその意図がわからなければ、借用を使わずに済む方法を考えた方がいいでしょう。

可変なコレクションには mut を指定する

プラクティスというか、単に必須事項なんですけど。コレクションにオブジェクトを追加するなどの破壊的な操作を行うコードがエラー扱いされた場合、変数定義で mut を付け忘れるケアレスミスが(私は)よくあります。「なんでこれがエラーなんだ?」と思ったら、まず変数定義を確認するといいと思います。

エラーハンドリングは Result 型で

ここに、従業員を表す次の構造体があるとします。とりあえずは従業員の名前のフィールドのみ用意するとします。

#[derive(Clone)]
struct Employee {
    name: String
}

Employee オブジェクトを生成するとき、 Employee { name: .. } と直に生成してもいいですが、コンストラクタ関数を実装するとします。シンプルに考えると、関数の型はこうでしょうか。

fn new(name: &str) -> Employee

どうせなら入力する名前をチェックしたいですね。空の名前を入力されても困りますから、受け付けられない名前であればエラーとします。

Result 型を使うタイミングはここです。名前が問題なければ Employee オブジェクトを返し、問題ありならその原因の旨を示すメッセージを返すとします。このエラー処理を捕捉不可能な panic! でやると、プログラムが終了してしまうので注意です。

Result 型を使うと、コンストラクタ関数はこのように実装できます:

impl Employee {
    fn new(name: &str) -> Result<Employee, String> {
        if name.is_empty() {
            Err("name must not be empty".to_string())
        } else {
            Ok(Employee { name: name.to_string() })
        }
    }
}

これなら、 Employee オブジェクト生成時のエラーを安全に処理できます。型宣言からもエラーが発生する可能性があるとわかるので、積極的に使いましょう。 try! マクロについても知っておくといいです。(具体例が思いつかなかった)

最初のうちはエラー値の詳細は置いといて、 Result<T, ()> などと unit 型にしておけばいいと思います。 Option<T> 型で代用したくなることもあるかもしれませんが、 Option 型は目的が異なるので避けましょう。

整数や文字列に特別な意味を持たせない

例えば、どの言語にも数値やオブジェクトの比較を行う関数が用意されています。比較の結果は概ね「等しい、小さい、大きい」のいずれかで、これらを表すのに "0, -1, 1" の整数を割り当てる言語があります。もしくは、それらの整数を定数として定義しているかと思います。 Rust ではこのような整数の使い方、整数に特別な意味を持たせる使い方は避けるべきです。

Rust では列挙体で値の意味を表します。なんだか抽象的な説明ですが、前述の比較の結果は、 Rust ではこう定義されています。

pub enum Ordering {
    Less,
    Equal,
    Greater,
}

列挙体を使うメリットは型検査です。これを整数で表してしまうと、定義外の数値を渡しても型検査が通ってしまいます。

また、構造体と同様に、列挙体にもメソッドを定義できます。例えば Ordering に定義されている reverse() メソッドは、比較の結果を逆転させます。昇順と降順を切り替えるのに使えます。

bool に特別な意味を持たせない(二択でも列挙体を定義する)

前節の続きです。 bool にも特別な意味を持たせないようにすべきです。例えば、扱う値が整数か浮動小数点数のどちらかだとしたら、「 true なら整数、 false なら浮動小数点数」という二択に bool を割り当てるべきではありません。代わりに列挙体でこう定義するといいです。

enum ValueType {
    Int,
    Float
}

unwrap() の落とし穴

Option 型と Result 型には、一部の列挙体が持つ値( Some または Ok )を取得できる unwrap() メソッドがあります。このメソッドは列挙体が None や Err だと panic を発生させますが、その場合はバックトレースの最後尾が unwrap の実装元になります( lib/std/option.rs とかそんなの)。 unwrap() を呼び出した位置ではないので気を付けたいです。

バックトレースを有効にしても、 unwrap() を呼び出した行番号まではわからないようです。どうしても unwrap() の呼び出し位置を確認したいなら、 unwrap() 相当の処理をマクロで実装するといいかもしれません。こんな感じでしょうか。

// Option.unwrap()
macro_rules! unwrap_opt {
    ($x:expr) => {
        $x.unwrap_or_else(|| panic!("unwrap failed"))
    }   
}

// Result.unwrap()
macro_rules! unwrap_result {
    ($x:expr) => {
        $x.unwrap_or_else(|_| panic!("unwrap failed"))
    }   
}

レコードのパターンマッチではフィールド値の変数名を省略する

レコード型(構造体、列挙体)やタプルのパターンマッチでは、フィールド値を代入する変数の記述を省略できます。楽ですので省略しましょう。

struct Person {
    name: String
}

fn main() {
    let me = Person { name: "me".to_string() };
    match me {
        // { ref name: name } と書かなくてもよい
        Person { ref name } => println!("name = {}", name)
    }
}

クロージャーに関する所有権でつまずいたら move を指定してみる

クロージャー内の所有権の扱いはいくらか複雑です。所有権に関するコンパイルエラーが出るのに原因も修正方法もわからないときは、クロージャー式の前に move キーワードをつけてみるとコンパイルにパスするかもしれません。

理屈もわからないのにやってみろというのも適当ですが、先に理屈を理解するには Rust は難しいのではないかと思います。どうしてもクロージャーを使ったコードがコンパイルできないのであれば、不格好でもクロージャーを使わないコードに書き換えてはどうでしょうか。

また、クロージャーを引数に取る関数を実装しようとすると、関数の型定義がいくらか複雑になります。慣れないうちは、安易にクロージャーを多用しない方がいいのではないでしょうか。

再代入可能なグローバル変数を使わない

実行中に再代入可能なグローバル変数( static mut で定義する可変の静的変数)の内容を書き換えるコード、誰もが(?)つい一度は試してしまうと思います。できることはできるのですが、本当にこの方法でなければできない処理でもなければ、可変のグローバル変数の使用は避けましょう。

例えば、実行中に書き換える目的で、 Option 型のグローバル変数を定義するとします。次のコードは問題なくコンパイルできます。

static mut COMMAND: Option<&'static str> = Some("default");

fn main() {
    // static mut の変数へのアクセスは unsafe で囲む
    unsafe {
        COMMAND = Some("mycommand");
        println!("command = {}", COMMAND.unwrap())
    }
}

これがコンパイルをパスするのは、 Option 型の内容が即値(文字列定数)だからです。これを動的に生成される String にするとコンパイルできません。

// None にしても同様
static mut COMMAND: Option<String> = Some("default".to_string());

エラー:

<anon>:3:38: 3:65 error: mutable statics are not allowed to have destructors [E0397]
<anon>:3 static mut COMMAND: Option<String> = Some("default".to_string());
                                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~
<anon>:3:38: 3:65 help: see the detailed explanation for E0397
<anon>:3:43: 3:64 error: method calls in statics are limited to constant inherent methods [E0378]
<anon>:3 static mut COMMAND: Option<String> = Some("default".to_string());

ですので、可変なグローバル変数を有効利用できる状況は非常に限られます。 unsafe の指示が必要な処理はバグの原因にもなりますから、他の方法を検討しましょう。

unsafe は諸刃の剣

生ポインタやスレッドセーフなどの安全ではない処理は、通常コンパイルエラーになります。どうしてもそういった処理を行いたければ、 unsafe ブロック内に記述するとコンパイルをパスできます。ただし、より柔軟な処理ができる一方で、当然安全じゃありませんからバグの原因にもなります。「 unsafe を使わざるを得ない」と思ったら、一旦考え直しましょう。

実行速度を過信しない

ネイティブコードにコンパイルする言語だからと言って、 Rust で書けば何でも速くなるということはないです。当然、サードパーティーのライブラリが速いとも限りません。わざわざ言われなくてもわかってる、と言われそうですが念のため。

私が書いてみた範囲では、サイズの大きいオブジェクトを含む Box のコピーで遅くなっていた場合がありました。とは言え必ずしも Box がネックになるとは限りませんし、今後の開発で変わるかもしれません。

所有権と生存期間

不要なクローンをしない

ここで言う不要なクローンとは、 clone() メソッドを呼び出さなくてもコンパイルエラーにならない処理です。

所有権のエラーに嫌気が差して、とりあえずクローンしとけという考えはお勧めしません。別にクローンのコストを削れなどの実用上の理由ではなくて(もちろんオブジェクトの内容如何でコストが高い場合もあります)、クローンの意味を理解する機会を奪ってしまうからです。エラーが出たら、エラー箇所の周辺に clone() を入れればだいたいは解決できると思うので、そこは一つ我慢して、エラー&修正の経験値を積むといいと思います。

明確な理由を説明できないうちは借用を使わない(例外もある)

Rust と言えば所有権、 Rust と言えば生存期間です。所有権を奪いたくなければ借用を使うことになり、借用を使うなら生存期間を管理する必要があります(生存期間は、コンパイラが推論できる範囲で省略できます)。所有権、借用、生存期間は扱いが複雑なので、ドキュメントやブログなどで詳しく取り上げられたりしてますが、残念ながら学習コストは低くありません。

ここからは個人的な意見ですが(この記事すべてがそうですが)、 Rust に慣れるまでは、これらの理解を棚上げにしてもいいと思います。特に構造体や列挙体のフィールドに借用を持たせるとなると、生存期間の理解が必要です。借用を使わなければ絶対に実装できないプログラムはそうそうあるものでもないと思います。なぜあなたのコードで借用を使わなければならないのか、その理由を明確に他人に説明できないようでしたら、ひとまず借用の使用は避けておくといいと思います。

しかし、それでも借用を使うべきであろう場面もありますので、これに続く節も合わせて参考にしてください。

少々古いドキュメントですが、所有権についてこう触れられています。

If you have good reason. It's not polite to hold on to ownership you don't need, and it can make your lifetimes more complex.

不要に所有権を持つと生存期間の管理が複雑になるからやめとけ、という忠告です。イディオム的でもないよともあります。確かに単純な関数の引数くらいなら、引数で借用すれば所有権を気にする手間が減ると思いますけど、私なんかは Rust に慣れないうちは手間がかかっても所有権の移動のみでなんとかやる方が楽なんじゃないかと思います。

むしろ、安易に借用を使わない=不変のオブジェクトを次々と生成しながら処理するコードを書けるスキルの方が、後々重要になるのではないか、とすら思います( C++ の人たちに怒られるかな?)。借用を使わなければ、所有権絡みのエラーは clone() を闇雲に挟めばなんとかなります。数打ちゃ当たります。打ちまくってるうちに、エラー原因となるコードの勘所もつかめると思います。

ただ、 Box や標準ライブラリを使う上では借用を気にしなければならない機会も多いのですが、そこは & や * を適当に付けて、気合いで乗り切って頂きたいと思います。

所有権を渡す(=借用の使用を避ける)ことのデメリット

上の節の主旨は「わかんなきゃ借用は使わなくても(所有権を渡しても)いいんじゃない?」なんですが、デメリットもあるんだと押さえておくといいと思います。

デメリットの一つはコストです。ある値を複数箇所で使うときにいちいち所有権を渡すとなると、その数だけ新しい値を生成するかコピー(クローンもコピー手段の一つ)するかして、新しい所有権を用意する必要があります。当然ながら、生成もコピーもコストがかかります。

もう一つは、コピーできない値が扱いにくくなることです。他の節で「所有権に関するエラーが出たらクローンしとけばなんとかなる」などと言いましたが、逆に言えば、クローンまたはコピーができなければどうにもならない可能性があります。クローンできない( Clone トレイトを実装していない)値も多々あります。

例えば、 File オブジェクトがそうです。ここに、引数に File オブジェクトを所有権ごと受け取る関数があるとします(内容は適当です)。

fn read_all(mut f: File) -> String {
    let mut buf = String::new();
    f.read_to_string(&mut buf);
    buf
}

この関数を呼び出すときは、引数に渡す File オブジェクトの所有権を渡さなければならず、呼び出し後はファイル操作ができなくなります。引数にクローンした値を渡そうにも、 File オブジェクトはクローンできません。

use std::fs::File;
use std::io::{Read, Seek, SeekFrom};

fn read_all(mut f: File) -> String {
    let mut buf = String::new();
    f.read_to_string(&mut buf);
    buf
}

fn main() {
    let mut f = File::open("foo.txt").unwrap();
    let s = read_all(f);

    // error: use of moved value: `f`
    f.seek(SeekFrom::Start(0));
    ...
}

まあこの程度であれば、次のように変更するなどして File の所有権を返せば処理を続けられます。

fn read_all(mut f: File) -> (File, String) {
    let mut buf = String::new();
    f.read_to_string(&mut buf);
    (f, buf)
}

fn main() {
    let mut f = File::open("foo.txt").unwrap();
    let (mut f, s) = read_all(f);
    f.seek(SeekFrom::Start(0));
}

ですが、万事そういうわけにもいかないでしょう。また、あらゆる場面で所有権の授受をしていてはコードが複雑に入り組んでしまいます。 Rust に慣れてきたら、まずは引数から借用を取り入れてみるといいかもしれません。構造体のフィールドに借用を含める場合と違い、だいたいは生存期間を気にする必要はない(省略可能)はずです。

ちなみに借用を使うと、上のコードはこう書けます。

use std::fs::File;
use std::io::{Read, Seek, SeekFrom};

fn read_all(f: &mut File) ->  String {
    let mut buf = String::new();
    f.read_to_string(&mut buf);
    buf
}

fn main() {
    let mut f = File::open("foo.txt").unwrap();
    let s = read_all(&mut f);
    f.seek(SeekFrom::Start(0));
}

戻り値を使わない式をエラーにする (unused result which must be used)

セミコロンで区切られた式の値が unit 以外のとき、その値に対して何も操作をしなければ、警告 unused_must_use が表示されます(またはエラー)。例えば次のコードです。

// エラーにする
#![deny(unused_must_use)]

use std::fmt::Write;

fn main() {
    let mut buf = String::new();
    write!(buf, "abc");
    ()
}
<std macros>:2:1: 2:54 error: unused result which must be used
<std macros>:2 $ dst . write_fmt ( format_args ! ( $ ( $ arg ) * ) ) )
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<anon>:7:5: 7:24 note: in this expansion of write! (defined in <std macros>)
<anon>:1:9: 1:24 note: lint level defined here
<anon>:1 #![deny(unused_must_use)]

このコードのエラー箇所は write!(buf, "abc"); です。セミコロンを終端とする式の値は unit であるべきですが、 write! マクロの戻り値は Result<(), std::fmt::Error> です。 Rust では式の値が操作(代入も含む)されないと、 "unused result which must be used" というメッセージで警告されます。値を無視すればバグにつながる可能性があります。

次の節に続きます。

戻り値を無視するには

write! のような連続して使うマクロや関数では、すべての呼び出しで戻り値を処理するのが面倒になることもあります。エラー時に落ちたり見逃したりしてもよければ、戻り値を無視する方法がいくつかあります。

  1. _ で始まる変数に値を代入する( _ を変数名の先頭に付けると、コードブロック内で使わなくても警告されません)
  2. #[allow(unused_must_use)] コンパイラ属性を指定する
  3. 戻り値を無視するマクロを定義する

手っ取り早いのは let _ = .. でアンダースコア付きの変数に代入する方法ですが、私は同等のマクロを定義して使っています。これだと _ への代入よりも無視の意思が明白です。

use std::fmt::Write;

macro_rules! ignore {
    ($x:expr) => {
        {
            let _ = $x;
            () // unit を返せばよい
        }
    }   
}

fn main() {
    let mut buf = String::new();
    ignore!(writeln!(buf, "無視してんのよ"));
}

もちろん、無視しないに越したことはないです。

総称型の型パラメーター名に意味はない

Vec<T> とか Result<T, E> などの T とか E の名前に意味はありません。慣習的に一文字で表しているだけで、 Type や Error などの頭文字を使うことが多いです。

自分で定義する構造体の型パラメーターの数が多くてわかりにくいようであれば、関連型の使用を検討するといいかもしれません。関連型は難しそうに見えても、単に書き方を変えただけです。

型推論に頼り過ぎない:時には型を宣言する

タプルやコレクションの変数を定義するとき、以降の処理が複雑になりそうであれば、先にコレクションの型を宣言しておくとあとで楽です。

こんな感じ:

let names: Vec<String> = Vec::new();

型推論のエラーは、自分の推測と離れた位置で発生する可能性があります。複雑な型になるとそうなりがちで、間違った型を基準に他の部分も推論が進むと、その結果思い掛けない位置が型エラーとして表示される場合があります。残念ながら、間違いの正確な位置を特定する決定的な方法はありません。

そこで、あらかじめ変数の型を宣言しておけば、エラー箇所の範囲を狭めやすくなります。それに実装中は型が頭の中に入っていても、席を立って三歩も歩けば忘れます。すべての変数に型を宣言していては面倒過ぎますが、エラー修正に手間取る場合は型を宣言しながらデバッグすると楽になります。特にコレクションの要素の型エラーは追いにくかったので、余程単純な処理でなければ型を宣言しておくといいと思います。

Fn, FnMut, FnOnce 型はクロージャーを示す

API リファレンスに登場する Fn* 型は、クロージャーを示す型です。例えば Fn(usize) -> bool なら、符号なし整数を引数に受け取って真偽値を返すクロージャーを示します。

Fn, FnMut, FnOnce はどれもクロージャーなんですが、それぞれ性質が異なります。大抵は FnMut が使われるので、当初はそれだけ覚えておけばいいと思います。

  • Fn: 外側のスコープの変数の内容を変更できません。
  • FnMut: 外側のスコープの変数の内容を変更できます。
  • FnOnce: このクロージャーは一度しか実行されません。変数の内容は変更できません。

所有権に慣れるまではクロージャーを使わない

特にクロージャーから外側のスコープの変数を扱う場合、所有権の管理がかなり面倒になります。所有権に慣れるまでは、クロージャーの使用はごく簡単なケースに留めておくのがいいと思います。

構造体と列挙体

Clone トレイトを自動導出しておく

構造体(列挙体)を定義するときは、同時に #[derive(Clone)] を記述して Clone トレイトを自動導出( clone() メソッドを自動的に追加する)しておくといいでしょう。クローンできて困ることはないと思います。フィールドの内容によっては自動導出できない場合もありますが、そのときはがんばって Clone トレイトを実装したりせずに、ひとまず置いて先に進むといいと思います。 Clone トレイトを自動導出できない構造体には、クローンできてしまうとメモリ安全を確保できなくなってしまうフィールドが含まれている可能性が高いと思います。

#[derive(Clone)]
struct Foo {
    ...
}

再帰的な構造体や列挙体を定義したい

自身の型をフィールドに持つ再帰的な構造体や列挙体の定義は、ただ同じ型を指定するだけではできません( 1.5.0 現在)。例えばこんな感じの構造体です。

struct Person {
    parent: Option<Person>,
    name: String
}

理屈はともかくとして、これは再帰的に扱う型を Box で囲めば定義できます。 Box を使うので、アクセスの手間は増えます。アクセス時は * を適当につけて乗り切ってください。コレクション型など Box を省略できる型もあり、都度調べてください。

struct Person {
    parent: Option<Box<Person>>,
    name: String
}

ただし、再帰的な構造体、列挙体を定義する前に、本当に必要かどうかを検討すべきだと思います。つい再帰的な構造体を定義したくなりますが、クローン時にやたらとコピーが発生する可能性があって、場合によってはボトルネックになりかねないかもしれません。とは言え最初からパフォーマンスを考えるのも徒労に終わるでしょうから、まずはやってみるといいと思います。列挙体ではわりと必要になると思いますし。

それなら借用を使ったらどうかと考えると思いますが、型定義と生存期間の管理が途端に複雑になります。このようにすれば一応できなくもないです。

struct Person<'a> {
    parent: Option<&'a Person<'a>>,
    name: String
}

大抵のケースでは再帰的な借用の管理に悩むだけ時間の無駄だと思いますので、再帰的な構造や借用をできるだけ使わない方法も考えてみるといいと思います。

ライブラリ

sprintf はどこにある?

format! マクロがそれです。

文字列バッファはいずこ

String がそれです。 String は可変の文字列です。

文字列を結合する

format!

一つの方法は format! です。文字列にしろ数値にしろ大抵のデータを文字列化できますから、とりあえずはこれで済ませておけば楽でしょう。フォーマットについても "{}" だけ覚えておけばなんとかなります。

let hello = "Hello ";
let world = "world!";
let hello_world = format!("{}{}", hello, world);
print!("{}", hello_world);

String::push_str()

もう一つは String::push_str() メソッドで、既存の文字列に他の文字列を追加できます。このメソッドの引数の型は String ではなく &str なので、 String を結合するなら & をつけます。なぜかって? 細かいことは後回しです。

let mut hello = "Hello ".to_string();
let world = "world!".to_string();
hello.push_str(&world);

+ 演算子

加えて + 演算子があります。 こちらは結合した新しい文字列を返します。 push_str() と同様に引数の型が &str ですので、 String を結合するならやっぱり & が必要です。

let hello = "Hello ".to_string();

// 文字列リテラル &str ならこれで OK
let hello_world = hello + "world!";

// String なら & をつける
let world = "world!".to_string();
let hello_world = hello + &world;

コメントを頂いて勘違いに気付いたのですが、文字列に対する + 演算子の戻り値は左辺の値です。メモリーアロケーションは発生しません。どういうことかと言うと、 + 演算子はこう実装されています。

// Add トレイトで + 演算子を実装できる
impl<'a> Add<&'a str> for String {
    type Output = String;

    #[inline]
    fn add(mut self, other: &str) -> String {
        self.push_str(other);
        self
    }
}

+ 演算子は左辺の値に対して push_str() を呼んでいるだけです。 push_str() との違いは二つ、戻り値があることと、呼び出し時に左辺の値の所有権を譲渡しなければならないことです。とは言え、 push_str() の別名みたいなものですし、そう難しく考えなくてもいいと思います。それに大抵の場合は format! か push_str() を使う方が楽でしょう。

コレクションのクローンに注意

もしかすると誰もこんな勘違いはしないかもしれませんが、クローン前とクローン後のコレクションは参照を共有しません。同じ要素を持っていても、それぞれは独立したコレクションです。つまり、 A という Vec をクローンした B があるとして、 B の要素を編集しても A には影響がない、ということです。

次のコードでは、 vec1 をクローンした vec2 の要素を編集して、それぞれの要素のリストを表示します。

let vec1 = vec![1,2,3,4,5];
let mut vec2 = vec1.clone();
vec2[0] = vec2[0]*2;
vec2[1] = vec2[1]*2;
vec2[2] = vec2[2]*2;
vec2[3] = vec2[3]*2;
vec2[4] = vec2[4]*2;
print!("vec1: ");
for e in vec1.iter() {
    print!("{} ", e)
}
println!("");
print!("vec2: ");
for e in vec2.iter() {
    print!("{} ", e)
}
println!("");

実行結果:

vec1: 1 2 3 4 5 
vec2: 2 4 6 8 10 

この通り、 vec2 を編集しても vec1 に影響はありません。どうしてだか私は「 vec2 への操作は vec1 に影響する」と思い込んでしまっていて、それを回避しようとして、データ構造を不要に複雑にしてしまったことがあります。かと言って、コレクションを共有しようとして可変の借用を使うと、それはそれで複雑になるので、慣れないと落とし所を見つけるのが難しいかもしれません。基本的には、新しいオブジェクトを作って操作することを考えるべきなのかもしれません。

イテレーターを探せ

イテレーター以外の、 map とか filter とか、コレクションの各要素に対して処理を行う他のメソッドを一度は探すと思います。探してませんか? 知ってましたか? さすがです。参りました。

そこら辺のメソッドはだいたいイテレーターに実装されています。正確には、イテレーターは Iterator トレイト を実装したオブジェクトですので、この API を探してみてください。難点は、型定義がやたらとややこしそうに見えることでしょうか。とりあえずは、 FnMut という型はクロージャーを指すと覚えておけばなんとかなるかと思います。

スマートポインター(または参照カウンター)

Rust で「何を」変更可能とするかはややこしくて、 mut というキーワードだけでも let mut の変数定義と &mut の可変の借用では意味が異なります。変更可能な値を使い回したい場合、単純に考えれば可変の借用を使えばいいのですが、時にはどうしても、借用に頼ることなく、値への参照をオブジェクト間で共有したい or しなければならない場合があります。しかもそういうときに限って、参照先の値を変更可能にしたいことが多いかと思います。できれば設計を見直すべきですが、それでもやっぱり避けられない場合もあるかと思います。一体どんな状況なのよと聞かれても一般的な例が思いつかない。すみません。

そういうとき、スマートポインター(または参照カウンター)に相当する手段が解決策の一つだったりするかもしれません。で、それはおそらく Rc<RefCell<T>> で実現できます。詳細はこちらが参考になります。

注意しなければならないのは、この種の操作は早い話が「所有権の管理は自分でやれ」ということです。 Rc も RefCell もスレッド間で受け渡しができず、 RefCell は扱い方を誤ると実行時に panic で落ちる可能性があります(その可能性をコンパイラは検出できません)。危険かつ面倒な処理になると思いますが、詳しくはドキュメントを参照してください。

その他

スタックオーバーフローの対処

thread '<main>' has overflowed its stack

このエラーが出たら、メインスレッド <main> のスタックが溢れた(オーバーフロー)ということです。このエラーで頻繁にアプリケーションが落ちる場合は、どこかで関数呼び出しをネストし過ぎてないか、無限ループに陥ってないか、まずソースコードを見直します。

それでも解決しなければ、スタックサイズを拡大する方法もあります。当面はバッドノウハウかもしれませんが、この方法でしか乗り切れない場面もあるかもしれません。

バックトレースを有効にする

環境変数 RUST_BACKTRACE をセットすると、 panic 発生時にバックトレースが表示されます。現時点では RUST_BACKTRACE の値は何でも構わないようですが、 RUST_BACKTRACE=1 としておくと無難そうです。ただ、各所の行番号は表示してくれないようで、わかりやすいとは言えないかも。

バックトレースの例:

thread '<main>' panicked at 'called `Option::unwrap()` on a `None` value', ../src/libcore/option.rs:365
stack backtrace:
   1:        0x109d5e010 - sys::backtrace::tracing::imp::write::he715e18a60aa49cbY7s
   2:        0x109d5d173 - panicking::on_panic::hf85837ce31d02cf7wWw
   3:        0x109d56282 - sys_common::unwind::begin_unwind_inner::h263a8f3a93eff05evas
   4:        0x109d5676e - sys_common::unwind::begin_unwind_fmt::he11604dee4a7c025B9r
   5:        0x109d5c9f7 - rust_begin_unwind
   6:        0x109d7c4b0 - panicking::panic_fmt::hc3363d565c1048da4HJ
   7:        0x109d7befc - panicking::panic::h14af70be4f3d4feaBGJ
   8:        0x109cf0b97 - main::hb74001eb22c0d750Zea
   9:        0x109d5efe2 - sys_common::unwind::try::try_fn::h8880715059644542985
  10:        0x109d5c838 - __rust_try
  11:        0x109d5ee6b - rt::lang_start::hf0f318bf9acb9103hUw
💖 💪 🙅 🚩
szktty
SUZUKI Tetsuya

Posted on January 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

Daemons on macOS with Rust
undefined Daemons on macOS with Rust

November 29, 2024