[Rust 入門] #04 借用と参照 — & と &mut、借用の決まりを動かして確かめる
はじめに
第3回のエラーメッセージに、2度、同じ方向の助言が出ていました。
note: consider changing this parameter type in function `takes` to borrow instead if owning the value isn't necessary
help: consider iterating over a slice of the `Vec<String>`'s content to avoid moving into the `for` loop
|
3 | for s in &v {
| +
どちらも、「所有権を取らずに、借りる」ことを勧めています。今回は、その借用と参照です。
借りる
関数の引数を、String から &String に変えて、渡す側も &s と書きます。
fn len_of(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = len_of(&s);
println!("{s} の長さは {n}");
}
hello の長さは 5
len_of(&s) のあとでも、s が使えています。第3回の takes(a) では、同じ形で、a が使えなくなっていました。
| 書き方 | 起きていること |
|---|---|
takes(a) | 所有権が、関数に移る(ムーブ)。呼んだ後、a は使えない |
len_of(&s) | 値を、関数に貸す(借用)。持ち主は、s のまま |
&s で作られたものが、参照です。参照を作ることを、借用と言います。s.len() のように、参照のままで、メソッドも呼べます。
参照は所有権を持たない
第3回の Noisy を使って、参照が drop に関わらないことを確かめます。
fn peek(n: &Noisy) {
println!("peek: {}", n.0);
}
fn main() {
let a = Noisy("a");
peek(&a);
peek(&a);
println!("main の終わり");
}
(Noisy と impl Drop は、第3回と同じです)
peek: a
peek: a
main の終わり
drop: a
peek が終わっても、drop は出ません。drop: a が出るのは、持ち主の a が、main の終わりで捨てられるときの、1回だけです。借りただけの peek は、後始末の責任を持っていません。
第3回の list.len() や println! が、所有権を取らなかったのも、同じ理由です。メソッド呼び出しや println! は、引数を借りて使います。
&String と &str
文字列を読むだけの関数は、&String ではなく、&str で受けるのが基本です。
fn count(s: &str) -> usize {
s.chars().count()
}
fn main() {
let owned = String::from("りんご");
println!("{}", count(&owned));
println!("{}", count("みかん"));
println!("{}", count(&owned[0..3]));
}
3
3
1
| 呼び出し | 渡している物 |
|---|---|
count(&owned) | String への参照(&String) |
count("みかん") | 文字列リテラル(&str) |
count(&owned[0..3]) | String の一部(&str)。"り" は 3 バイトなので、1 文字 |
&str で受けると、この3通りが、全部通ります。&String で受けると、文字列リテラルは渡せません。
error[E0308]: mismatched types
--> g1.rs:6:26
|
6 | println!("{}", count("みかん"));
| ----- ^^^^^^^^ expected `&String`, found `&str`
第2回で触れた「greet(name: &str)」も、この理由です。
Vec も、同じ考え方です。読むだけの関数で、&Vec<String> と書くと、cargo clippy が指摘します。
warning: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
--> src/main.rs:1:21
|
1 | fn print_list(list: &Vec<String>) {
| ^^^^^^^^^^^^
|
help: change this to
|
1 - fn print_list(list: &Vec<String>) {
1 + fn print_list(list: &[String]) {
&[String] は、Vec<String> の一部または全体を、見るだけの型です。スライスは、あとの節で扱います。
可変参照
借りたまま、書き換えたいときは、&mut を使います。
fn add_exclaim(s: &mut String) {
s.push('!');
}
fn inc(n: &mut i32) {
*n += 1;
}
fn main() {
let mut s = String::from("hello");
add_exclaim(&mut s);
add_exclaim(&mut s);
println!("{s}");
let mut n = 5;
inc(&mut n);
println!("{n}");
}
hello!!
6
| 部分 | 意味 |
|---|---|
s: &mut String | 引数の型。「書き換えてよい参照」 |
&mut s | 渡す側。書き換えを許して貸す |
*n += 1 | * で参照の先の値を取り出して、書き換える |
String の s.push('!') は、参照のまま書けます。i32 のような値は、*n と書いて、参照の先を指します。
書き間違えたとき
変数が mut でないと、&mut は取れません。
let s = String::from("hello");
add_exclaim(&mut s);
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> e1.rs:7:17
|
7 | add_exclaim(&mut s);
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
6 | let mut s = String::from("hello");
| +++
&mut を渡すべきところに、& を渡しても、エラーです。
let mut s = String::from("hello");
add_exclaim(&s);
error[E0308]: mismatched types
--> e2.rs:7:17
|
7 | add_exclaim(&s);
| ----------- ^^ types differ in mutability
| |
| arguments to this function are incorrect
|
= note: expected mutable reference `&mut String`
found reference `&String`
「& で貸す」と「&mut で貸す」は、別の型として、区別されます。
借用の決まり
借用には、決まりがあります。
| 同時に持てる参照 | 数 |
|---|---|
不変参照(&) | 何個でも |
可変参照(&mut) | 1個だけ |
| 不変参照と可変参照 | 同時には、持てない |
可変参照は1個だけ
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1} {r2}");
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> e3.rs:4:14
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | println!("{r1} {r2}");
| -- first borrow later used here
不変と可変は同時に持てない
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{r1} {r2}");
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> e4.rs:4:14
|
3 | let r1 = &s;
| -- immutable borrow occurs here
4 | let r2 = &mut s;
| ^^^^^^ mutable borrow occurs here
5 | println!("{r1} {r2}");
| -- immutable borrow later used here
読むときは、3つ目の矢印が、手がかりです。immutable borrow later used here(あとで、不変の借用が使われている)が、「その間、借りたままだった」ことを示しています。
借りている期間は、最後に使うところまで
借用は、参照を最後に使った場所で、終わります。スコープの終わりまでは、続きません。
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");
let r3 = &mut s;
r3.push('!');
println!("{r3}");
hello hello
hello!
r1 と r2 は、println! で最後に使われています。その後に &mut s を取っているので、通ります。上の E0502 との違いは、不変参照を、あとで使うかどうかだけです。
なぜ、こういう決まりか
「読んでいる最中に、書き換えられる」ことを、防ぐためです。2つの名前から、同時に書き換えられる状況も、同じ理由で防がれます。
Java と並べる
Java では、リストを for で回している最中に、リストを変えると、実行時に例外になります。
List<String> list = new ArrayList<>(List.of("a"));
for (String s : list) {
list.add(s);
}
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
Rust で、同じ形のコードを書くと、コンパイルで止まります。
let mut v = vec![String::from("a")];
for s in &v {
v.push(s.clone());
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> e7.rs:4:9
|
3 | for s in &v {
| --
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | v.push(s.clone());
| ^^^^^^^^^^^^^^^^^ mutable borrow occurs here
for s in &v は、v を借りて回します。回している間は、不変参照が続いているので、v.push の可変参照が、取れません。
第3回の for s in v と、今回の for s in &v の違いも、確かめます。
let v = vec![String::from("a"), String::from("b")];
for s in &v {
println!("{s}");
}
println!("{}", v.len());
a
b
2
&v と書くと、v は、ムーブされずに、あとでも使えます。
参照は、持ち主より長く生きられない
参照が、持ち主より長く残ることは、できません。
fn main() {
let r;
{
let s = String::from("x");
r = &s;
}
println!("{r}");
}
error[E0597]: `s` does not live long enough
--> e5.rs:5:13
|
4 | let s = String::from("x");
| - binding `s` declared here
5 | r = &s;
| ^^ borrowed value does not live long enough
6 | }
| - `s` dropped here while still borrowed
7 | println!("{r}");
| - borrow later used here
s は、内側の { } の終わりで捨てられます(第3回)。捨てられた物を指す参照が、外に残る状況は、こうして防がれます。
関数の中で作った値の参照を、返そうとしても、同じです。
fn dangle() -> &String {
let s = String::from("x");
&s
}
error[E0106]: missing lifetime specifier
--> e9.rs:1:16
|
1 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: instead, you are more likely to want to return an owned value
|
1 - fn dangle() -> &String {
1 + fn dangle() -> String {
|
「借りる元が、ありません」と言っています。help は、所有権ごと返す(-> String)ことを勧めています。lifetime(ライフタイム)は、第8回で扱います。
スライス
文字列や配列の一部を、借りるのが、スライスです。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{hello} / {world}");
let v = [10, 20, 30, 40, 50];
let mid = &v[1..4];
println!("{:?} len={}", mid, mid.len());
hello / world
[20, 30, 40] len=3
| 型 | 意味 |
|---|---|
&str | 文字列の一部(または全体)を、借りたもの |
&[T] | 配列や Vec の一部(または全体)を、借りたもの |
0..5 は、第2回の範囲(終わりを含まない)です。
スライスも借用
スライスは、元の値を借りているので、借用の決まりが、そのままかかります。
let mut s = String::from("hello world");
let w = &s[0..5];
s.clear();
println!("{w}");
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> e6.rs:4:5
|
3 | let w = &s[0..5];
| - immutable borrow occurs here
4 | s.clear();
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{w}");
| - immutable borrow later used here
w が見ている最中に、s.clear() で中身を消すと、w が、存在しない文字を指すことになります。それを、コンパイルの時点で、止めています。
文字の途中では、切れない
第2回で、"あいう".len() が 9(バイト数)になることを見ました。スライスの範囲も、バイトで数えます。
let s = "あいう";
println!("{}", &s[0..1]);
これは、コンパイルは通りますが、実行すると、止まります。
thread 'main' panicked at e8.rs:3:22:
end byte index 1 is not a char boundary; it is inside 'あ' (bytes 0..3 of string)
'あ' は 3 バイトなので、1 バイト目で切ると、文字の途中になります。&s[0..3] なら、通ります。
サンプル: 買い物リストを、借用で書き直す
第3回の買い物リストを、借用で書き直します。
cargo new shop2
cd shop2
src/main.rs を、次のようにします。
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &[String]) {
println!("{:?}({} 品目)", list, list.len());
}
fn count_chars(list: &[String]) -> usize {
let mut total = 0;
for s in list {
total += s.chars().count();
}
total
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "りんご");
add_item(&mut list, "みかん");
print_list(&list);
add_item(&mut list, "ぶどう");
print_list(&list);
println!("合計 {} 文字", count_chars(&list));
}
cargo run
["りんご", "みかん"](2 品目)
["りんご", "みかん", "ぶどう"](3 品目)
合計 9 文字
第3回の版との違いは、次のとおりです。
| 第3回 | 今回 |
|---|---|
let list = add_item(list, "りんご"); と、返して受け取り直す | add_item(&mut list, "りんご"); と、貸すだけ |
list を毎回 let で作り直す(シャドーイング) | let mut list を、1回作るだけ |
consume(list) で、最後に所有権を渡す | 所有権は、最後まで main に残る。main の終わりで、捨てられる |
| 読むだけの関数も、返す必要がある | 読むだけの関数は、&[String] で借りて、何も返さない |
count_chars の for s in list は、list が &[String](参照)なので、ムーブされません。s は &String になります。
cargo clippy と cargo fmt --check は、指摘なしで通りました。
歴史
| 年 | できごと |
|---|---|
| 2018年12月 | Rust 1.31 と、2018 エディションが出る(第1回の年表)。このころ、NLL(Non-Lexical Lifetimes)が、2018 エディションに入る |
NLL は、「借用の期間を、スコープの終わりまでではなく、参照を実際に使う範囲で決める」変更です。この記事の「借りている期間は、最後に使うところまで」の節は、この変更の上に成り立っています。借用チェッカーが賢くなり、それまで拒否されていた、正しいコードの一部が、通るようになりました。
RFC 2094 では、NLL を、「ライフタイムを、レキシカルスコープではなく、制御フローグラフに基づいて決める」ものと説明しています。
動作確認
確認できたこと
- 上のコードと出力は、すべて、Linux(Rust 1.97.0、Cargo 1.97.0)で、実際に実行した結果です
- Java の
ConcurrentModificationExceptionは、同じ環境の Java で、実際に実行した結果です - 借用の決まり(不変参照は複数、可変参照は1つ、両方は同時に持てない)と、そのエラー(
E0499、E0502) - 参照を最後に使ったあとは、可変参照が取れること(
r1・r2のあとの&mut s) - 参照は、
dropを起こさないこと(peekを2回呼んでも、drop: aは1回だけ) &strで受けると、&String・文字列リテラル・スライスの3つが通ること&Vec<String>に、cargo clippyがptr_argの警告を出すこと- 9つのエラー(
E0596、E0308を2種類の形、E0499、E0502を3種類の形、E0597、E0106)のメッセージ &[String]をforで回すと、取り出されるsが&Stringになること(型を指定してコンパイルが通ることで確認)- スライスの範囲が、バイトで数えられ、文字の途中で切ると、実行時に止まること(終了コード 101)
- 買い物リストのサンプルが、
cargo runで動くこと。cargo clippyとcargo fmt --checkが、指摘なしで通ること
歴史の出典
- NLL が「借用チェッカーが賢くなり、それまで拒否されていた正しいコードの一部を、受け付ける」変更であることは、Packt の記事(Rust 1.31 のリリース記事)の記述です。この1つの出典だけで、他の資料とは、突き合わせていません
- NLL が 2018 エディションに入ったことと、RFC 2094 の説明(制御フローグラフに基づく)は、Rust コンパイラチームの NLL ワーキンググループのページの記述です。このページには、安定化の日付や、Rust のバージョンは、書かれていません
- Rust 公式ブログ(1.31 のリリース告知、NLL 移行の記事)は、リダイレクトのページしか取得できず、本文は、確認できていません
確認できていないこと
- NLL が入る前は、この記事の
r1/r2/r3のコードが、エラーになっていたかどうか。「それまで拒否されていたコードの一部が通るようになった」という、一般的な記述だけを書いています - NLL が、2015 エディションを含めて、すべてのエディションで有効になった時期
- 「読んでいる最中に、書き換えられるのを防ぐため」という、借用の決まりの理由。これは、言語の設計の説明で、この記事のコードでは、確かめていません
- Windows と macOS での実行結果。出力の内容は、同じになるはずですが、実行はしていません
次回
次回は、構造体・enum・matchです。今回の Noisy(&'static str) のように、いくつかの値を1つにまとめる struct と、「いくつかの選択肢のうちの1つ」を表す enum、それを場合分けする match を、見ていきます。
連載の目次(予定)
入門編(8回)
- Rust とは / 環境構築 / Hello World
- 変数・型・関数(Java との対比)
- 所有権(ムーブ)
- 借用と参照(今回)
- 構造体・enum・match
- Result / Option とエラー処理
- トレイトとジェネリクス(Java の interface との違い)
- ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)