[Rust 入門] #04 借用と参照 — & と &mut、借用の決まりを動かして確かめる

[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回)

  1. Rust とは / 環境構築 / Hello World
  2. 変数・型・関数(Java との対比)
  3. 所有権(ムーブ)
  4. 借用と参照(今回)
  5. 構造体・enum・match
  6. Result / Option とエラー処理
  7. トレイトとジェネリクス(Java の interface との違い)
  8. ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)