前回は、Rustのプロジェクト管理ツール「Cargo」の基本と、変数の定義、そして「不変性」の概念について学びました。
さて、今回のテーマは、Rust学習者が最初にぶつかるであろう「最大の壁」と言われる概念です。その名も「所有権(Ownership)」と「借用(Borrowing)」。
「変数を使おうとしたら『値が移動しています』とコンパイラに怒られた…」「なんだかよく分からないエラーが出る!」
もしあなたがそんな経験をしているなら、それはまさに所有権と借用の概念に触れている証拠です。
ここがRust学習の最大の難所であることは間違いありませんが、ご安心ください。この記事を最後まで読めば、以下の「学習のゴール」が達成され、コンパイラはあなたの強力な味方になります!
それでは、Rustの奥深さを一緒に探求していきましょう!
Rustが他の多くの言語と大きく異なる点、それは「所有権システム」にあります。これは、メモリを安全に管理するためのRust独自の仕組みで、コンパイル時にメモリの安全性を保証する画期的なアプローチです。
他の言語では、ガベージコレクション(GC)が自動的に不要なメモリを解放したり、プログラマが手動でメモリを確保・解放したりします。しかしRustでは、所有権システムによって、コンパイル時にどのタイミングでメモリが解放されるかを決定します。これにより、GCによる実行時コストなしに、メモリリークやヌルポインタ参照といったメモリ関連のエラーを未然に防ぎます。
所有権システムには、以下の3つのシンプルなルールがあります。
これらのルールが、一見するとシンプルでありながら、Rustの強力な安全性を支えています。
具体的なコードで見ていきましょう。
fn main() {
// 1. "hello"という文字列データは、変数s1に「所有」される
let s1 = String::from("hello");
// s1がスコープを抜けるとき、"hello"のメモリは解放される
} // ここでs1はスコープを抜けるので、s1が所有していた"hello"のメモリはドロップされるRustでは、String::from() のようにヒープ領域にデータを確保する型(String、Vecなど)に対して、この所有権の概念が特に重要になります。スタック領域に確保されるプリミティブ型(i32、boolなど)とは少し挙動が異なりますが、その違いは後述します。
所有権のルールの中でも特に重要なのが「一度に所有者は一つだけ」という原則です。これが、Rustでしばしば遭遇する「値が移動しました」エラーの原因となります。
まずはコードを見てみましょう。
fn main() {
let s1 = String::from("hello"); // s1が"hello"の所有者
let s2 = s1; // ★ここで何が起こるか?
println!("s1: {}", s1); // コンパイルエラーになる!
println!("s2: {}", s2); // これはOK
}このコードを実行しようとすると、println!(“s1: {}”, s1); の行でコンパイラが以下のようなエラーメッセージを出します。
error[E0382]: borrow of moved value: `s1`
--> src\main.rs:6:24
|
2 | let s1 = String::from("hello"); // s1が"hello"の所有者
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |
4 | let s2 = s1; // ★ここで何が起こるか?
| -- value moved here
5 |
6 | println!("s1: {}", s1); // コンパイルエラーになる!
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
4 | let s2 = s1.clone(); // ★ここで何が起こるか?
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `study_variables` (bin "study_variables") due to 1 previous error「s1が移動された後で使われているよ!」というエラーです。これはまさに、「ムーブ(Move)」という現象が起きているためです。
Rustでは、s1 のような String 型の値を別の変数 s2 に代入すると、s1 が持っていた「所有権」が s2 に移動します。これにより、s1 はもうその値を「所有」していない状態になるため、以降 s1 を使おうとするとコンパイラに怒られるのです。
もし s1 の所有権が s2 に移動せず、両方が同じデータを所有している状態(浅いコピーのような状態)になったらどうなるでしょうか?
s1 と s2 の両方がスコープを抜ける際に、それぞれが同じメモリを解放しようとします。これは「二重解放(Double Free)」という深刻なメモリバグを引き起こす可能性があります。
Rustは、この二重解放のようなメモリ安全性の問題をコンパイル時に防ぐために、一度に一つの所有者しか存在しないというルールを徹底し、所有権の移動(ムーブ)を行うのです。
ここで、前回の記事で触れたi32のようなプリミティブ型の変数を思い出してください。
fn main() {
let x = 5;
let y = x; // ここでは何が起こる?
println!("x: {}, y: {}", x, y); // 両方とも問題なく使える
}このコードは問題なく動作し、xもyも両方使えます。なぜでしょうか?
これは、i32のような単純な値は、値を代入する際に「ムーブ」ではなく「コピー(Copy)」されるためです。これらの型は、Copyトレイトを実装しており、データをスタック上に保存するため、コピーが非常に高速で安価に行えます。
つまり、let y = x; の行で、xの値5がまるごとコピーされ、yという新しい変数に格納されます。そのため、xもyもそれぞれの5という値の所有者となるため、両方とも問題なく使えるのです。
一般的に、固定サイズでスタックに格納できるようなシンプルな型(整数型、浮動小数点型、ブール型、char、Tupleにプリミティブ型しか含まれない場合など)はCopyトレイトを実装しています。一方、サイズが不定でヒープにデータを保持するStringやVecなどはCopyトレイトを実装していません(実装できない)。
所有権の概念は、関数への値の受け渡しにも影響します。関数に値を渡す際、その値の所有権がどのように移動するかを見ていきましょう。
fn main() {
let s = String::from("hello"); // sが"hello"の所有者
takes_ownership(s); // ★ここでsの所有権が関数に移動する
println!("{}", s); // ここでコンパイルエラーになる!
// sはもう所有権を持たないため使えない
}
fn takes_ownership(some_string: String) { // some_stringがsの所有権を受け取る
println!("{}", some_string);
} // ここでsome_stringはスコープを抜けるので、
// some_stringが所有していた"hello"のメモリはドロップされるこのコードも、println!(“{}”, s); の行で「sが移動された後で使われているよ!」というエラーになります。
takes_ownership 関数に s を渡した時点で、s の所有権は関数の引数 some_string に移動してしまいます。そのため、関数呼び出し後には s は無効となり、使えなくなるのです。
関数から値を返す場合も同様に所有権が移動します。
fn main() {
let s1 = gives_ownership(); // gives_ownershipから返された値の所有権をs1が受け取る
let s2 = String::from("world"); // s2が"world"の所有者
let s3 = takes_and_gives_back(s2); // s2の所有権が関数に移動し、
// 関数から返された値の所有権をs3が受け取る
println!("s1: {}, s3: {}", s1, s3);
println!("s2: {}", s2); // s2は所有権を失っているのでエラー
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 所有権を呼び出し元に返す
}
fn takes_and_gives_back(a_string: String) -> String { // a_stringが所有権を受け取る
a_string // 受け取った所有権をそのまま呼び出し元に返す
}このように、Rustの所有権システムは、関数呼び出しのたびに値の所有権の移動を強制します。これにより、メモリのライフサイクルがコンパイル時に明確になり、実行時のメモリ管理のオーバーヘッドをなくしているのです。
しかし、これは同時にプログラマにとっては不便な点でもあります。「ただ値を見たいだけなのに、所有権を渡してしまうと元の変数が使えなくなる…」という問題が発生します。
そこで登場するのが、次のセクションで学ぶ「借用」です。
「値を参照したいだけで、所有権は渡したくない!」そんなときに使うのが「借用(Borrowing)」です。
借用とは、所有権を移動させずに、値への「参照(Reference)」を関数に渡すことです。
参照は、& 演算子を使って作成します。
fn main() {
let s = String::from("hello"); // sが"hello"の所有者
// &s を渡すことで、sの所有権は移動しない
// calculate_lengthはsへの「参照」を受け取る
let len = calculate_length(&s);
println!("文字列 '{}' の長さは {} です。", s, len); // sは引き続き使える!
}
fn calculate_length(some_string: &String) -> usize { // 引数は&String型(Stringへの参照)
some_string.len() // 参照を通して値を使う
} // some_stringは参照なので、スコープを抜けても何もドロップしないこのコードは問題なくコンパイル、実行されます!
&s とすることで、s の所有権はそのまま main 関数内に残り、calculate_length 関数には s の値への「参照」が渡されます。関数内でその参照を使って値にアクセスできますが、所有権は持っていないため、関数を抜けても元の s は無効になりません。
これで「値が移動しました」エラーから解放され、コンパイラと和解の一歩を踏み出しましたね!
先ほどの例で使った &String は「不変参照(Immutable Reference)」と呼ばれます。不変参照を通しては、参照している値を変更することはできません。
fn main() {
let s = String::from("hello");
change(&s); // エラーになる!
}
fn change(some_string: &String) {
// some_string.push_str(", world!"); // コンパイルエラー!
// `some_string` は不変参照なので変更できない
}値を変更したい場合は、「可変参照(Mutable Reference)」を使います。可変参照は &mut 演算子を使って作成します。
fn main() {
let mut s = String::from("hello"); // s自体も可変にしておく必要がある
change(&mut s); // 可変参照を渡す
println!("{}", s); // "hello, world!"
}
fn change(some_string: &mut String) { // 引数も&mut String型
some_string.push_str(", world!"); // 可変参照を通して値を変更できる
}ポイント:
可変参照を使えば値を変更できるようになりましたが、ここには重要な制限があります。それが、Rustのメモリ安全性を強力に保証する「借用ルール」です。
借用ルールは以下の通りです。
このルールは、データ競合(Data Race)を防ぐために非常に重要です。データ競合とは、以下の3つの条件が全て揃ったときに発生する状況で、未定義の動作を引き起こす深刻なバグの原因となります。
Rustの借用ルールは、コンパイル時にこれらのデータ競合を完全に排除します。
具体的なコード例を見てみましょう。
fn main() {
let s = String::from("hello");
let r1 = &s; // 不変参照1
let r2 = &s; // 不変参照2
println!("{}, {}", r1, r2); // 問題なく両方使える
// r1とr2はここより先は使われない
}不変参照は、データを読み取るだけで変更しないため、同時にいくつ存在しても安全です。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // 可変参照1
let r2 = &mut s; // ★コンパイルエラー!
// 同時に複数の可変参照は作れない
println!("{}, {}", r1, r2); // エラーになる
println!("{}", r1);
}コンパイルエラー: cannot borrowsas mutable more than once at a time
「sを同時に複数回可変で借用することはできません」という明確なメッセージです。
もし複数の可変参照が同時に存在すると、どちらか一方がデータを変更している間に、もう一方がそのデータを読み書きしてしまうデータ競合のリスクが生じるため、Rustはこれを許しません。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不変参照
let r2 = &s; // 不変参照 (r1とr2はOK)
// let r3 = &mut s; // ★コンパイルエラー!
// 不変参照(r1, r2)が存在する間に可変参照は作れない
println!("{}, {}", r1, r2); // r1とr2はここで使われなくなる
let r3 = &mut s; // ここではOK (r1, r2はもう使われないため)
println!("{}", r3);
}コンパイルエラー: cannot borrowsas mutable because it is also borrowed as immutable
「sが不変で借用されているため、可変で借用することはできません」というエラーです。
不変参照が存在するということは、そのデータは変更されないと保証されている状態です。その保証がある間に可変参照が作られ、データが変更されてしまうと、不変参照を使っている側が意図しない動作をする可能性があるため、Rustはこれも許しません。
これらの借用ルールによって、Rustは実行時に発生しがちなメモリ関連のバグ(特に並行処理におけるデータ競合)をコンパイル時に確実に取り除くことができます。これがRustの最大の特徴であり、他の言語にはない強力な安全性と信頼性を生み出す秘訣なのです。
所有権と借用の概念を理解したところで、もう一つ便利な概念である「スライス(Slice)」についても見ていきましょう。
スライスは、コレクション(配列や文字列など)の連続した一部分への参照です。所有権を持たずに、データの一部を安全に参照したい場合に非常に役立ちます。
Rustにおける文字列は大きく分けて String と &str の2種類があります。
String から &str を作成してみましょう。
fn main() {
let s = String::from("hello world");
// "hello" の部分へのスライス
let hello = &s[0..5];
// "world" の部分へのスライス
let world = &s[6..11];
println!("hello: {}", hello); // hello: hello
println!("world: {}", world); // world: world
// s全体へのスライスも作れる
let whole_string = &s[..];
println!("whole_string: {}", whole_string); // whole_string: hello world
}スライスは &[開始インデックス..終了インデックス] の形式で指定します。終了インデックスは含まれません。
インデックスはバイト単位なので、日本語のようなマルチバイト文字を含む文字列をスライスする場合は注意が必要です(UTF-8文字境界を考慮しないとパニックする可能性があります)。
スライスを使うことで、文字列や配列の一部分を安全に扱えます。特に便利なのは、スライスが参照している元のデータが変更されたり解放されたりしないことをRustコンパイラが保証してくれる点です。
例えば、文字列の最初の単語を返す関数を考えてみましょう。
fn first_word(s: &String) -> &str { // Stringへの参照を受け取り、Stringスライスを返す
let bytes = s.as_bytes(); // Stringをバイト配列に変換
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' { // スペースが見つかったら
return &s[0..i]; // その位置までのスライスを返す
}
}
&s[..] // スペースが見つからなければ文字列全体のスライスを返す
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // wordはsへのスライスを参照
// s.clear(); // ★もしここでsをクリアしたらどうなる?
// wordが指すデータが無くなってしまう。
// しかし、Rustはこれを許さない!
println!("最初の単語: {}", word); // s.clear()がなければ "最初の単語: hello"
}s.clear() のコメントアウトを外すと、コンパイルエラーが発生します。
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:20:5
|
17 | let word = first_word(&s);
| -- immutable borrow occurs here
...
20 | s.clear(); // ここでsをクリアすると
| ^^^^^^^^^ mutable borrow occurs here
21 | println!("最初の単語: {}", word);
| ---- immutable borrow later used hereこれはまさに借用ルール「不変参照がある間は、可変参照は作れない」が適用されている例です。
word が s への不変参照 (&str) を持っている間は、s を変更する操作 (s.clear()) は許可されません。これにより、スライスが無効なデータを指してしまう「ダングリングポインタ(Dangling Pointer)」のような問題をコンパイル時に防いでくれるのです。
この安全性が、Rustが他の言語と一線を画す強力な特徴の一つです。
文字列スライスと同様に、配列の一部への参照である配列スライスも使えます。
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // インデックス1から3(2と3番目の要素)へのスライス
assert_eq!(slice, &[2, 3]); // スライスは配列リテラルと比較可能
println!("配列スライス: {:?}", slice); // 配列スライス: [2, 3]
}これまでの内容を振り返ると、Rustがガベージコレクション(GC)なしにメモリ安全性を保証できる理由がはっきりと見えてきます。
これら全ての仕組みが連携し、コンパイル時にプログラムのメモリ安全性を静的に検証します。もしルールに違反するコードを書けば、コンパイラが容赦なくエラーを出し、バグの芽を摘んでくれます。
まるで、コンパイラが最高のコードレビューアのように、常にあなたのコードの安全性をチェックしてくれるのです。最初は厳しく感じるかもしれませんが、一度この仕組みを理解してしまえば、Rustがいかに強力で信頼性の高い言語であるかを実感できるはずです。
「Rustコンパイラは、あなたの最強の味方!」
今回はRust学習の最大の壁と言われる「所有権」と「借用」について、徹底的に掘り下げてきました。
これらの概念は、Rustの性能と安全性を両立させるための核心であり、使いこなせるようになれば、あなたはRustプログラマとして大きく成長したと言えるでしょう。
最初はコンパイラのエラーメッセージに戸惑うかもしれませんが、それはRustがあなたをより良い、より安全なコードへと導いてくれている証拠です。諦めずに、ぜひこの強力なシステムをマスターしてください!