Rustの所有権とは、Rust言語を学ぶ上で避けては通れない、けれどちょっぴり手ごわい仕組みです。
この記事では、Rustのプログラミングで核となる「所有権」システムの基本、つまり「ムーブ」「コピー」「借用」といったルールが、なぜ存在し、どのように機能するのかを、初心者の方にも「なるほど!」と思っていただけるように、図やサンプルコードを使いながら解説していきます。
「なんか難しそう…」と感じている方も、読み終わるころにはきっと、「そういうことか!」とスッキリしているはず。
この記事で学べること
- Rustの所有権がどんな考え方なのか
- 所有権があることで得られるいいこと(メモリ安全性!)
- 所有権の基本となる3つのルール
- 値が移動する「ムーブ」の仕組み
- 値が複製される「コピー」の条件
- 所有権を貸し借りする「借用」のルール(不変と可変)
Rustの所有権とは?なぜ他の言語と違うの?
さて、プログラミング言語には、作ったデータ(メモリ上の値)をいつ、どのように片付けるか(解放するか)という管理方法があります。
例えば、JavaやPythonには「ガベージコレクタ(GC)」という自動お掃除機能がありますし、C言語やC++ではプログラマが自分で「ここを片付けて!」と指示(メモリ解放)を出しますね。
Rustは、そのどちらでもない、独自の「所有権」という仕組みを採用しています。データには「所有者」がいて、その所有者がいなくなったら(スコープを抜けたら)自動的にデータが片付けられる、という考え方です。
なぜRustはこんな仕組みを持っているのでしょうか?主な理由は、「メモリ安全性」と「実行速度」を両立させるためです。
GCは楽ですが、いつ動くか分からず、時々プログラムを止めちゃうことがあります。手動管理は速いですが、うっかりミスでバグ(メモリリークや不正アクセスなど)を生みやすい…。
Rustの所有権は、コンパイル時(プログラムを実行する前)にメモリに関するルール違反がないか厳しくチェックすることで、実行時の速度を犠牲にせず、かつ安全なプログラムを実現しようとしているのです。
ちょっと厳しいけど、頼りになる用心棒みたいなイメージでしょうか!
所有権を理解するメリット - メモリ安全性をコンパイル時に保証
所有権システムの一番いい点は、なんといっても「メモリ安全性」をプログラムの実行前に保証してくれることです。
メモリに関するバグは、発見が難しく、プログラムが突然落ちたり、予期せぬ動作をしたりする原因になります。例えば、以下のような問題を未然に防いでくれます。
ダングリングポインタ
メモリが解放された後も、その場所を指し示してしまうポインタ(参照)。もう誰も住んでいない住所を訪ねるようなものです。Rustでは所有権ルールで、このような状況が起こらないようにしています。データ競合
複数の場所から同じデータに同時にアクセスし、少なくとも一方が書き込みを行おうとすると発生する問題。複数人が同時に同じノートに書き込もうとしてぐちゃぐちゃになるイメージです。Rustの借用ルール(後で説明します)で、これを防ぎます。二重解放
同じメモリ領域を二度解放しようとしてしまうエラー。これも所有権ルール(ムーブ)で防げます。これらのチェックをコンパイル時に行うため、実行時にメモリ関連のエラーで悩まされることが格段に減ります。バグ修正の時間を減らして、もっと楽しい機能開発に時間を使えるようになりますよ!
【重要】Rustの所有権3つの基本ルール
Rustの所有権システムは、たった3つのシンプルなルールに基づいています。この3つをしっかり押さえることが、所有権マスターへの第一歩です!
- 各値は、所有者と呼ばれる変数をただ1つ持つ。
- 所有者は、同時に1人(1つの変数)しか存在できない。
- 所有者がスコープから外れたら、値は破棄(drop)される。
「ふむふむ…」となんとなく分かったような、分からないような感じでも大丈夫。これから一つずつ、具体的なコード例と一緒に見ていきましょう。
【ルール1】全ての値には「所有者」がいる
Rustでは、変数束縛(変数に値を代入すること)を行うと、その変数が値の「所有者」になります。
fn main() { let s = String::from("こんにちは"); // ここで変数`s`が"こんにちは"という文字列データの所有者になる println!("{}", s); } // ここで`s`がスコープを抜けるので、`s`が所有していた文字列データは破棄される
上のコードでは、`String::from("こんにちは")` で作られた文字列データ(メモリ上のどこかに存在する)の所有者は変数 `s` です。他の変数が同時に所有者になることはありません。
【ルール2】所有者はただ一人!値の移動「ムーブ」
2つ目のルール「所有者は同時に1人だけ」が、Rustの面白い動き「ムーブ」を生み出します。
特に、`String` のような、データをヒープ領域(プログラム実行中にサイズが変わる可能性のあるデータを置く場所)に持つ型で顕著です。
fn main() { let s1 = String::from("hello"); let s2 = s1; // ここで所有権が s1 から s2 へ「ムーブ」する // println!("s1 is: {}", s1); // これはコンパイルエラー!s1はもう所有者ではない println!("s2 is: {}", s2); // s2が所有者なので、これはOK }
上のコードでは、`let s2 = s1;` という行で何が起こるかというと、`s1` が持っていた文字列データへの所有権が、まるごと `s2` に移動します。引っ越しみたいなものですね。
その結果、元の変数 `s1` は無効になり、もう使うことができません。もし使おうとすると、コンパイラが「おいおい、`s1` はもう空っぽだぞ!」とエラーを出して教えてくれます。
なぜこんな動きをするのでしょう?
もし所有権が移動せず、`s1` も `s2` も同じデータを指していたら、プログラムの最後で両方の変数がデータを解放しようとして「二重解放」エラーが起きてしまいます。ムーブは、それを防ぐための賢い仕組みなのです。
【ルール3】スコープを抜けたらサヨナラ(値の破棄)
最後のルールはシンプルです。変数が定義された「スコープ」(通常は `{}` で囲まれた範囲)を抜けると、その変数は無効になります。
そして、変数が所有していた値も自動的に破棄され、メモリが解放されます。Rustが裏で `drop` という特別な関数を呼んでくれるのです。
fn main() { // main関数のスコープ開始 { // 内側のスコープ開始 let message = String::from("ないしょ話"); // messageはこのスコープ内でのみ有効 println!("{}", message); } // 内側のスコープ終了。messageはここでスコープを抜け、所有していた文字列データはdropされる // println!("{}", message); // エラー! messageはこのスコープではもう使えない let num = 100; // numはmain関数のスコープで有効 println!("{}", num); } // main関数のスコープ終了。numが所有していた値もここでdropされる( যদিও i32のような型は少し違う挙動)
スコープを意識することは、Rustプログラミングにおいて非常に基本となります。変数がいつまで使えて、いつメモリが解放されるのかを把握する助けになります。
所有権の例外?「コピー」される型とは
さて、先ほどのムーブの話で、「`String`のような型では」と言いました。実は、すべての型がムーブするわけではありません。
整数型(`i32`など)、浮動小数点数型(`f64`など)、ブーリアン型(`bool`)、文字型(`char`)といった、サイズが分かっていてスタック領域(関数呼び出し時に使われる一時的なメモリ領域)にまるごと置けるような単純な型は、代入時に「コピー」されます。
fn main() { let x = 5; let y = x; // ここでは所有権のムーブではなく「コピー」が起きる println!("x = {}, y = {}", x, y); // xもyも有効!どっちも使える }
この場合、`let y = x;` を実行しても、`x` は無効になりません。`x` の値 `5` が単純にコピーされて `y` に割り当てられるだけです。
これらの型は `Copy` トレイト(型の特性を示す目印のようなもの)を持っているため、コピーが可能です。スタックに置かれるデータはコピーのコストが非常に安いので、ムーブではなくコピーする方が効率的な場面が多いのです。
`String` のようにヒープにデータを持つかもしれない型は、コピーのコストが高くなる可能性があるので、デフォルトではムーブになります。
所有権を貸し出す「借用」とは?(参照と不変・可変)
値を関数に渡したり、別の場所で使いたいけれど、所有権は渡したくない…そんな場面はよくあります。
関数の処理が終わった後も、元の場所でその値を使い続けたいですよね。そんなときに使うのが「借用(Borrowing)」です。
借用は、値の所有権を移動させる代わりに、その値への「参照」を貸し出す仕組みです。参照は、値そのものではなく、値がメモリ上のどこにあるかを示すアドレスのようなものだと考えてください。
「ちょっと見せてね」「ちょっと使わせてね」とお願いするイメージです。参照は `&` 記号を使って作ります。
fn main() { let s1 = String::from("貸してください"); let len = calculate_length(&s1); // s1の所有権を渡さず、参照(&s1)を貸し出す println!("文字列 '{}' の長さは {} です。", s1, len); // s1はまだ有効!所有権は移動していない } fn calculate_length(s: &String) -> usize { // 文字列スライス(&String)を借りる s.len() } // s はここでスコープを抜けるが、参照しているだけなので何も解放しない
`calculate_length` 関数は `s1` の所有権を受け取るのではなく、`&String` という「`String`への参照」を受け取ります。
関数内で `s1` の中身(長さ)を見ることはできますが、所有権は `main` 関数に残ったままです。だから `main` 関数で `println!` を実行しても `s1` は有効なままなのです。便利ですね!
借用には、さらにルールがあります。「不変の借用」と「可変の借用」です。
ルールを守る① - 不変の借用(&T)は複数可能
値を変更しない、ただ読み取るだけの参照を「不変の参照」といい、`&T` (Tは型)と書きます。不変の参照は、同じ値に対して同時に複数作ることができます。
fn main() { let s = String::from("みんなで見よう"); let r1 = &s; // 不変の参照1 let r2 = &s; // 不変の参照2 println!("r1 = {}, r2 = {}", r1, r2); // OK! 複数の不変参照は共存できる }
これは安全です。だって、誰もデータを書き換えないのですから、複数人が同時に見ていても問題は起こりません。
ルールを守る② - 可変の借用(&mut T)は一つだけ
値を変更する可能性のある参照を「可変の参照」といい、`&mut T` と書きます。
ここが注意点!可変の参照は、特定のスコープ内で、同じ値に対してただ一つしか存在できません。さらに、可変の参照が存在している間は、その値への不変の参照も作ることはできません。
fn main() { let mut s = String::from("書き換えるよ"); // 可変にするには `mut` が必要 let r1 = &mut s; // 可変の参照1 (OK) // let r2 = &mut s; // エラー!同時に2つの可変参照は持てない // let r3 = &s; // エラー!可変参照(r1)があるときは不変参照も持てない r1.push_str("!"); // 可変参照を使って値を変更 println!("{}", r1); // OK } // r1 はここでスコープを抜けるので、これ以降ならまた参照を作れる
なぜこんなに厳しいのでしょう? もし複数の場所から同時にデータを書き換えられたら、どんな結果になるか予測できませんよね(データ競合)。
Rustは、このルールをコンパイル時に強制することで、データ競合の可能性を完全に排除しているのです。「変更する権利は、常に一人だけ!」と覚えておきましょう。
【まとめ】Rustの所有権を理解して、安全で効率的なコードへ!
ふぅ、お疲れ様でした!Rustの所有権、ムーブ、コピー、そして借用(不変・可変)の基本について見てきました。
最初はちょっと戸惑うかもしれませんが、この所有権システムこそが、Rustが目指すメモリ安全性とパフォーマンスの両立を実現するための土台となっています。ルールは厳格ですが、コンパイラが常にチェックしてくれるので、実行時のメモリ関連の心配事を大幅に減らすことができます。
所有権のルールに慣れてくると…
- コンパイルエラーの意味が理解できるようになる!
- メモリを意識した、より効率的で安全なコードが書けるようになる!
- Rustの思想や設計の意図が見えてきて、もっとRustが好きになる!
…はずです!所有権はRustを使いこなす上で避けて通れない道ですが、一度理解してしまえば、これほど頼りになる仕組みはありません。
もし、もっと深く知りたくなったら、ぜひRustの公式ドキュメント(The Rust Programming Language、通称 The Book)の所有権の章を読んでみてください。今回の内容をより深く、網羅的に学ぶことができますよ。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。