【Rust】借用と参照、ライフタイム

1. 概要 

今回はRustの借用と参照について学んでいきたいと思います。

2. 借用と参照

所有権は、関数の引数を渡すことでも移動します。ですが、関数を呼び出すたびに所有権が移動してしまうと、変数をミュータブルで宣言しなければいけないですし、関数の戻りを再代入するなどの手間が増えます。そこで、所有権を一時的に貸し出すことができる仕組みがあります。これを借用と言います。

まずは、関数の呼び出しで所有権が移動するのかを確認してみましょう。

fn main() {
    // Vec<String>型のオブジェクト作成
    let vect = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡す
    vev_print(vect);

    // vecの要素ごとに出力
    for str in vect {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>を出力する
 */
fn vev_print(vect: Vec<String>) {
    println!("{:?}", vect);
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0382]: use of moved value: `vect`
 --> src/main.rs:9:16
  |
3 |     let vect = vec![String::from("a"), String::from("b"), String::from("c")];
  |         ---- move occurs because `vect` has type `Vec<String>`, which does not implement the `Copy` trait
...
6 |     vev_print(vect);
  |               ---- value moved here
...
9 |     for str in vect {
  |                ^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership-rust` due to previous error

所有権が移動しているのでエラーになりました。関数vec_printに変数vectの所有権が移動しているので、未初期化の状態です。なので、9行目のfor文では使用できないのでエラーになっています。

前回にもしましたが、関数の呼び出し元に所有権を戻すようにしてみます。

fn main() {
    // Vec<String>型のオブジェクト作成
    let mut vect = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvectを渡して、関数vec_printから戻り値を受け取る
    vect = vev_print(vect);

    // vecの要素ごとに出力
    for str in vect {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>を出力し、呼び出し元に値を返す
 */
fn vev_print(vect: Vec<String>) -> Vec<String> {
    println!("{:?}", vect);
    vect
}

実行します。

cargo run
   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/ownership-rust`
["a", "b", "c"]
a
b
c

今回は、関数vec_printから値を戻したので、問題なく実行できました。ですが、上記でも述べたように、値を参照したいだけなのに毎回所有権の移動が起きてしまうと面倒です。そこで、関数の引数に値の参照を渡すことで手間を省くことができます。

fn main() {
    // Vec<String>型のオブジェクト作成
    let vect = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡す
    vev_print(&vect);

    // vecの要素ごとに出力
    for str in vect {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>を出力する
 */
fn vev_print(vect: &Vec<String>) {
    println!("{:?}", vect);
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/ownership-rust`
["a", "b", "c"]
a
b
c

問題なく実行できました。値を借用する場合は「&」を指定します。今回のプログラムでは変数vectに&を指定することで、vectの値を借用しています。所有権を移動しているのではなく一時的に借りているだけなので、関数vec_printの処理が終わった時点で、貸し出した値は返ってきます。なので、その後のfor文の処理を問題なく行うことができています。

次に、参照には2種類あります。共有参照可変参照です。上記のプログラムは共有参照です。次は可変参照について確認してみましょう。

fn main() {
    // Vec<String>型のオブジェクト作成
    let mut vect = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡す
    vev_print(&mut vect);

    // vecの要素ごとに出力
    for str in vect {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>を出力し、vectに要素を追加する
 */
fn vev_print(vect: &mut Vec<String>) {
    println!("{:?}", vect);
    vect.push(String::from("d"))
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/ownership-rust`
["a", "b", "c"]
a
b
c
d

Rustで変数を可変にする場合は、「mut」を指定します。可変参照を関数に渡す場合にも同じく「mut」を指定します。書き方は「&mut 変数」です。関数側にも「引数: $mut 型」と書きます。これで借用した値の変更ができます。気をつけるところは、変数を定義するときです。可変にしておかないと関数vec_printで借用している値にpushして要素を追加することは、初期化以外の動作なのでエラーになります。

参照渡しと値渡し

Rustだけではなく、他の言語にも関数を呼び出す際に、引数に指定した変数の参照を与える「参照渡し」と値そのものを関数に受け渡す「値渡し」があります。値渡しの場合は、引数に指定した値がそのまま渡るわけではなく、値がコピーされて関数に渡されます。なので、その関数の中でどれだけその値を加工したとしても、呼び出し元には何ら影響はりません。ですが、参照渡しは関数に対して、値の「参照(変数が差しているアドレス)」が渡されます。参照渡しの場合は、関数の中で値を加工すると、呼び出し元の値も変更されます。Rustで参照渡しを行う場合は、「&」を指定します、ただし、値渡しをする場合は所有権システムが適用されます。前回にも言ったようにプリミティブ型にはコピートレイトが実装されているので、他の言語のように値のコピーが渡されますが、それ以外の型は所有権の移動が行われます。

参照外し

「& mut」の可変参照では、「*」演算子を使用して参照を外す(dereference)ことで所有者の値を設定することができます。

fn main() {
    // ミュータブルな変数を定義
    let mut num1 = 10;

    // 参照を渡す
    let n = &mut num1;

    // 参照を外し、num1の値を設定する
    let mut num2 = *n;

    // 2倍にする
    num2 *= 2;

    println!("num1 = {}", num1);
    println!("num2 = {}", num2);
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.50s
     Running `target/debug/ownership-rust`
num1 = 10
num2 = 20

問題なく実行できました。

Rustは暗黙的に参照外しをすることができます。方法は、タプル型であれば「tuple.0」で要素にアクセスすることがあります。その場合、.(ピリオド)の左側の値は暗黙的に参照外しされます。

fn main() {
    let tuple = ("1", 2, [3]);
    let tuple_ref = &tuple;

    println!(
        "value = {:?}, type = {}",
        tuple_ref.0,
        get_type(tuple_ref.1)
    );
}

// 型の名前を取得
fn get_type<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/ownership-rust`
value = "1", type = i32

出力結果の型の名前を見てもらうと「i32」になっていて参照が外れていることがわかります。参照が外れていない場合は、「&i32」と表示されます。

fn main() {
    let num1 = 10;
    let n = &num1;

    println!("type = {}", get_type(n));
}

// 型の名前を取得
fn get_type<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
     Running `target/debug/ownership-rust`
type = &i32

3. ライフタイム

ライフタイムとは、値の参照(借用)が有効な範囲(スコープ)のことです。ライフタイムの主な目的は、Dangling Referenceを回避することです。ライフタイムを注釈する場合は、<‘a>のようなライフタイム注釈記法を使用します。値の参照が一般的な参照の場合は、暗黙的にライフタイムが推論されます。

ライフタイムのルール

  • ある変数の参照は、その変数よりも長生きできない。なぜなら、参照先の変数が先にドロップした場合、Dangling Referenceを指すため
  • ある変数に格納した参照は、その格納した変数と同じだけ生きなければいけない。なぜなら、参照が先にドロップした場合、Dangling Referenceを指すため

まずは、暗黙的にライフタイムが推論されているかを、実際にプログラムを見てみましょう。

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    } // 変数xのライフタイムはここまで

    println!("r: {}", r);
} // 変数rのライフタイムはここまで

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     } // 変数xのライフタイムはここまで
  |     - `x` dropped here while still borrowed
8 | 
9 |     println!("r: {}", r);
  |                       - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `ownership-rust` due to previous error

エラーになりました。原因は変数xのライフタイムが短いことです。このライフタイムのチェックはコンパイル時に、Borrow Checkerでしています。

Borrow checkerがどのようにDangling Referenceをチェックしているのかを確認してみましょう。

fn main() {
    {
        let r;                // ---------+-- 'a
                              //          |
        {                     // -+-- 'b  |
                              //  |       |
            let x = 5;        //  |       |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         //          |
}                             // ---------+

変数rのライフタイムは<‘a>、変数xのライフタイムは<‘b>と注釈したとします。コンパイル時に、ライフタイムのサイズをBorrow Checkerで比較してrは<‘a>のライフタイムだけど、<‘b>のライフタイムのメモリを参照していることを確認します。この時に、参照しているxのライフタイムがrのライフタイムより短いのでエラーになっています。

解決するためには、次のように修正します。

fn main() {
    {
        let x = 5;            // ----------+-- 'b
                              //           |
        let r = &x;           // --+-- 'a  |
                              //   |       |
        println!("r: {}", r); //   |       |
                              // --+       |
    }                         // ----------+
}                             // ---------+

今回の場合、変数xのライフタイム<‘b>は変数rのライフタイム<‘a>よりも大きいです。なのでxが有効な間は参照が可能なのでコンパイルエラーになることなく実行できます。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/ownership-rust`
r: 5

関数のライフタイム

まずはエラーになるパターンを確認します。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    // 最長の文字列は、{}です
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0106]: missing lifetime specifier
  --> src/main.rs:10:33
   |
10 | fn longest(x: &str, y: &str) -> &str {
   |               ----     ----     ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
   |
10 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
   |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership-rust` due to previous error

原因ですが、まずはエラー内容を確認して見ましょう。「expected named lifetime parameter」戻り値がライフタイムパラメーターである必要があると書かれています。なぜこのエラーが出力されているかというと、コンパイルエラーにはどちらの引数の参照が戻り値として返却されるかを判断ができず、Borrow Checkerでの検証ができないからです。

この問題を解決するには、ヒントにもあるように、明示的にライタイムを注釈します。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    // 最長の文字列は、{}です
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 2.52s
     Running `target/debug/ownership-rust`
The longest string is abcd

今度は問題なく実行できました。

4. まとめ

今回は、借用とライフタイムについてやっていきました。正直、まだまだ理解ができてないです。今回学んだことはこれからも使う頻度が高いので手を動かしながら覚えていくしかないかなと言った感じです。幸い、Rustのコンパイラーは親切でヒントが出てきます。エラーが発生したら、ヒントの内容を確認して修正するのが良いですね。

5. 参考

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html

https://qiita.com/deta-mamoru/items/e7d0b52f56b23b403c62

投稿者プロフィール

OkawaRyouta
最新の投稿

関連記事

  1. 【Rust】変数、定数、シャドーイング

  2. 【Rust】所有権システム、所有権の移動

  3. 【Rust】文字列型

  4. 【Rust】ベクタ、スライス

  5. 【Rust】整数型、浮動小数点型

  6. 【Rust】構造体

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. AWS
  5. AWS
  6. AWS
  7. AWS

制作実績一覧

  1. Checkeys