コンテンツにスキップ

Rustの借用

所有権のページでは、ひとつ困ったことがありました。String を関数に渡すとムーブされてしまい、そのあと使えなくなるのでした。値をいちいちムーブしたり clone したりするのは大変です。そこで Rust が用意しているのが借用(borrowing)です。

借用とは、所有権を渡さずに、値を使わせてあげることです。友だちに本を貸すのと同じです。友だちは本を読んだら返してくれますし、本の持ち主はずっとあなたのままです。

値の前に & を付けると、参照(reference)が作れます。参照を使うと、関数はデータを所有せずに中身を見ることができます。

fn print_length(s: &String) { // s を借りるだけで、所有はしない
println!("長さ: {}", s.len());
}
fn main() {
let text = String::from("hello");
print_length(&text); // text を関数に貸す
println!("まだ自分のもの: {text}"); // ✅ text はここでもまだ使える
}

実行結果:

長さ: 5
まだ自分のもの: hello
▶ 実際に試してみよう

(この例では文字列の長さを表示するので、"hello" は英語のままにしています。日本語の文字は UTF-8 で1文字3バイトあるため、.len() の結果が変わってしまうからです。)

&text は「この関数に text を貸してあげて」という意味です。関数は借りただけなので、所有権は main に残ったままです。だから、呼び出したあとも text を使い続けられます。所有権のページでは、& のない同じようなコードがエラーになっていたことを思い出してください。

ふつうの & 参照は読み取り専用です。見ることはできても、書き換えることはできません。借りた値を関数の中で書き換えたいときは、&mut を使って可変参照(mutable reference)にします。

fn add_excitement(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut text = String::from("こんにちは");
add_excitement(&mut text); // 書き換えOKとして貸す
println!("{text}"); // こんにちは!!!
}

実行結果:

こんにちは!!!
▶ 実際に試してみよう

ここでは3つのことがそろっている点に注目してください。text が mut で宣言されていること、関数が &mut String を受け取ること、そして呼び出すときに &mut text を渡していることです。借りた値を書き換えるには、この3つすべてが必要です。

Rust は参照について2つのルールを守らせます。これはデータ競合(data race)を防ぐためのものです。データ競合とは、2か所のコードが同じデータに同時に触れて、データを壊してしまう種類のバグのことです。

ある時点で持てるのは、次のどちらか一方です。

  • 読み取り専用の参照(&)をいくつでも、または
  • 可変参照(&mut)をちょうどひとつ

ただし、両方を同時に持つことはできません。

かんたんに言えば、「読む人は何人いてもいいけれど、書く人はひとりだけ。読む人と書く人が同時にいてはいけない」ということです。次のコードはこのルールを破っています。

fn main() {
let mut text = String::from("こんにちは");
let r1 = &text; // 読み取り専用で借りる
let r2 = &mut text; // ❌ r1 が生きている間は、書き換え用に借りられない
println!("{r1} {r2}");
}

コンパイラはここで止めてくれます。

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

このメッセージは「text はすでに不変(読み取り専用)として借用されているので、可変として借用することはできません」という意味です。

借用はスコープの閉じ } まで続く、と思うかもしれません。実はそうではありません。借用が続くのは、その参照が最後に使われる場所までです。そこから先は、値をまた借用できます。可変で借りることもできます。

つまり、上のエラーになる例は、r2 を作る前に r1 を使い終えるだけで直せるのです。

fn main() {
let mut text = String::from("こんにちは");
let r1 = &text;
println!("読み取り中: {r1}"); // r1 を使うのはここが最後なので、借用はここで終わる
let r2 = &mut text; // ✅ OK:もう読んでいる人はいない
r2.push_str("、世界");
println!("{text}");
}

実行結果:

読み取り中: こんにちは
こんにちは、世界
▶ 実際に試してみよう

ルールは変わらず「読む人は何人でも、または書く人はひとりだけ」です。ただ、それぞれの参照が実際に使われている範囲で判定されるだけです。書く人の r2 が登場する時点で、読む人の r1 はもう用が済んでいます。そして最後の println! がもう一度 text を読む前に、r2 も用が済んでいます。どこも重なっていないので、Rust は満足してくれます。

これが借用エラーのいちばんよくある直し方です。順番を入れ替えましょう。読み取りと書き込みを混ぜずに、必要な読み取りを全部済ませてから書き込む(またはその逆)ようにします。

これらのルールはすべてコンパイル時にチェックされます。そのため、プログラムの実行時には一切コストがかかりません。Rust が「データ競合が起きない」ことを約束しながら C と同じくらい速く動けるのは、このためです。安全性は、プログラムが動き出す前にすでに証明されているのです。

まずは自分で解いてみましょう。Rust Playground でも、手元のプロジェクトでもかまいません。コンパイラに怒られたら、エラーを読んでみてください。たいていは、借用が重なっている場所をちょうど示してくれます。

Vec<String> を借用して、すべての単語の文字数の合計を返す関数 total_letters を書いてください。呼び出したあとも、main でそのベクタを表示できるようにします。

ヒント:引数の型は &Vec<String> で、呼び出すときは &words を渡します。借りたベクタを for word in list でループして、word.len() を足していきましょう。

解答を見る
fn total_letters(list: &Vec<String>) -> usize {
let mut total = 0;
for word in list {
total += word.len();
}
total
}
fn main() {
let words = vec![String::from("borrow"), String::from("check")];
let count = total_letters(&words); // ベクタを貸す
println!("{count}文字");
println!("{:?} はまだ main のもの", words);
}

実行結果:

11文字
["borrow", "check"] はまだ main のもの
▶ 実際に試してみよう

関数は単語を見るだけなので、読み取り専用の & で借りれば十分です。もし list: Vec<String> と書いていたら、ベクタは関数へムーブしてしまい、最後の println! がコンパイルできなくなります。

なお、単語は英語のままにしています。.len() が数えるのは文字数ではなくバイト数で、日本語の文字は1文字あたり3バイトになるため、日本語にすると結果が変わってしまうからです。

買い物リストに品物を追加する add_item と、すべての品物を表示する print_list の2つの関数を書いてください。品物を3つ追加してから、リストを表示します。

ヒント:一方の関数はリストを変更し、もう一方は読むだけです。&mut Vec<String> が必要なのはどちらで、&Vec<String> で足りるのはどちらでしょうか。&str を String に変えるには String::from(item) を使います。

解答を見る
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &Vec<String>) {
for item in list {
println!("- {item}");
}
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "牛乳");
add_item(&mut list, "パン");
add_item(&mut list, "りんご");
print_list(&list);
}

実行結果:

- 牛乳
- パン
- りんご
▶ 実際に試してみよう

add_item はリストを変更するので &mut で借ります。そのため main の list は mut にする必要があります。print_list は読むだけなので、ふつうの & で十分です。&mut list の借用はそれぞれ呼び出しが戻った時点で終わるので、次の呼び出しでまた借りられるのです。

次のプログラムはコンパイルできません。first と second の両方を残したまま、スコア: 10点 と表示されるように直してください。

fn main() {
let mut message = String::from("スコア:");
let first = &mut message;
let second = &mut message;
first.push_str(" 10");
second.push_str("点");
println!("{message}");
}
error[E0499]: cannot borrow `message` as mutable more than once at a time
--> src/main.rs:4:18
|
3 | let first = &mut message;
| ------------ first mutable borrow occurs here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

このエラーは「message を同時に2回以上、可変として借用することはできません」という意味です。

ヒント:書く人は同時にひとりだけです。上の借用はいつ終わる?を見てみましょう。second を作る前に、first を使い終えられないでしょうか。

解答を見る
fn main() {
let mut message = String::from("スコア:");
let first = &mut message;
first.push_str(" 10"); // first を使うのはここが最後:借用はここで終わる
let second = &mut message;
second.push_str("点");
println!("{message}");
}

実行結果:

スコア: 10点
▶ 実際に試してみよう

元のコードでは、4行目で second を作った時点で、first はまだ(5行目で)使われる予定でした。つまり、書く人が同時にふたりいたわけです。first.push_str を上に移せば、second が生まれる前に first の用が済むので、&mut が同時に2つ存在することはなくなります。

  • 借用(&)を使うと、所有権を渡さずに値を使わせられる。値は手元に戻ってくる。
  • ふつうの & 参照は読み取り専用。借りた値を書き換えるには &mut を使う。
  • 参照を通して書き換えるには、変数・引数・呼び出しのすべてに mut / &mut が必要。
  • ルール:読む人は何人でも、または書く人はひとりだけ。両方同時はダメ。
  • 借用は参照が最後に使われた場所で終わる。そのため、コードの順番を入れ替えるだけで借用エラーが直ることが多い。
  • これらはすべてコンパイル時にチェックされるので、実行時のコストはゼロ。

理解度チェック

1. 関数に値を渡すとき、値の前に & を付けるとどうなりますか?

2. 借用のルールで、同時に持つことが許されている組み合わせはどれですか?

3. 参照を通して値を書き換えるには、何が必要ですか?

4. Rust が実行時のパフォーマンスを落とさずに借用のルールを守らせられるのはなぜですか?

スコア: 0 / 4