はじめに:なぜ「借用」が必要なのか? 🤔
前のステップで学んだ「所有権」は、Rustのメモリ安全性を保証する非常に重要な概念です。ある値の所有権は、通常、一つの変数にしか存在できません。関数に値を渡すと、所有権も移動(move)してしまい、元の変数は使えなくなりましたね。
しかし、プログラムを書いていると、「所有権を移動させずに、一時的に値を使いたい」という場面がたくさんあります。例えば、関数の引数として値を渡して処理を行い、関数が終わった後も元の変数を使いたい場合などです。
そこで登場するのが借用(Borrowing)という考え方です。借用を使うと、値の所有権を移動させることなく、その値への参照(Reference)を貸し出すことができます。これにより、安全かつ効率的にデータにアクセスできます。
Rustには主に2種類の借用があります。
&T
: 不変の借用(Immutable Borrow)または共有参照(Shared Reference)&mut T
: 可変の借用(Mutable Borrow)または排他参照(Exclusive Reference)
それぞれの詳細を見ていきましょう!
不変の借用(&)- 読み取り専用アクセス 📖
&T
は、値への不変の参照を作成します。これは「値を借りて見るだけ」というイメージです。不変の参照を使っている間、その値を変更することはできません。
一番の特徴は、一つの値に対して複数の不変の参照を同時に持つことができる点です。データを読み取るだけなら、何人が同時に見ていても問題ない、という考え方ですね。
例を見てみましょう。
fn main() {
let s1 = String::from("hello");
// s1への不変の参照を作成
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len); // s1 はまだ有効!
// 複数の不変参照も可能
let r1 = &s1;
let r2 = &s1;
println!("r1 = {}, r2 = {}", r1, r2);
// 不変参照を使っている間は、元の値を変更できない
// s1.push_str(", world"); // これはコンパイルエラー!
}
// 文字列の長さを計算する関数(不変参照を受け取る)
fn calculate_length(s: &String) -> usize {
// s.push_str(", world"); // ここでも変更はできない!
s.len()
} // ここで s (参照) はスコープを抜けるが、参照先の s1 は影響を受けない
&
を使って関数の引数に参照を渡すことを借用と呼びます。関数は値の所有権を得るのではなく、一時的に借りるだけです。
可変の借用(&mut)- 読み書きアクセス ✏️
&mut T
は、値への可変の参照を作成します。これは「値を借りて変更する」ことを可能にします。
可変の借用には非常に重要な制約があります。それは、特定のスコープにおいて、一つの値に対して可変の参照は一つしか存在できないということです。これは、複数の場所から同時にデータが変更されることによって起こる「データ競合」を防ぐためのRustの強力な安全機能です。
例を見てみましょう。
fn main() {
let mut s = String::from("hello"); // 可変にするには mut が必要
// s への可変の参照を作成
change(&mut s);
println!("{}", s); // change 関数で変更された後の値が出力される
// 可変参照は一つだけ
let r1 = &mut s;
// let r2 = &mut s; // これはコンパイルエラー!同時に2つの可変参照は持てない
// r1 を使って変更
r1.push_str(" world!");
println!("{}", r1);
// 可変参照が存在する間は、不変参照も作れない
// let r3 = &s; // これもコンパイルエラー!
}
// 文字列を変更する関数(可変参照を受け取る)
fn change(some_string: &mut String) {
some_string.push_str(", world");
} // ここで some_string (可変参照) はスコープを抜ける
&mut T
)が存在する間は、その値に対する他の参照(不変&T
も含む)は一切作れません。逆に、不変の参照(&T
)が一つでも存在する間は、可変の参照(&mut T
)を作ることはできません。
借用のルールまとめ ✅
Rustのコンパイラ(特に借用チェッカー Borrow Checkerと呼ばれる部分)は、以下のルールが守られているか厳しくチェックします。これにより、コンパイル時に多くの潜在的なバグ(特にメモリ安全に関するもの)を防ぐことができます。
ルール | 説明 | 不変の借用 (&T) | 可変の借用 (&mut T) |
---|---|---|---|
共存ルール 1 | 同時に存在できる参照の数 | ✅ 複数可能 | ❌ 1つだけ |
共存ルール 2 | 不変参照と可変参照の共存 | ❌ 同時には存在できない (不変参照が複数ある時、可変参照は作れない。逆も同様) |
|
有効期間ルール | 参照は、参照先の値(所有者)よりも長く生存してはいけない | ✅ 必須 (ダングリングポインタ防止) | |
データ変更 | 参照を通じて元のデータを変更できるか | ❌ できない | ✅ できる |
これらのルールは、最初は少し厳しく感じるかもしれませんが、Rustの安全性と並行性の高さを支える根幹となっています。慣れてくると、これらのルールがプログラムの正しさを保証してくれる強力な味方であることがわかります。
ダングリングポインタの防止 🚫🔗
借用の重要な利点の一つは、「ダングリングポインタ(dangling pointer)」、つまり既に解放されたメモリ領域を指してしまう無効な参照の発生を防ぐことです。
Rustの借用チェッカーは、参照がその参照先のデータよりも長く生存しないことをコンパイル時に保証します。
// fn dangle() -> &String { // dangle は String への参照を返す
// let s = String::from("hello"); // s は新しい String
//
// &s // s への参照を返す
// } // ここで s はスコープを抜け、解放される。しかし参照は残ろうとする!
fn main() {
// let reference_to_nothing = dangle(); // これはコンパイルエラー!
println!("このコードはコンパイルできません");
}
上記の例では、関数dangle
は関数内で作成したString
(s
) への参照を返そうとします。しかし、s
はこの関数の終わりでスコープを抜けて解放されてしまいます。そのため、返される参照は無効なメモリを指すことになり、これはコンパイルエラーとなります。
このように、借用チェッカーは参照が常に有効なデータを指すことを保証してくれるのです。参照の有効期間(ライフタイム)については、次のステップでさらに詳しく学びます。
まとめ 🎉
今回はRustの重要な概念である「借用」について学びました。
- 不変の借用 (
&
): データを読み取るための参照。複数同時に存在可能。 - 可変の借用 (
&mut
): データを変更するための参照。同時に一つしか存在できない。 - 借用ルールにより、データ競合やダングリングポインタといった問題をコンパイル時に防ぐことができる。
所有権と借用はRustの核心的な機能であり、安全で効率的なコードを書くための基盤となります。これらの概念をしっかり理解することが、Rustマスターへの道です! 💪
次のステップでは、参照がどれくらいの期間有効であるかを示す「ライフタイム」について詳しく見ていきます。