Rustの所有権について、初心者でも理解しやすいようにわかりやすく整理してみました。
本記事では、基本的な所有権の説明から具体的な使い方まで、分かりやすいサンプルコードを交えて解説します。Rustの鍵となる所有権の概念を楽しく学びましょう!
Rustの所有権とは
所有権は、Rustの最もユニークな機能で、プログラムのある部分がリソース(主にメモリ)を所有することを表します。
Rustにはガベージコレクタがありません。その代わりに所有権を使っています。Rustでは、あるリソース(主にメモリ)の所有権がスコープから抜けると、そのリソースは自動的に解放されるようになっています。
所有権が重要な理由
所有権は、メモリ安全性を保証しつつ、ガーベジコレクタなしで高速な実行を可能にします。これにより、データ競合や無効なメモリへの参照を防ぐことができます。
今までのプログラミング言語では、メモリの確保と解放のために、さまざまな取り組みがなされてきました。
ガベージコレクタ(GC)付きの言語では、ガベージコレクタが使用されていないメモリを検知して、メモリから解放していました。ガベージコレクタにより、開発者はメモリの確保と解放について考慮する必要がなくなりました。しかし、ガベージコレクタは実行時のオーバーヘッドがかかりプログラムの実行速度を低下させてしまうことがあります。
また、他の取り組みとして、開発者自身が明示的にメモリを確保と解放をする言語もあります。しかし、メモリの確保と解放は完全に1対1に対応していなければならず、これを人手で実現することは難しく、しばしばバグの原因になっています。
そんな中、Rustでは所有権という概念を取り入れることでメモリの確保と解放をうまく取り扱えるようにしました。
Rustの所有権はどのように機能するか
Rustの所有権システムは以下の3つの規則に基づいて機能します。
- Rustの各値は「所有者」と呼ばれる変数と対応している
- 一度に存在できる所有者は1人だけ
- 所有者がスコープから外れたら値は破棄される
これを読んだだけでは意味がわからないと思うので、一つずつサンプルコードを交えながら説明します。
1. Rustの各値は「所有者」と呼ばれる変数と対応している
Rustの各変数では、値に対して所有権をもっています。
fn main() { let s1 = String::from("hello"); // 変数s1は"hello"という値の所有権をもっている let s2 = String::from("hi"); // 変数s2は"h1"という値の所有権をもっている }
2. 一度に存在できる所有者は1人だけ
Rustでは代入(=)や関数の引数や戻り値などで値を受け渡すと所有権が移動します。これにより、一度に存在できる所有者は1人だけにしています。
fn main() { let s1 = String::from("hello"); let s2 = s1; // 変数s1から変数s2に所有権の移動が発生(一度に存在できる所有者は1人だけ) // 所有権が移動しているので、変数s1を利用するとコンパイルエラーが起きる // println!("{}, world!", s1); // value borrowed here after move println!("{}, world!", s2); }
3. 所有者がスコープから外れたら値は破棄される
Rustでは、所有権はメモリの解放に使われています。値を所有している変数がスコープから外れると該当の値は破棄(メモリから解放)されます。
fn main() { let s1 = String::from("hello"); { let s2 = String::from("hi"); println!("{}, world!", s2); } // 値"hi"の所有者であるs2がスコープが外れるので、"hi"は破棄される println!("{}, world!", s1); } // 値"hello"の所有者であるs1がスコープが外れるので、"hello"は破棄される
このように、Rustでは値に対して所有権をもつことで、適切なタイミングでメモリを解放できるようにしています。また、所有権の仕組みがあることで、メモリ利用やメモリ解放の不整合をコンパイルエラーという形でコンパイル時にチェックすることもできるようになっています。
所有権の仕組みをより詳しく知りたい方は、公式の 所有権とは? - The Rust Programming Language 日本語版 を一読してみてください。Rustのプログラムがスタック領域とヒープ領域をどのように使いデータを保持し、所有権がどのように使われているかを理解することができます。
Rustの所有権のムーブと参照・借用
Rustで所有権を理解するには、「ムーブ(所有権の移動)」と「参照・借用(所有権は移動しない)」を理解する必要があります。
ムーブ(所有権の移動)
所有権が移動することをムーブと呼びます。所有権が移動すると、移動元の変数は無効化されます。
fn main() { let s1 = String::from("hello"); let s2 = s1; // 変数s1から変数s2に所有権の移動が発生、変数s1は無効化される // 所有権が移動しているので、変数s1を利用するとコンパイルエラーが起きる // println!("{}, world!", s1); // value borrowed here after move println!("{}, world!", s2); }
補足:整数値やブール値などの値はコピーになるので所有権は移動しない
初心者泣かせなのですが、整数値やブール値などの値はコピーされるので、所有権は移動しません。理由として、内部的にスタック領域に積まれているデータなのでコピーを行っているようです。詳細を知りたい方は 所有権とは? - The Rust Programming Language 日本語版を一読ください。
fn main() { let i1 = 5; let i2 = i1; // 値のコピーがされている。所有権は移動しない println!("i1 is {}", i1); // 値のコピーなので、変数i1を利用してもエラーにならない println!("i2 is {}", i2); }
参照・借用(所有権は移動しない)
参照は、ポインタと似ており、他のデータへのアクセスを提供します。参照は &
記号を使って作成されます。参照はデフォルトではイミュータブル(不変)であり、ミュータブル(可変)にするには&mut
を使います。
値を受け渡された側は、参照を使って他の部分から値を借用できます。&
で渡された場合は、借用している値を変更はできません。借用している値を変更したい場合は&mut
で渡す必要があります。
fn main() { let mut s1 = String::from("hello"); change(&mut s1); // &mut 参照を渡しているので書き込み可能。所有権は移動しない let len = calculate_length(&s1); // & で参照させているので読み取りのみ可能。所有権は移動しない println!("The length of '{}' is {}.", s1, len); // The length of 'hello, world' is 12. } fn change(some_string: &mut String) { some_string.push_str(", world"); } fn calculate_length(s: &String) -> usize { s.len() }
補足:ミュータブルな参照は(&mut
を利用)はスコープ内で1つしかもてない制約がある
ミュータブルな参照をスコープ内で複数利用するとコンパイルエラーになります。この制約により、コンパイル時にデータ競合を防ぐことができます。
fn main() { let mut s = String::from("hello"); // 同じスコープ内でsのミュータブルな参照を2つ宣言しているためコンパイルエラーが起きる let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); }
// error[E0499]: cannot borrow `s` as mutable more than once at a time // | // 22 | let r1 = &mut s; // | ------ first mutable borrow occurs here // 23 | let r2 = &mut s; // | ^^^^^^ second mutable borrow occurs here // 24 | // 25 | println!("{}, {}", r1, r2); // | -- first borrow later used here
Rustの所有権の具体的な使い方
Rustの所有権の具体的な使い方を、関数、構造体、列挙型、スライスで見ていきます。
関数
関数に値を渡すとき、所有権が移動(ムーブ)または借用(参照)されます。関数が値を返すときも、所有権が移動します。
fn main() { let s1 = String::from("hello"); // s1の値の所有権は、takes_and_gives_backにムーブする let s2 = takes_and_gives_back(s1); println!("s2 is {}", s2); } fn takes_and_gives_back(a_string: String) -> String { a_string // a_stringが返され、呼び出し元関数に所有権がムーブされる }
構造体
構造体のインスタンスを作成するとき、所有権はそのフィールドに移動します。構造体がドロップされると、そのフィールドの所有権も解放されます。 ※構造体には、参照を保持することもできますが、Rustのライフタイムという機能を使用する必要があります。ここでは省略しています。
struct Person { name: String, } fn main() { let name = String::from("Alice"); let person = Person { name }; //'name'の所有権が'person'に移動 // 所有権が移動しているので、ここでは'name'は利用できない }
列挙型
列挙型も構造体と同様に所有権を扱います。列挙型の列挙子が所有権を持つことができます。
enum Message { Text(String), Empty, } fn main() { let text = String::from("hello"); let message = Message::Text(text); // 'text'の所有権が'message'に移動 // 所有権が移動しているので、ここでは'text'は利用できない }
スライス
スライスは、コレクションの一部を参照するためのデータ構造です。スライスは、[start..end]
記法を使って作成されます。
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
部分的ムーブ
デストラクトの中で、 ムーブと参照の両方のバインディングを使用することができます。これにより、変数の一部がムーブされ、他の部分が参照されるという変数の部分的ムーブが発生した状態になります。
補足: ref
キーワードは構造体やタプルのフィールドへの参照を取得できます。参照を取得する&
と意味は同じで、let
と一緒に変数宣言時に利用できます。
struct Person { first_name: String, last_name: String, } fn main() { let person = Person { first_name: "John".to_string(), last_name: "Doe".to_string(), }; // デストラクトの中で、ムーブと参照の両方を使用している let Person { first_name, ref last_name } = person; println!("person full name is {} {}", first_name, last_name); //=> person full name is John Doe // first_nameの所有権は移動しているので、参照できない // println!("person first name is {}", person.first_name); println!("person last name is {}", person.last_name); //=> person last name is Doe }
まとめ
Rustの所有権システムは、メモリ安全性を保証するための強力な仕組みです。所有権、借用、参照などの概念を理解することで、Rustで効率的で安全なコードを書くことができます。最初は難しく感じるかもしれませんが、練習を重ねることで自然に理解できるようになります。
参考サイト