Rustの文字列とコピーのlifetimeについて
Rustで簡単なマルチスレッドプログラムを書いているとき、ふとコンパイルが通らなくなって困ったのでいろいろと調べていて、gitterできいて解決したことを書き留めておく。
標準入力から入力された文字列を取得して関数に渡し、その中でチャネルを通してスレッドに送り、スレッド側で標準出力にその文字列を出力する。 指定された回数分だけそのスレッドが文字列を受け取ったら、メインスレッド側にまた別のチャネルを使って終わったことを知らせる。 sync_channelはブロックするので、このように別のチャネルをスレッドジョインの役目として使うこともできる。 以下が動くバージョンのプログラム。
use std::thread; use std::sync::mpsc; use std::io; fn call<'a>(txt:&'a String, tx1: &mpsc::SyncSender<String>){ tx1.send(txt.clone()).unwrap(); } fn main() { let mut txt = String::new(); let _ = io::stdin().read_line(&mut txt); let (tx1,rx1) = mpsc::sync_channel(1); let (tx2,rx2) = mpsc::sync_channel(1); thread::spawn(move||{ for _ in 0..2 { let rtext = rx1.recv().unwrap(); println!("{}",&rtext); } tx2.send(()).unwrap(); }); call(&txt,&tx1); txt = "xyz".to_string(); call(&txt,&tx1); rx2.recv().unwrap(); }
この場合、txtはmainで複数回使われているので、callに対して所有権を渡すことはできず、callがそれを借用することになる。ここで、callの引数となるtxtを&String
にすべきか&str
にすべきかが良く分からなかった。文字列リテラルは&str
で、to_string()
メソッドでStringに変換できるし、&
をつけることで&str
にもできる。ただしこのプログラムでよく分かる明らかな違いは、str
はClone
を実装していないということで、callの引数となるtxtはおいそれと&str
にはできないはず、ということ。チャネルの先はメインスレッドよりも長生きするかもしれないスレッドのため、txtのポインタを送ることはできないので、ここでClone
は必須。
例えば以下のようにすると、
fn call<'a>(txt:&'a str, tx1: &mpsc::SyncSender<&str>){ tx1.send(txt.clone()).unwrap(); }
以下のようなコンパイルエラーになる。
src/main.rs:6:18: 6:25 error: cannot infer an appropriate lifetime for lifetime parameter `'a` due to conflicting requirements src/main.rs:6 tx1.send(txt.clone()).unwrap(); ^~~~~~~
これは、callの第2引数の型に'a
がついていないので第1引数のtxtとはlifetimeが異なるはずだが、callが借用したtxtの参照先をチャネルで渡そうとしているので要求が矛盾している、と言われていることになる。txtに対してclone()を呼んでいるので、意図としては借用したtxtの値をコピーしたものを渡しているつもりだが、これはtxtが&str
であることにより、値ではなくポインタをコピーしていることになってしまう。str
はClone
を持っていないが、ポインタはそうではないということらしい。
なお、ここのコンパイルエラーでは以下のようにも言われてしまうが、
consider using an explicit lifetime parameter as shown: fn call<'a>(txt: &'a str, tx1: &mpsc::SyncSender<&'a str>)
これに従ったからといって解決しない。結局は、
`txt` does not live long enough
となり、上記で説明した通りスレッドの方がメインスレッドよりも長生きするかもしれないため、コンパイルは通らない。