第3章 所有権・借用とライフタイム
所有権(Ownership)は、Rust の最も独創的で重要な概念です。
GC(ガベージコレクタ)に頼らず、かつ手動解放のミスを起こさずにメモリ安全性を達成するための核心となる仕組みです。本章では、所有権のルール、値の移動(ムーブ)、参照と借用、スライス型、そしてライフタイムの基礎を順を追って理解します。
3.1 所有権の 3 つのルールとムーブ
プログラムが実行時に利用するメモリには、「スタック」と「ヒープ」があります。サイズが固定のデータ(整数や真偽値など)は高速なスタックに置かれますが、サイズが可変のデータ(動的な文字列 String や配列 Vec など)はヒープに領域を確保します。
Rust では、以下の 3 つの所有権ルール によってヒープメモリを自動的かつ安全に管理します。
- Rust の各値には、「所有者(Owner)」と呼ばれる変数が存在する。
- いかなる時点においても、所有者は常に 1 人(1 つの変数)だけである。
- 所有者がスコープ(
{})から外れたとき、その値は自動的にメモリから破棄(ドロップ)される。
所有権の移動(ムーブセマンティクス)
ヒープを保持する String 型の変数を別の変数へ代入すると、データの「所有権」が移動(ムーブ)します。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s1 から s2 へ移動する
// println!("{s1}"); // コンパイルエラー:s1 はすでに無効
println!("{s2}"); // OK
}
代入後、元の変数 s1 は無効化されるため、同じメモリを二重に解放してしまう事故(ダブルフリー)がコンパイル時に根本から防がれます。
データの複製(Clone)とスタック上のコピー(Copy)
ヒープ領域のデータそのものを深く複製(ディープコピー)したい場合は、明示的に .clone() を呼び出します。
#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープデータを複製
println!("s1: {s1}, s2: {s2}"); // 両方有効
}
一方、整数や真偽値などのサイズが固定されたスカラー型は、高速にビット単位で複製できるため、Copy トレイトが実装されています。これらの型は代入時にもムーブせず、自動的に値がコピーされます。
3.2 参照と借用
所有権を渡さずに、値の読み取りや変更の権限だけを一時的に貸し出す仕組みを「借用(Borrowing)」と呼び、借用するために作るポインタを「参照(Reference)」と呼びます。
不変参照(&T)
アンパサンド & を付けると、所有権を奪わずに値を参照できます。参照先は読み取り専用です。
fn calculate_length(s: &String) -> usize {
s.len()
} // s は参照なので、スコープを抜けても元の値は破棄されない
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 で参照を渡す
println!("「{}」の長さは {} です", s1, len); // s1 は依然として有効
}
可変参照(&mut T)
参照先の内容を変更したい場合は、&mut を使って可変参照を作ります。
fn append_world(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut greeting = String::from("hello");
append_world(&mut greeting);
println!("{greeting}"); // "hello, world!"
}
借用規則(コンパイラが保証する安全性の鉄則)
Rust では、データ競合や参照の無効化を防止するため、以下の規則がコンパイル時に厳密にチェックされます。
- あるデータに対して、不変参照(
&)は同時にいくつでも存在できる。 - あるデータに対して、可変参照(
&mut)は同時に 1 つしか存在できない。 - 不変参照と可変参照を同時に存在させることはできない。
誰かがデータを読み取っている間は誰も変更できず、誰かが変更している間は他の誰も読み書きできないという排他制御が、言語レベルで自動保証されます。
3.3 スライス型(Slice)
スライスは、コレクション全体ではなく、その中の一連の連続した要素を参照する型です。
文字列スライス(&str)
String は可変長のヒープ確保文字列ですが、&str はその一部または全体を指し示す不変の文字列スライスです。
#![allow(unused)]
fn main() {
let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..11];
println!("{hello} {world}");
}
関数の引数に文字列を受け取る際は、&String ではなく &str を指定するのが Rust の標準です。これにより、String の参照と文字列リテラル("...")の両方を柔軟に受け取ることができます。
3.4 ライフタイムの基礎
ライフタイムとは、参照が有効である期間(スコープ) のことです。
Rust のコンパイラ(ボローチェッカー)は、すでに破棄されたメモリを指す不正なポインタ(ダングリング参照)が生まれないか、ライフタイムを検証します。
#![allow(unused)]
fn main() {
// ライフタイム注釈の例
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
}
'a という記号がライフタイム注釈です。この記述は、「戻り値の参照は、引数 x と y の両方が生存している期間(より短い方の期間)と同じだけ有効である」ことをコンパイラに伝えます。
多くの日常的なコードでは、コンパイラの「ライフタイム省略規則」が働くため手動での記述は不要ですが、構造体に参照を保持させる場合や複数の参照を返す関数では必要になります。