Rustでスレッドを起動するにはthread::spawn()を使う。この関数は引数にクロージャをとるのだけど、引数の定義を見ると、 F: FnOnce() -> T + Send + 'static
となっていて初見では理解するのが難しい。ここではFnOnce()
、つまりクロージャの型について、どういう風に使い分けるのか、といった視点で見ていこうと思う。
Rustにはクロージャを表す型が3つある。
- Fn()
- FnMut()
- FnOnce()
型が3つ必要なのはRustが値の所有権や借用、生存期間をきちんと把握するためである。
Fn()
は使う側、引数として受け取る側としては使い勝手が一番よい。何度でも呼び出せるし、クロージャ自体が可変じゃなくてもいい。これは立場を変えると、クロージャを渡す側としてはFn()
は制約が一番厳しいことを意味する。Fn()
として渡されるクロージャは、何度実行されても所有権や借用のルールを破らないようにしなければならない。例えば、値の所有権をとってそれを消費するクロージャはFn()
としては渡せない。一度値を消費してしまったら二度は使えないから。以下のコードはコンパイルエラーで通らない。
fn do_something<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let nums = vec![1,2,3];
do_something(move || {
for n in nums { ... } // Consume `nums`
});
}
FnOnce()
の使い勝手はFn()
の逆と言える。使う側としての制約は一番厳しい。一度しか実行できないし、実行にはそのクロージャの(借用ではなく)所有権が必要となる。一方、渡すクロージャの自由度は大きい。クロージャは一度しか呼ばれないことが保証されているので、そのクロージャ内で値を消費してもいいし、生存期間さえ守っていれば可変参照を取って値を変更することもできる。
fn do_something<F: FnOnce()>(f: F) {
f();
// Can't use `f` more than once
}
fn main() {
let nums = vec![0,1,2];
let mut greeting = "Hello".to_string();
let greeting_ref = &mut greeting;
do_something(move || {
for n in nums { ... } // Consume `nums`
greeting_ref.push_str(", World"); // Mutate value via mutable ref
});
println!("{}", greeting);
}
この流れで行くとFnMut()
はFn()
とFnOnce()
の中間的な立ち位置になる。値の消費はしないけれど可変参照経由で変更はする、みたいなクロージャはFnMut()
として取り扱うことができる。FnMut()
は何度も呼べるけれど、内部に可変参照をもっているのでそのクロージャを実行するにはmutを必要とする。
fn do_something<F: FnMut()>(mut f: F) {
f();
f();
}
fn main() {
let mut greeting = "Hello".to_string();
let greeting_ref = &mut greeting;
do_something(move || {
greeting_ref.push_str(", World");
});
println!("{}", greeting);
}
ここでthread::spawn()
に立ち戻ってみよう。ライブラリ関数としては利用方法にできるだけ制限をかけたくない。ライブラリ利用者側、つまりクロージャを渡す側にとって一番制約がゆるいのはFnOnce()
である。おそらくこれがspawn()
がFnOnce()
なクロージャを受け付けるようになっている理由だろう。spawn()
は渡されたクロージャを一度実行すればよいだけなので、利用者側の制約が厳しいFn()
を要求する必要はない。
自分がAPIを提供する立場だとしたら、引数として受け取るクロージャの型には注意しておきたい。ライブラリの中身を実装している最中に、コンパイラを通すためだけに安易にFn()
を要求するAPIを作ってしまうと、あとで利用する側で困る、みたいなことになりかねない。
ただ、thread::spawn()
と似たようなAPIを自作して提供する場合にはひとつ落とし穴がある。公式ドキュメントで言及されているので、自分で作ってみようとする前に目を通しておくとよい。自分はこれに悩んで時間を浪費してしまった。
追記
Send
について記事を書いた。