Chapter 26

参照渡し

引数とムーブ

引数として Vec<i32> 型のベクタを受け取り,要素の総和を返す次のような関数を考えます.

fn sum(v: Vec<i32>) -> i32 {
    let mut ret = 0;
    for &i in &v {
        ret += i;
    }
    ret
}

この sum 関数を定義しておくと,たとえば

fn main() {
    let vector = vec![20, 80, 60, 40];
    let s = sum(vector);
    assert_eq!(s, 200);
}

のように使えます.プログラムの中でベクタの総和を求める処理が頻繁に登場するときに便利でしょう.

しかし,引数の型 Vec<i32> はコピー可能ではありません.よって, sum 関数の呼び出しによって次のようなことが起こります.

  1. sum 関数を呼び出す前, vector はベクタの所有権を持っています.
  2. sum 関数の開始時点で,ベクタの所有権が vector から v に移動します.
  3. sum 関数が終了するとき,所有権を持ったまま v のスコープが終了するためベクタがドロップされます.
  4. sum 関数が終了して main 関数に制御が戻ったとき,所有権を失った変数 vector だけが残ります.

よって, sum(vector) 以降 vector を使うことはできません.

fn main() {
    let vector = vec![20, 80, 60, 40];
    let s = sum(vector); // ムーブ

    // エラー: vector は所有権を持たない
    println!("{:?}", vector);
}

このようなムーブが起こってしまうのを防ぐために使うのが,この章で説明する参照渡しです.

参照渡し

sum 関数の引数を,ベクタではなく,ベクタへの参照に変えてみます.

fn sum(v: &Vec<i32>) -> i32 {
    todo!();
}

v の型は, Vec<i32> ではなく &Vec<i32> になります.

この関数を呼び出すときは,ベクタの代わりにその参照を渡します.

sum(&vector)

先ほどと違い,今度は vector を借用しているだけなので,ムーブが起こりません.よって,呼び出しの後も vector を使うことができます.

これを使って sum 関数を書き直すと,次のようになります.

fn main() {
    let vector = vec![20, 80, 60, 40];
    let s = sum(&vector);
    println!("{:?} の総和は {}", vector, s); // vector が使える
}

fn sum(v: &Vec<i32>) -> i32 {
    let mut ret = 0;
    for &i in v {
        ret += i;
    }
    ret
}

vectormain 関数に属する変数であるのに対し, vsum 関数に属する変数です.異なる関数に属してはいますが, v の参照を外して vector の中身を使用することは問題ありません.

このように,引数として変数そのものの代わりに変数への参照を渡すことを,引数の参照渡しといいます.これに対し,変数そのものを渡すことを,引数の値渡しといいます.

ライフタイム

上の sum 関数が引数として受け取った参照 &vector の値が最後に使用されるのは, sum 関数の中の for 式です.よって,この参照のライフタイムは, main 関数内で借用が起こった点で始まり, sum 関数の中で終わります(まず vector を借用し,次にそのアドレスを sum 関数に引数として渡しているので,借用自体が起こっているのは main 関数内です).

sum 関数の環境と main 関数の環境は切り離されています.独立した 2 つの環境の間で値がやり取りされるのは, main 関数が sum 関数に引数を渡すときと, sum 関数が main 関数に値を返すときだけです.これはすなわち,ある参照のライフタイムが関数間の境目を超えるのは,参照が引数となる場合と,参照が返り値となる場合に限られるということです.

今回, sum 関数は参照を引数として受け取るだけで,返り値は参照ではありません.この時点で, sum 関数が受け取った参照のライフタイムは sum 関数内で終了するということが判断できます.よって, main 関数内で sum(&vector) と書いても,参照のライフタイムが元の変数 vector のスコープを超えることはありえません.

返り値が参照であるような関数については,後の章で説明します.

可変参照

ここまで,引数として不変参照をとる関数を考えてきました.一方,引数として可変参照をとる関数も考えてみましょう.

次の関数を見てください.

fn double(x: &mut i32) {
    *x *= 2;
}

引数として, &mut i32 型の参照 x を受け取っています.そして,関数内で参照外しを行い,値を 2 倍にしています.

この double 関数を呼び出すときは,可変参照を渡します.たとえば hogei32 型の変数なら

double(&mut hoge)

となります.

よって,たとえば次のようになります.

fn main() {
    let mut hoge = 10;
    double(&mut hoge);
    assert_eq!(hoge, 20);
    double(&mut hoge);
    assert_eq!(hoge, 40);
}

fn double(x: &mut i32) {
    *x *= 2;
}

double 関数に属する変数 xmain 関数に属する変数 hoge を指しています.関数間の境界を超えて参照外し *x を行うことで, double 関数の中で main 関数の変数を間接的に書き換えることができています.これは,値渡しのときにはできなかったことです.

複数個の引数

複数個の参照引数を受け取る,次のような関数を考えます.

fn fnc1(x: &i32, y: &i32) {}
fn fnc2(x: &i32, y: &mut i32) {}
fn fnc3(x: &mut i32, y: &mut i32) {}

fnc1 は,第 1 引数と第 2 引数がともに不変参照です. fnc2 は,第 1 引数が不変参照で,第 2 引数が可変参照です. fnc3 は,第 1 引数と第 2 引数がともに可変参照です.

どの関数においても,引数として受け取る参照 xy のライフタイムは関数内で終了するので, main 関数の中に変数 hoge と変数 fuga があれば,これらの関数はそれぞれ

fnc1(&hoge, &fuga);
fnc2(&hoge, &mut fuga);
fnc3(&mut hoge, &mut fuga);

と呼び出すことができます.

一方,第 1 引数と第 2 引数に同じ変数への参照を渡し,

fnc1(&hoge, &hoge);
fnc2(&hoge, &mut hoge);
fnc3(&mut hoge, &mut hoge);

のように呼び出すことはできるでしょうか.

第 18 章で説明した通り,ある変数を同時に複数回借用できるのは,全ての借用が不変であるときに限ります.よって,

  • 2 回とも不変借用をしている fnc1(&hoge, &hoge)

は問題ありませんが,

  • 1 回目で不変借用, 2 回目で可変借用をしている fnc2(&hoge, &mut hoge)
  • 2 回とも可変借用をしている fnc3(&mut hoge, &mut hoge)

はエラーになります. fnc2 は,第 1 引数が可変借用で第 2 引数が不変借用だったときも同じです.

このように,複数個の参照引数を受け取る関数に,同じ変数への借用を渡して呼び出す際には,借用のルールに抵触していないか考える必要があります.

配列やベクタの借用

複数個の参照引数を受け取る関数として, std::mem::swap 関数というものがあります.これは, 2 つの可変参照を受け取り,その中身を入れ替えます.

fn main() {
    let mut x = 20;
    let mut y = 30;
    std::mem::swap(&mut x, &mut y);
    assert_eq!(x, 30);
    assert_eq!(y, 20);
}

std::mem::swap(&mut x, &mut y) を呼び出す前と後で, xy の値が入れ替わっています.

この std::mem::swap 関数を使った次のようなコードを考えてみます.

fn main() {
    proconio::input! {
        i: usize,
        j: usize,
    }
    let mut array = [1, 2, 3, 4, 5];
    std::mem::swap(&mut array[i], &mut array[j]);
    println!("{:?}", array);
}

入力として 2 つの値 ij を受け取り, array[i]array[j] の値を入れ替えようとしています.たとえば入力として i = 1, j = 3 を与えたら, array[1]array[3] の値が入れ替わって [1, 4, 3, 2, 5] と出力されると思うかもしれません.

実はこのコードはエラーになります.

もし入力が i = j = 2 だったらどうなるでしょう? array[2] を 2 回可変として借用することになってしまいます.このような事態を防ぐため,配列とベクタの借用については,さらに次のようなルールが定められています.

  • 配列/ベクタの要素が不変として借用されたら,配列/ベクタ全体が書き換え不可能になる.
  • 配列/ベクタの要素が可変として借用されたら,配列/ベクタ全体が使用不可能になる.

今回のように,配列やベクタの 2 つの要素を入れ替えたいときは, std::mem::swap 関数の代わりにスライスの章で紹介した別の swap 関数を使います.

fn main() {
    proconio::input! {
        i: usize,
        j: usize,
    }
    let mut array = [1, 2, 3, 4, 5];
    array.swap(i, j);
    println!("{:?}", array);
}

この場合の swap のような都合の良い関数がもし存在しなければ,この記事に書かれているように split_at_mut 関数などを使う必要があります.

dbg! マクロ

コードを書いている途中で,プログラムが問題なく動作しているか確認するために,変数の値を一時的に出力したいことがあります.このようなときには, dbg! マクロを使います.

たとえば次のコードを見てください.

fn main() {
    let mut x = 0;
    for i in 18..=20 {
        x += i;
    }
    println!("{}", x);
}

x に 18, 19, 20 を足して最後に出力しているので, 57 と出力されます.このコードを書いたときに,各ループで x の値がどう変化しているか追いかけたいとします.このようなとき, dbg! マクロを使って次のように書きます.

fn main() {
    let mut x = 0;
    for i in 18..=20 {
        x += i;
        dbg!(x);
    }
    println!("{}", x);
}

各ループの最後に, dbg!(x) と付け加えました.実行すると,標準出力と標準エラー出力に対してそれぞれ次のように出力されるはずです.

標準出力
57
標準エラー出力
[src/main.rs:5] x = 18
[src/main.rs:5] x = 37
[src/main.rs:5] x = 57

println! マクロが標準出力に 57 と出力する他に,毎ループの最後で dbg! マクロが x の値を標準エラー出力に出力しています. [src/main.rs:5] は, dbg! マクロが src/main.rs というファイルの 5 行目に記述されたものであることを表しています. dbg! マクロの出力を見ることで

  1. 1 回目のループで x18 が足されて 18 になった
  2. 2 回目のループで x19 が足されて 37 になった
  3. 3 回目のループで x20 が足されて 57 になった

という経過が明らかになります.

dbg! マクロをコピー不可能な型に使うと,ムーブが起こります.よって,たとえば vector がベクタの場合 dbg!(vector) ではなく dbg!(&vector) と参照渡しにすることでムーブを防ぎます.