前書き
筆者はFEメインのエンジニアなのですが、参画中のプロジェクトでRustが使われているため、BE側の解像度を上げるべき最近キャッチアップを始めました。中でもRustの所有権は重要かつよく聞く言葉かなと思うので、一緒に理解を深めていければ思います。
なお、ドキュメンテーションを確認しながら書いているわけではないので、一部間違った情報が書かれている可能性がありますので参考程度にお読みください。
所有権?何それ食べれるの?
所有権は食べれません。 所有権とは、「データをアクセス・変更する権利」の事で、メモリ管理を適切に行うための仕組みです。Rustではコンパイル時に所有権が適切に管理されているかチェックが入るため、いち早く不適切なメモリ管理を阻止できます。実際にはスタックとヒープのメモリ領域やポインタなどが裏側で使われていますが、ひとまず抽象的な理解から入りましょう。
「所有権」のみに焦点を当てて例えると、家の鍵と所有権を持っている事と一緒です。
家の場合:
家の鍵と所有権を持っていると、
- 鍵を持っているので家に入れる
- 家を所有してるので内装を変えても怒られない
- 他の人に家を譲渡したり、内装を変える権利を与えられる
反対に、家の鍵と所有権を持っていないと、
- そもそも鍵がないので家に入れない
- 内装を変えたら犯罪
- 他の人に譲渡するなんてもってのほか
データの場合:
データの場合も一緒で、データの所有権を持っていると、
- そのデータにアクセスできる
- 値を編集できる
- データの所有権や編集権を他の変数に移動できる
反対に、データの所有権を持っていないと、
- そもそもデータにアクセスできない
- 値を編集できない
- 他の変数に所有権を移動したり編集権限を与える事ができない
コードで見てみる
ここからは実際のコードを見た方が分かりやすいと思うので、実際のコードを見てみましょう。
① 所有権を「持っている」場合
fn main() {
// 変数s は String データの「所有者」
let mut s = String::from("マイホーム");
// ✅ 所有権を持っているので中身を見ることができる
println!("{}", s);
// ✅ 所有権を持っていて、かつ mut なので中身を編集できる
s.push_str("(リフォーム済み)");
// ✅ 所有権を他の変数に「移動」できる(代入によって所有権は移動される)
let new_owner = s;
}
② 所有権を「持っていない」場合
fn main() {
let data = String::from("データ");
// 別の変数に代入する事によって所有権がdataからnew_dataに「移動」される
let new_data = data;
// ❌ 変数dataは値の所有権を失っているため、中身を見ようとするとコンパイルエラーが出る
println!("{}", data);
// ❌ 同じ理由で、値を編集しようとするとコンパイルエラーが出る
data.push_str("変更");
}
実際には何が行われてるの?スタックとヒープ
Rustに限らずですが、上記のコードを実行時にはメモリ領域であるスタックとヒープが使われています。簡単に言うと、スタックは拡張不可の固定のメモリ領域でヒープは拡張可能な領域です。多くの場合スタックにデータのメタデータ(アドレス、長さ、容量など)が保存されて、実際のデータがヒープ領域に保存されます。(この記事でスタック・ヒープの概要が分かりやすく解説されているので是非読んでみてください。)
先程の「② 所有権を「持っていない」場合」のコードでメモリ領域内で何が起こっているかを順に追っていきましょう。
- 変数定義時
let data = String::from("データ");
- スタック領域に変数データメタデータに保存される
- 実際の値の「データ」文字列はヒープ領域に保存される
- 所有権移動時
let new_data = data;
- 変数dataからnew_dataに所有権が移動される
- ヒープ領域にあるデータは変わらずポインタが変わるだけ
変数dataからは所有権がなくなり、データのアクセス・変更やメモリ解放もできなくなります。dataが改変不能の明確な不要データになる事で、以下の問題を防げます。
- メモリリーク
- 二重解放
- 解放後使用
- データ競合
最終的に変数がスコープアウト(関数の区切りが終了)すると、メモリが解放されます。スコープアウトについてはまた別記事で。。
なぜ所有権が必要なの?
まだまだ所有権に関して説明する事はありますが、基本の概念を理解したタイミングでRustになぜこんな仕組みがあるのか立ち返っておきましょう。
所有権の仕組みが存在する意義は、メモリ管理仕組み化とそれに伴うアプリケーションの高速化です。
既存のメモリ管理の仕組みの問題
「メモリが無駄遣いされていると、RAMが圧迫され、アプリケーションの処理速度が落ちる」という前提の元、C/C++ 言語では明示的にメモリ管理をする仕組みがあったり、Java/Python/GoなどはGC(Garbage Collector)を使用しています。
C/C++の明示的なメモリ管理の問題:
- 明示的でメモリを細かく管理できる反面、メモリ漏れや解放後のアクセスが起きやすい
- コンパイル時にこれらをキャッチできないためランタイムでクラッシュしたり、気付かないうちにアプリが遅くなっていたりする
- デバッグが難しい
GCの問題:
- GCがランタイム時に実行される追加オーバーヘッド
- 解放タイミングなどの挙動が予測しづらい
- 複雑かつブラックボックスなのでデバッグが難しい
コンパイル時に適切なメモリ管理を強制するRustの「所有権」と解決方法
Rustの場合、コンパイル時に所有権のチェックを行う事でランタイムでアプリが実際に動き始める前に適切なメモリ管理を強制します。
- 適切なメモリ管理のチェックが仕組み化されているため、ランタイム前に脆弱性に気付け、かつ修正を強制される
- GCを使用しないためその分のオーバーヘッドが減る
- 複雑とはいえ規則的なため、ブラックボックス化しない
静的言語の型チェックのメモリ管理版とイメージするのが良さそうです。実際にRustをバリバリプロダクションで書いている訳ではないので分かりませんが、JS(動的)からTS(静的)へ移行した時は感動があったので似た感覚を想像しています。。
これでRustの所有権の仕組みにより、安全で高速なアプリケーション実装ができる事がお分かり頂けたでしょうか?
終わりに
今回はRustの「所有権」の簡単な解説でしたが、まだまだ借用や、ライフタイムなどの様々Rustの機能があります。とはいえざっくりとでもRustがなぜ安全で高速だと言われるのか、少しでも理解が深まっているたら幸いです。他のRustの機能の解説を別記事で今後続けたいと思います。ありがとうございました!

