所有権
概要
所有権とは、Rustの核となるメモリ管理システムです。
メモリのアドレス参照に対して、参照先の値に対する「唯一の所有者」を明確化し、その所有者がスコープを抜ける際に自動的にメモリ解放を行う仕組みを指します。
これにより、ガベージコレクションなしで安全かつ効率の良いメモリ管理が実現可能となり、C/C++で多発していたnull参照問題によるエラー抑止や処理速度の向上といったRustの特徴を生み出す形となっています。
所有権の発生
参照型の変数束縛時、ヒープメモリに対するメモリ確保が行われ、対象の変数に所有権が発生します。
fn main() {
let str = String::from("Hello, world!"); // ここで変数strに所有権が発生します
let str2 = str;
println!("{}", str2);
println!("{}", str);
}
所有権の移動
参照型の変数を代入する場合、通常のプログラミング言語であれば単純にアドレスがコピーされるだけです。
しかしながらRustでは、所有権は「唯一の所有者」によって保持されます。
複数の変数が同一の値に対して所有権(つまりは参照・更新権限)を持つことはできません。
ここが非常に重要なポイントなのですが、Rustでは代入を行なった場合、所有権が移動したものと見なします。
代入先の変数が新たな「所有権の唯一の所持者」となり、所有権を失った代入元の変数は値に対して参照・更新ができなくなります。
fn main() {
let str = String::from("Hello, world!");
let str2 = str; // ①所有権がstr2に移動します
println!("{}", str2); // ②str2が所有権を持っているので値参照ができます
println!("{}", str); // ③strは所有権を失っており参照できないため、この処理はコンパイル時にエラーとなります
}
末尾のコードを削除すれば問題なく動作することを確認してみてください。
fn main() {
let str = String::from("Hello, world!");
let str2 = str; // ①所有権がstr2に移動します
println!("{}", str2); // ②str2が所有権を持っているので値参照ができます
// 所有権を失ったstrに対する処理がないので問題なく実行できます
}
所有権に基づくメモリ解放
Rustでは、所有権を持つ変数がスタックメモリ上から解放(ここは他言語と同様、ブロック終了時に自動解放)されることで、自動的にヒープメモリの値もクリアされることになります。
fn main() {
let str = String::from("Hello, world!");
let str2 = str;
println!("{}", str2);
} // str2がスタックメモリから解放されると、参照先のヒープメモリの値もクリアされる
所有権の一時的な借用
所有権はメモリを無駄なく利用できるという点において非常に美しい仕組みです。
が、実際問題あちこちに所有権が移ってしまうと、それを追いかけるだけで一苦労になってしまいますし、管理地獄に陥ってしまいます。
そこで、Rustでは所有権を一時的に借りることができる仕組みがあります。
それが所有権の借用と呼ばれるものです。
共有参照
共有参照とは、値を変更しないので読取権限だけ一時的に貸してください、という所有権の借用方法です。
変数の前に&をつけることで表すことができます。
fn main() {
let str = String::from("Hello, world!");
let str2 = &str; // ①所有権はstr2に移動せず、参照権限だけstr2が一時的に借用します
println!("{}", str2); // ②str2は参照権限を借用しているので読取OK
println!("{}", str); // ③strは所有権を失っていないので、この場合はエラーになりません
}
内部的には所有権を持つ変数に対するアドレス参照を行なっており、借用側が直接アドレスを保持したり値をコピーして新規にメモリ確保したりしないようになっています。
fn main() {
let str = String::from("Hello, world!"); // ヒープメモリに文字列を保持、strはヒープメモリのアドレスを保持
let str2 = &str; // str2はstrのスタックメモリのアドレスを保持。ヒープメモリは参照しない
println!("{}", str2);
println!("{}", str);
}
共有参照は値を勝手に書き換えてしまわないことが約束されていることから、いくつでも参照を作ることが可能です。
fn main() {
let str = String::from("Hello, world!");
let str2 = &str;
let str3 = &str; // 読取権限は複数の変数で借用してもOK!
println!("{}", str2);
println!("{}", str3);
println!("{}", str);
}
ただし、借用中に所有権を持つ変数が値を書き換えることは許容されていません。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &str;
str.push_str("!!"); // str2が借用中なので、更新はコンパイルエラーとなる
println!("{}", str2);
println!("{}", str);
}
これは、本来想定していた値とは異なる値を参照して処理してしまうリスクを回避し、安全性を保つための制約となります。
可変参照
可変参照とは、読取権限だけでなく値を更新する権限も一時的に貸してください、という所有権の借用方法です。
変数の前に&mutをつけることで表すことができます。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &mut str; // ①所有権はstr2に移動せず、参照・更新権限をstr2が一時的に借用します
str2.push_str("!!"); // ②str2は更新権限を持っているので、ヒープメモリの値変更が可能です
println!("{}", str2); // ③str2は読取権限も借用しているのでOK
println!("{}", str); // ④str2によって値が書き換えられているので、str2と同じ結果が出力されます
}
こちらも内部的には所有権を持つ変数に対するアドレス参照を行なっており、借用側が直接アドレスを保持したり値をコピーして新規にメモリ確保したりしないようになっています。
注意点として、可変参照が可能なのはひとつの変数のみです。
共有参照と異なり、複数の変数が借用することはできません。
また、いずれかの変数が可変参照中は共有参照も不可となります。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &mut str;
str2.push_str("!!");
let str3 = &mut str; // str2が可変参照中なので、別変数の可変参照はNG
str3.push_str("!!");
println!("{}", str2);
println!("{}", str3);
println!("{}", str);
}
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &mut str;
str2.push_str("!!");
let str3 = &str; // str2が可変参照中なので、他変数の共有参照もNG
println!("{}", str2);
println!("{}", str3);
println!("{}", str);
}
こちらも、あちこちで勝手に値が書き換えられたり、本来想定していた値とは異なる値を参照して処理してしまったりするリスクを回避し、Rustが安全性を保つための制約となります。
ライフタイム
共有参照中は所有権を持つ変数であっても値の更新ができない。
また、可変参照が可能なのはひとつの変数のみ、しかも可変参照中は共有参照不可……。
こうなると、所有権の縛りきつすぎて何もできなくない? と感じる方もいるかもしれません。
しかし、ここでポイントとなるのは、「共有参照中」とか「可変参照中」ってどこからどこまでの期間を示すのだろう? ということです。
ここで登場するのがライフタイムという概念です。
ライフタイムとは文字通り変数の生存期間を意味するものです。
この生存期間は原則として「変数束縛〜変数が最後に利用されるまで」になります。
ライフタイムを過ぎるとその変数は無効と見なされ、借用していた権限を返します。
これにより、値の更新不可や共有参照不可といった制限が解除されます。
共有参照の権限が返却されて更新可能になる例
下記の場合ではコンパイルエラーになってしまいますが、
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &str; // ①str2のライフタイム開始
str.push_str("!!"); // ②str2がまだ生存中で借用権限が活きているのでNG
println!("{}", str2); // ③str2が最後に利用されるところなので、ここでライフタイム終了
println!("{}", str);
}
下記の場合はstr2のライフタイムを過ぎたタイミングで更新処理を行なっているので、コンパイルエラーになりません。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &str; // ①str2のライフタイム開始
println!("{}", str2); // ②str2が最後に利用されるところなので、ここでライフタイム終了
str.push_str("!!"); // ③str2は権限返却済なので、strは更新処理が可能
println!("{}", str);
}
可変参照の権限が返却されて別変数が可変参照可能になる例
下記の場合ではコンパイルエラーになってしまいますが、
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &mut str; // ①str2のライフタイム開始
str2.push_str("!!");
let str3 = &mut str; // ②str2がまだ生存中で借用権限が活きているのでNG
str3.push_str("!!");
println!("{}", str2); // ③str2が最後に利用されるところなので、ここでライフタイム終了
println!("{}", str3);
println!("{}", str);
}
下記の場合はstr2のライフタイムを過ぎたタイミングで可変参照を新たに行なっているので、コンパイルエラーになりません。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &mut str; // ①str2のライフタイム開始
str2.push_str("!!");
println!("{}", str2); // ②str2が最後に利用されるところなので、ここでライフタイム終了
let str3 = &mut str; // ③str2は権限返却済なので、str3は新たに可変参照が可能
str3.push_str("!!");
println!("{}", str3);
println!("{}", str);
}
借用チェッカー
このライフタイムおよび所有権のチェックは全てコンパイル時に行われ、プログラムの実行前時点でメモリの安全性が担保されます。
借用チェッカーと呼ばれるこの仕組みはRustの強固な堅牢性を支える優秀なコア機能です。
が、悪名高いビルド時間の長さにも繋がっているのがジレンマでもあります。
共有参照に対する共有参照も可能
共有参照に対してさらに共有参照することも可能です。
この場合、メモリ参照が連結している形となります。
fn main() {
let mut str = String::from("Hello, world!");
let str2 = &str; // strのスタックメモリをアドレス参照
let str3 = &str2; // str2のスタックメモリをアドレス参照
println!("{}", str3); // アドレス参照を繰り返すことでちゃんと値の参照が可能
println!("{}", str2);
str.push_str("!!");
println!("{}", str);
}
参照外し
参照外しとは、スタックメモリにアドレスを保持している場合に、アドレスの参照先の値を取り出すことを言います。
あれ、でも今まで特に参照外しとかせずに普通に値参照してたよね? と思われた方は非常に鋭いです。
Rustには二種類の参照外しがあります。
- 明示的な参照外し
- 暗黙の参照外し
これまでは暗黙の参照外しにより、開発者が意識せずともアドレスの先にある値を取り扱うことができていた、というのが真相になります。
実際のところ明示的な参照外しが必要な場面はあまり多くないですが、いざという時に学んでおきましょう。
暗黙の参照外し
Rustでは暗黙の参照外しが発生するパターンが2つあります。
- 構造体のフィールドアクセス
- 関数・メソッド内で参照外しが行われている
これまで使ってきていたprintln!という処理は後者になっており、Displayトレイトの実装内で明示的に参照外しが行われています。
前者については、構造体のフィールドにアクセスする=前段階として構造体自体のアドレス参照が必ず必要であるため、Rust側が自動的に参照外しを行います。
struct Person {
name: String,
}
fn main() {
let x = Person { name: String::from("Alice") }; // ヒープメモリのアドレス参照
let y = x.name; // xの参照外しをRustが暗黙的に行い、nameフィールドにアクセス
println!("{}", y);
}
明示的な参照外し
明示的に参照外しを行うには*を変数の前に記述します。
少々強引な実装(というか参照する意味がない処理)ですが、例を見てみましょう。
fn main() {
let x = 5;
let y = &x;
let z = &y;
println!("{}", y + 1); // &i32はAddトレイトを実装している(内部で参照外し)ので、+演算=y.Add(1)が可能
println!("{}", z + 1); // &&i32はAddトレイトを実装していないので、+演算=z.Add(1)が不可能によりエラー
}
y + 1はコンパイルエラーにはなりませんが、z + 1はコンパイルエラーになります。
分かれ目になるのはコメントで記載している通り、+演算するために必要なAddというトレイトを実装しているかどうかです。
前者はRust言語仕様としてデフォルト実装されており、メソッド内で参照外しが行われています(つまり暗黙の参照外し)。
後者は実装されていないため、演算ができない形となります。
こういった場合、明示的に参照外しを行うことで、&という参照を外す必要があります。
結果として、下記であれば問題なく実行できます。
fn main() {
let x = 5;
let y = &x;
let z = &y;
println!("{}", *z + 1); // *で明示的な参照外し
}
お疲れ様でした!
お疲れ様でした。
Rustの所有権システム、理解できましたでしょうか?
もしうまく飲み込めていなかったら力不足で申し訳ないですが、ここが最難関で後は何とでもなる想定なので、色々試したりAIに聞いたりしながら学習してみてください。
次回は、RustのResult・Option・クロージャ・スライスについて学びます。
Rust入門講座④Result・Option・クロージャ・スライス
Hope you enjoy it!