Posted at

Rustの関数・メソッドとライフタイム

More than 1 year has passed since last update.

思い出したようにRustを書くので学習が進まない……


問題

このコードはエラーになります。

{

let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
let array = line.trim().split_whitespace().collect::<Vec<_>>();
line.clear();
}

error[E0502]: cannot borrow `line` as mutable because it is also borrowed as immutable

|
| let array = line.trim().split_whitespace().collect::<Vec<_>>();
| ---- immutable borrow occurs here
...
| line.clear();
| ^^^^ mutable borrow occurs here
| }
| - immutable borrow ends here

一方これはエラーになりません。

let mut line = String::new();

std::io::stdin().read_line(&mut line).unwrap();
let a: u8 = line.trim().parse().unwrap();
line.clear();


検討

上の違いについて考える前に、先のエラーを確認します。


cannot borrow line as mutable because it is also borrowed as immutable


と出ていますから、これは「mutableな参照を取ったら他にいかなる参照も取れない」というルールに違反していることがわかります。

しかし、すると1点気になることがあります。それは「そもそもtrim()の前に&mut lineでmutableな参照を取っている」ということです。

これがなぜセーフなのかは、read_line()のシグニチャを見るとわかります。

pub fn read_line(&self, buf: &mut String) -> Result<usize>

( https://doc.rust-lang.org/std/io/struct.Stdin.html#method.read_line )

ライフタイム省略の規則により&selfbufは別々のライフタイムとなり、そしてbufのライフタイムについては何も書かれていない(Resultはライフタイムパラメータを持たないから)ので、bufという変数に束縛された借用はread_line()の中でだけ保持されているということになります。

従ってread_line()の次の行では、lineはもう借用されていないので、問題ないというわけです。

この観点で以下の2つの呼び出しを確認します。

let array = line.trim().split_whitespace().collect::<Vec<_>>();

let a: u8 = line.trim().parse().unwrap();

メソッドのシグニチャは以下の通りです。

pub fn trim(&self) -> &str

( https://doc.rust-lang.org/std/primitive.str.html#method.trim )

pub fn split_whitespace(&self) -> SplitWhitespace

( https://doc.rust-lang.org/std/primitive.str.html#method.split_whitespace )

fn collect<B>(self) -> B 

where
B: FromIterator<Self::Item>,

( https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect )

pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err> 

where
F: FromStr,

( https://doc.rust-lang.org/std/primitive.str.html#method.parse )

どちらの場合でも、まずtrim()は、ライフタイム省略の規則により、&selfと返り値のライフタイムは同じとなります。

split_whitespace()は、SplitWhitespaceは実際SplitWhitespace<'a>なので、ライフタイムは返り値に引き継がれます。collect()についてはちょっと複雑なのですが、SplitWhitespace<'a>におけるIteratorの実装はtype Item = &'a strとなっているので、やはりcollect()の返り値までライフタイムが引き継がれています。

結果として、arrayが残っている限りlineは借用された状態になります。

一方parse()&selfのライフタイムについて何も書いていない(Resultはライフタイムパラメータを持たないし、F = u8だから)ので、lineの借用はここで終わります。

意味的には、前者は「メモリ上の文字列をホワイトスペースで区切った先頭の文字へのポインタ」を持っているのだから、元の文字列を書き換えられると困るのに対し、後者は「メモリ上の文字列から得られた別なデータ」を持っているので、元の文字列がどうなっても困らないということです。これをコンパイル時に判断できるのがありがたいですね。


対応

意味的には「もう元の文字列を好きにしていいよ」ということが示せればよいです。文法的にはブロックスコープを使って借用期間を制限すればよいです。

let mut line = String::new();

std::io::stdin().read_line(&mut line).unwrap();
{
let array = line.trim().split_whitespace().collect::<Vec<_>>();
}
line.clear();