Rustへようこそ。ここからがRustの学習における最大の山場であり、同時に最大の特徴である「所有権(Ownership)」システムについて解説します。
他のプログラミング言語(Python, Java, C++など)の経験がある方にとって、この概念は最も馴染みがなく、直感に反する場合があるかもしれません。しかし、所有権こそがガベージコレクション(GC)なしでメモリ安全性を保証するRustの魔法の源です。
本章では、参照(借用)の概念に入る前に、基礎となる「所有権の移動(ムーブ)」とメモリ管理の仕組みを理解します。
所有権を理解するには、計算機科学の基礎である「スタック(Stack)」と「ヒープ(Heap)」の違いを意識する必要があります。多くの高水準言語ではこれを意識しなくてもコードが書けますが、システムプログラミング言語であるRustでは、値がどこに配置されるかが言語の挙動に直結します。
i32, bool, 固定長配列など)が置かれます。String, Vecなど)が置かれます。Rustの所有権システムは、主にこの「ヒープデータの管理」を自動化・安全化するためのルールセットです。
Rustのコンパイラは、以下の厳格なルールに基づいてメモリ管理を行います。これを破るとコンパイルエラーになります。
所有権のルール
- Rustの各値は、所有者(Owner)と呼ばれる変数を持つ。
- いかなる時も、所有者は一人だけである。
- 所有者がスコープから外れると、値は破棄(ドロップ)される。
まずは単純なスコープの例を見てみましょう。これは他の言語とほぼ同じです。
fn main() {
{ // s はここで宣言されていないので無効
let s = "hello"; // s はここから有効になる
println!("{}", s); // s を使用できる
} // ここでスコープ終了。s は無効になる
}hello
ここで重要なのは、スコープを抜けた瞬間にRustが自動的にメモリを解放する処理(drop関数)を呼び出すという点です。これはC++のRAII (Resource Acquisition Is Initialization) パターンと同様です。
ここからがRust独自の挙動です。データ型によって、「代入」の意味が変わります。
整数型のような単純な値は、サイズが固定でスタック上にあります。この場合、変数を代入すると値がコピーされます。
let x = 5; let y = x; // xの値(5)がコピーされてyに入る // ここでは x も y も両方有効
String型のようにヒープにメモリを確保する型を見てみましょう。
let s1 = String::from("hello");
let s2 = s1;
C++などの経験があると、これは「ポインタのコピー(浅いコピー)」あるいは「ディープコピー」のどちらかだと思うかもしれません。 しかしRustでは、これは所有権の移動(Move)とみなされます。
s1 はヒープ上の "hello" を指すポインタ、長さ、容量をスタックに持っています。s2 = s1 を実行すると、スタック上のデータ(ポインタ等)のみが s2 にコピーされます。s1 を無効とみなします。なぜなら、もし s1 も有効なままだと、スコープを抜けた時に s1 と s2 が同じヒープメモリを2回解放しようとしてしまう(二重解放エラー)からです。
以下のコードを実行して確認してみましょう。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s1 から s2 へ移動(ムーブ)
// s1 はもう無効なので、以下の行はコンパイルエラーになる
// println!("{}, world!", s1);
println!("s1 is moved.");
println!("s2 is: {}", s2);
}s1 is moved. s2 is: hello
もし println!("{}", s1) のコメントアウトを外すと、value borrowed here after move という有名なコンパイルエラーが発生します。
もしヒープ上のデータも含めて完全にコピーしたい場合は、明示的に .clone() メソッドを使用します。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープデータごとコピーする(コストは高い)
println!("s1 = {}, s2 = {}", s1, s2);
}s1 = hello, s2 = hello
関数に変数を渡す動作も、代入と同様に機能します。つまり、関数へ値を渡すと所有権が移動します(Copy型を除く)。
fn main() {
let s = String::from("hello"); // s がスコープに入る
takes_ownership(s); // s の値が関数にムーブされる
// ここで s はもう有効ではない!
let x = 5; // x がスコープに入る
makes_copy(x); // x も関数に移動するが、
// i32はCopyトレイトを持つので、
// この後も x を使って問題ない
} // ここで x がスコープアウト。s もスコープアウトだが、
// 所有権は既に移動しているので何も起きない。
fn takes_ownership(some_string: String) { // some_string に所有権が移る
println!("{}", some_string);
} // ここで some_string がスコープアウトし、`drop` が呼ばれる。メモリ解放。
fn makes_copy(some_integer: i32) { // some_integer に値がコピーされる
println!("{}", some_integer);
} // ここで some_integer がスコープアウト。何も起きない。hello 5
関数から値を返すことで、所有権を呼び出し元に戻すことができます。
fn main() {
let s1 = gives_ownership(); // 戻り値の所有権が s1 に移動
let s2 = String::from("hello"); // s2 スコープイン
let s3 = takes_and_gives_back(s2); // s2 は関数にムーブされ、
// 戻り値が s3 にムーブされる
println!("s1: {}, s3: {}", s1, s3);
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 所有権を呼び出し元に返す
}
// 文字列を受け取り、それをそのまま返す関数
fn takes_and_gives_back(a_string: String) -> String {
a_string // 所有権を返す
}s1: yours, s3: hello
clone() を使う。このシステムのおかげで、Rustは実行時のガベージコレクションによる停止を避けつつ、ダングリングポインタ(無効なメモリを指すポインタ)や二重解放のバグをコンパイル時に完全に防ぐことができます。
しかし、「関数に値を渡すたびに所有権がなくなってしまい、使えなくなる」のは不便だと感じたでしょう。値を一時的に関数に使わせたいだけなのに、いちいち所有権を返してもらうのは面倒です。
次章の「借用(Borrowing)」では、所有権を渡さずに値を参照する方法を学びます。
以下のコードは、s1 の所有権が s2 に移動してしまったためコンパイルエラーになります。s1 と s2 の両方を表示できるように修正してください(cloneを使用する方法と、新しい文字列を作る方法のどちらでも構いません)。
fn main() {
let s1 = String::from("Rust");
let s2 = s1;
println!("Original: {}", s1); // ここでエラー
println!("New: {}", s2);
}以下の process_string 関数は文字列を受け取って長さを出力しますが、戻り値がないため、呼び出し元で元の文字列が使えなくなってしまいます。
process_string を修正して、受け取った文字列の所有権を呼び出し元に返すようにし、main 関数でその後の処理ができるようにしてください。
fn main() {
let s = String::from("ownership");
// ここで s を渡して、処理後に戻ってきた所有権を new_s で受け取るように修正する
process_string(s);
// 修正後は以下のコメントを解除しても動作するようにする
// println!("String is still valid: {}", new_s);
}
// 戻り値の型と実装を修正してください
fn process_string(input: String) {
println!("Length is: {}", input.len());
}