S-JIS[2024-09-16/2024-09-19] 変更履歴

Rust 所有権メモ

Rustの所有権のメモ。


概要

所有権は値と変数に関わる話だが、Copyトレイトを実装している型については関係ない。
つまり、基本データ型(整数や浮動小数など)はCopyトレイトが実装されているので、所有権を気にする必要は無い。

所有権が関わるのは、以下のような、値の移送を行う箇所。

このような場合に値の移送を行う必要があるが、Copyトレイトが実装されている型の場合は、単純に値がコピー(複製)できる。(コピーセマンティクスと言う)

Copyトレイトが実装されていない型については、所有権の移動(ムーブ)を伴う。(ムーブセマンティクスと言う)
ただし、常に所有権が移動してしまうのも困るので、所有権が移動しないようにする方法はある。→借用


自分はRust初心者なので間違っているかもしれないが、Copyトレイトが実装されていない場合に所有権が関係することについて、以下のように理解している。

  1. Copyトレイトが実装されていない型では、データ本体はヒープ(グローバルな領域)にメモリーが確保されてそこに置かれることが多く、その場合、変数はそのデータへのポインターを保持する。
    この場合は、値の移送は(データ本体は動かさず)ポインターのコピーとなる。
  2. ヒープにメモリーが確保された場合、誰かが最終的に一回だけヒープ上のデータを破棄(確保したメモリーを解放)しなければならない。そのため、メモリーを解放する責務(誰がメモリーを解放するのか)も管理する必要がある。
    この責務が所有権である。

なお、所有権はコンパイル時にチェックされるものであって、コンパイル結果の実行コードの中に(所有権のフラグのような情報が)含まれているわけではない。


所有権の解説

所有権の基本的な考え方は単純。

値を変数に代入すると、変数がその値を所有する(所有権を持つ)。
所有権を持った変数がスコープから外れると、値が破棄される。(デストラクター(Dropトレイトのdropメソッド)が呼ばれる)

    { // スコープの始まり
        let s = "abc";         // sが"abc"の所有権を持つ
        println!("s = {}", s);
    } // スコープが終わるので、sが所有権を持っている"abc"は破棄される

これは、Javaのtry-with-resourcesと似ていると思う。

// Java
    try (var resource = 〜) { // tryでresource(変数)を宣言する
        〜
    } // tryブロックが終わるとき、tryで宣言された変数に対してcloseメソッドが呼ばれる(リソースを解放する)

Javaではtry文で指定した変数しか対象にならないが、
Rustでは(Copyトレイトが実装されていない型の)全ての変数に対して自動的に対象になるという感じ。


ひとつの値(インスタンス)に対し、所有権を持つ変数は常にひとつのみである。

別の変数へ代入を行うと、所有権はその変数に移る。
関数の引数に渡すと、所有権はその引数に移る。

所有権を失った変数は使用できなくなる。(使うようなコーディングをするとコンパイルエラーになる)

    {
        let s1 = String::from("abc"); // s1が所有権を持つ
        let s2 = s1;                  // 所有権がs2に移る。以降、s1は使えない
//×    println!("s1 = {}", s1);      // s1は所有権を失っているので使えない。使おうとするとコンパイルエラーになる
        println!("s2 = {}", s2); 
    } // スコープが終わるとき、s2が所有権を持っている値は破棄される。s1は所有権を持っていないので何もしない

別の見方をすると、「所有権を持つ変数は、値を破棄する責務がある」と言えるだろう。
所有権を移すと、破棄する責務を渡したということになる。


変数から別の変数へ代入するのはあまりしないような気がするが、
複数の関数やメソッドに引数として渡すことはよくある。

しかし、最初の関数呼び出しの引数で所有権が移動してしまうと、次の関数に渡せなくなってしまう!

fn func1(s: String) {
    println!("func1.s = {}", s); 
}
    let s = String::from("abc");
    func1(s); // 所有権がfunc1の引数に移る。以降、sは使えない
    func2(s); // sは所有権を失っているので使えない。コンパイルエラーになる

こういった場合に備えて、所有権を渡さずに残したまま借用するという方法がある。


借用

関数やメソッドを呼び出す際に引数に所有権を渡したくない場合は、参照で渡すという方法がある。

関数やメソッドで引数を定義する際に、型の前に「&」を付けると参照で受け取ることになる。

fn func1(s: &String) {
    println!("func1.s = {}", s); 
}

関数の引数を参照で受け取ることを借用と呼ぶ。
所有権は元の変数に残したまま、値だけを借りるというニュアンスだろうか。


参照を受け取る引数に変数を渡すには、変数の前に「&」を付ける。

    let s = String::from("abc");
    func1(&s); // func1には参照が渡され、所有権はsに残る
    func2(&s); // func2には参照が渡され、所有権はsに残る

関数の引数の定義方法


参照外し

参照(「&」付き)で受け取っても、大抵は「&」無しと同様に扱えるが、「*」を付けると参照を外すことが出来る。[2024-09-19]

fn func(s: String, r: &String) {
    println!("r.len = {}", r.len());	// &付きの変数でも、&無しと同様に扱える
    println!("s.len = {}", s.len());

    println!("*r.len = {}", (*r).len());	// 参照外し
    println!("*s.len = {}", (*s).len());	// &付きでないのに、これも通る^^;
}

例えばSomeに参照を渡すと、unwrapしたものも参照になる。こういった場合に参照を外すには「*」を付ける。

    let s = Some(&123);	// sの型は Option<&i32>
    let r = s.unwrap();	// rの型は &i32
    let n = *r;        	// nの型は i32

Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま