Rust 基礎講座 #4 参照と借用 — & と &mut、借用規則のコンパイルエラーを解決する

読了 5分

前回の最後で、関数に値を渡すたびに所有権を奪われ、戻り値で返してもらうぎこちないコードを見ました。今回の参照(reference)がその答えです。所有権は元の場所に置いたまま、値へアクセスする権利だけを借りる。Rust の用語で借用(borrowing)です。そしてこの借用に付いた 1 つの規則が、所有権とともに Rust の安全性の残り半分を担います。

参照 — 所有権なしで読む #

src/main.rs
fn count_chars(text: &String) -> usize {
    text.chars().count()
} // text は参照にすぎないので、ここでは何も解放されない

let summary = String::from("月次サマリー");
let n = count_chars(&summary); // & で参照だけを渡す
println!("{summary}: {n}文字");  // summary は依然として有効

&summary は、値を渡す代わりに値を指す参照を作ります。関数は値を読めますが所有者ではないので、関数が終わっても値は解放されず、summary はそのまま使えます。前回の「返してもらう」コードが丸ごと消えます。読むだけの関数の引数は参照で受け取るのが Rust の基本形です。

可変参照 — 借りて変更する #

変更まで必要なら &mut で借ります。貸す側の変数自体も mut でなければなりません。

src/main.rs
fn append_signature(body: &mut String) {
    body.push_str("\n-- 田中");
}

let mut mail = String::from("お疲れさまです。");
append_signature(&mut mail);

呼び出し側に &mut が明示される点が重要です。他の言語では関数が引数を変更するかどうかシグネチャだけでは分からないことが多いですが、Rust では変更されうる受け渡しが、呼び出す側のコードに表示されます。

借用規則 — 読み手は複数、書き手は 1 人 #

参照には規則が 1 つあります。ある時点で、不変参照(&)はいくつでも可能ですが、可変参照(&mut)は 1 つだけ、そして両方を同時に使うことはできません。 読んでいる人がいる間、値は変わらないという保証です。

この規則が実際に防ぐ事故を見てみます。コレクションを反復しながらそのコレクションに追加する、多くの言語で微妙なバグやランタイム例外になるコードです。

src/main.rs
let mut items = vec![1, 2, 3];
for item in &items {          // items を不変で借用中
    if *item == 2 {
        items.push(99);       // コンパイルエラー: 可変借用の試み
    }
}
コンパイルエラー
error[E0502]: cannot borrow `items` as mutable because it is also
              borrowed as immutable
  |
2 |     for item in &items {
  |                 ------ immutable borrow occurs here
4 |             items.push(99);
  |             ^^^^^^^^^^^^^^ mutable borrow occurs here

push は内部の格納領域が足りなくなるとデータ全体を新しいメモリへ移すのですが、その瞬間、反復中の参照は解放済みの古いメモリを指すことになります。C++ なら未定義動作、Java ならランタイムの ConcurrentModificationException になる状況が、Rust ではコンパイルエラーです。マルチスレッドに進むと、同じ規則がデータ競合(片方が書いている間にもう片方が読む競争状態)をコンパイル時に遮断する装置になります。

cannot borrow as mutable エラーに出会ったら、戦うのではなく借用の範囲を狭めるのが定石です。参照は最後に使われた箇所までしか生きていないものとして計算されるので、読み取りを先に終わらせ、変更を後ろへ移せば大半は解決します。上の例なら、反復中は追加する値を集めるだけにして、反復が終わった後にまとめて push する形です。

ダングリング参照 — 解放済みメモリは指せない #

src/main.rs
fn make_greeting() -> &String {
    let s = String::from("こんにちは");
    &s // コンパイルエラー: s は関数が終わると解放される
}

ローカル変数の参照を返すと、関数が終わった瞬間に値は解放され、参照だけが残ります。C でスタックのアドレスを返してしまう古典的なバグです。Rust のコンパイラは「借りた値が参照より先に死ぬ」すべてのケースを拒否します。この場合の答えは、参照ではなく String そのものを返して所有権を渡すことです。参照がどれだけの間有効かを扱うこの検査の全体像が、第 9 回で扱うライフタイム(lifetime)です。

スライス — 一部だけを借りる #

参照の変種として、コレクションの連続した一部を借りるスライスがあります。

src/main.rs
let scores = vec![90, 85, 72, 60];
let top: &[i32] = &scores[..2];    // 先頭 2 つだけを借用

let title = String::from("Rust 基礎講座");
let first: &str = &title[..4];     // "Rust"

文字列スライス &str は特に重要です。文字列リテラル("hello")の型がまさに &str で、関数が文字列を読むだけなら、引数は &String より &str で受け取るのが慣例です。String でもリテラルでも両方受け取れるからです。先ほどの count_chars を実務の形に直すとこうなります。

src/main.rs
fn count_chars(text: &str) -> usize {
    text.chars().count()
}

1 つ注意があります。文字列スライスのインデックスは文字数ではなくバイト位置です。日本語は UTF-8 で 1 文字がおおむね 3 バイトなので、&title[..4] のような任意の切り出しは、文字の途中に当たるとパニックになります。文字列のこうした性質は第 7 回できちんと扱います。

まとめ #

  • 参照 & は、所有権を移さずに値を読む借用です。読み取り専用の関数引数の基本形で、変更が必要なら &mut で借ります。
  • 借用規則は 1 つです。不変参照は複数、可変参照は 1 つ、同時は不可。反復中の変更とデータ競合が、この規則 1 つでコンパイル時に遮断されます。
  • cannot borrow as mutable エラーは借用の範囲を狭める方向で解決します。読み取りを先に終わらせ、変更を後ろへまとめるのが定石です。
  • 解放される値を指す参照の返却はコンパイルが拒否します。その場合は所有権そのものを返します。
  • 文字列を読む関数は &String ではなく &str で受け取るのが慣例です。リテラルと String の両方を受け取れます。
  • 次回はデータを構造化する道具である構造体と列挙型、そして Rust で null を代替する Optionmatch を扱います。
X