【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つあり、xyがそれぞれ所有している状態です。

コピー不可能

String,Vecなどはコピー不可です。

例として以下の処理を図で示します。

fn main() {
    let s = String::from("ABC");
    let t = s;
}

コピー不可の代入

このように、sからtABCの所有権が移っています。なので、所有権を失った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());
}

これを解決するのが「参照」です。

&ssへの参照を生成します。

参照のおかげで関数の引数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では参照が解放されたデータを指さないようにコンパイラが確認します。

以下のコードを見てください。rsの参照を得ています。しかし、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)

書いた人

profile_image

お茶の葉

物理とプログラミングが好きな人