【Rust】所有権・参照・借用・ライフタイムについて
投稿日: 更新日:
所有権とは
Rustの特徴的な機能に所有権があります。所有権はメモリをコンパイル時に管理するシステムです。
どんなプログラムもメモリを使用します。そして、使用されないメモリは解放する必要があります。解放には2つの方法があり、1つはガベージコレクション(GC)で自動的に、もう1つはコードを書く時に明示的に解放処理を書く方法です。
Rustでは第3の方法を用いています。それは、コンパイル時に所有権システムを通じて管理する方法です。
所有権のルール
3つあります。
- 各値に所有権がある
- いかなる時も所有権を持っているのは1つの変数のみ
- 所有者がスコープから外れたら値は破棄される
スコープとメモリ解放
Rustは変数のスコープが終了する時にメモリ解放を行います。スコープが終了する閉じ括弧で自動的にdrop
関数を呼び出してくれます。drop
関数はメモリ解放の処理を担っています。
{
let s = String::from("Hello world");// String::fromでメモリが確保される
}// sがスコープから外れ破棄される
明示的にメモリを解放する必要もなく、GCを利用することもなくメモリ解放の処理が行われます。
コピーとムーブ
コピー可能な変数と不可の変数があります。
コピー可能
i32
,bool
などCopy
トレイトを持つものはコピー可能です。
例として以下の処理を図で表します
let x: i32 = 1;
let y: i32 = x;
メモリ上に1
という値が2つあり、x
とy
がそれぞれ所有している状態です。
コピー不可能
String
,Vec
などはコピー不可です。
例として以下の処理を図で示します。
fn main() {
let s = String::from("ABC");
let t = s;
}
このように、s
からt
へABC
の所有権が移っています。なので、所有権を失ったs
は使用することが出来ません。
所有権が別の変数に移ることを「ムーブ」といいます。コピーされない大雑把な理由は、もし巨大なデータを持っていた場合、コピーに時間がかかり処理速度低下に繋がるからです。コピー可能なデータはi32
のような固定長で小さなデータです。
なぜ所有者が1人か
コピー不可の例をもう一度考えます。
ソースコードは以下の通りでした。
fn main() {
let s = String::from("ABC");
let t = s;
}
変数s
,t
がスコープを抜けた時に解放処理が行われます。どちらも同じ領域を指しているため、同じ領域に対し2回の解放処理が行われることになります。これは二重解放エラーが起きます。これを防止するために、s
を無効にすることで解放処理はt
のみとなり、二重解放を防ぎます。
参照と借用
関数に引数を渡すことを考えます。これは代入処理と似ています。そのため、関数側に所有権が移ります。
以下のコードのように、s
の所有権が関数側に移ってしまったのでその後s
が無効となってしまいました。
fn main() {
let s = String::from("Hello world");
print_len(s);
print!("{}", s);// sの所有権が関数に移ってしまい利用できない
}
fn print_len(t: String){
println!("{}", t.len());
}
これを解決するのが「参照」です。
&s
でs
への参照を生成します。
参照のおかげで関数の引数t
は所有権をもらうことなくデータを参照することができます。
fn main() {
let s = String::from("Hello world");
print_len(&s);// 参照を生成し関数に渡す
print!("{}", s);
}
fn print_len(t: &String){// 参照を受け取る
println!("{}", t.len());
}
このように関数の引数に参照をとることを「借用」といいます。
可変な参照
参照も変数と同様に不変です。よって、以下のコードはコンパイルエラーとなります。
fn main() {
let mut s = String::from("Hello world");
add(&s);
print!("{}", s);
}
fn add(t: &String){
t.push('!');// エラー:不変な参照で書き換えようとした
}
可変な参照を生成するには&mut s
と書きます。
修正すると以下のようになります
fn main() {
let mut s = String::from("Hello world");
add(&mut s);// 可変な参照を渡す
print!("{}", s);
}
fn add(t: &mut String){
t.push('!');
}
可変な参照にはルールがあります。
可変な参照は1つまで
以下のコードでは可変な参照を2つ生成しようとしてますがコンパイルエラーとなります。
fn main() {
let mut s = String::from("Hello world");
let r1 = &mut s;
let r2 = &mut s;
println!("{} {}", r1, r2);
}
不変な参照と可変な参照は同時生成できない
r1
で不変な参照を、r2
で可変な参照を得ようとしてますがコンパイルエラーになります。
fn main() {
let mut s = String::from("Hello world");
let r1 = &s;
let r2 = &mut s;
println!("{} {}", r1, r2);
}
この制約によりデータ競合を防ぐことができます。
データ競合とはメモリ上の同じ場所に同時に操作が行われて一貫性が失われた状態のことです。詳細は省きます。
ライフタイム
可変な参照は1つまでと言いましたが、以下のコードは正常に動きます。なぜでしょう
fn main() {
let mut s = String::from("Hello world");
let r1 = &mut s;//可変な参照を生成
r1.push('!');
let r2 = &mut s;//可変な参照を生成
r2.push('s');
println!("{}", s);
}
これには「ライフタイム」が関係します。ライフタイムは全ての参照が持ちます。
ライフタイムは参照が生成されてから最後に使用されるまでのことです。
fn main() {
let mut s = String::from("Hello world");
let r1 = &mut s;//--+ r1のライフタイム
r1.push('!'); //--+
let r2 = &mut s;//--+r2のライフタイム
r2.push('s'); //--+
println!("{}", s);
}
上記の例の場合r2
が生成される時にはr1
のライフタイムは終わっており、r2
だけが唯一の可変参照となるため生成することができます。
ダングリング参照
Rustでは参照が解放されたデータを指さないようにコンパイラが確認します。
以下のコードを見てください。r
はs
の参照を得ています。しかし、s
がスコープから出ても、r
のライフタイムは続いています。そのため、既に解放されたs
の参照をr
は持っていることになります。これではダングリング参照が起きてしまいます。
let r;
{
let s = String::from("hello");
r = &s;// sの参照をrに持たせた
}// sはスコープ外となり、sはドロップされる
dbg!(*r);// rのライフタイムがここまで続く
Rustはこのような事態を防ぐために、コンパイルエラーを出します。
ルールとしては、参照のライフタイムが元の変数(所有者)より長くなることは許されません。
参考文献
The Rust Programming Language 日本語版 https://doc.rust-jp.rs/book-ja/
(最終閲覧:2022/09/01)