引数とムーブ
引数として 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
関数の呼び出しによって次のようなことが起こります.
-
sum
関数を呼び出す前,vector
はベクタの所有権を持っています. -
sum
関数の開始時点で,ベクタの所有権がvector
からv
に移動します. -
sum
関数が終了するとき,所有権を持ったままv
のスコープが終了するためベクタがドロップされます. -
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
}
vector
は main
関数に属する変数であるのに対し, v
は sum
関数に属する変数です.異なる関数に属してはいますが, 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
関数を呼び出すときは,可変参照を渡します.たとえば hoge
が i32
型の変数なら
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
関数に属する変数 x
が main
関数に属する変数 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 引数がともに可変参照です.
どの関数においても,引数として受け取る参照 x
, y
のライフタイムは関数内で終了するので, 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)
を呼び出す前と後で, x
と y
の値が入れ替わっています.
この 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 つの値 i
と j
を受け取り, array[i]
と array[j]
の値を入れ替えようとしています.たとえば入力として array[1]
と array[3]
の値が入れ替わって [1, 4, 3, 2, 5]
と出力されると思うかもしれません.
実はこのコードはエラーになります.
もし入力が 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 回目のループで
x
に18
が足されて18
になった - 2 回目のループで
x
に19
が足されて37
になった - 3 回目のループで
x
に20
が足されて57
になった
という経過が明らかになります.
dbg!
マクロをコピー不可能な型に使うと,ムーブが起こります.よって,たとえば vector
がベクタの場合 dbg!(vector)
ではなく dbg!(&vector)
と参照渡しにすることでムーブを防ぎます.